forgeos 0.1.0-alpha.63 → 0.1.0-alpha.64

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.
Files changed (63) hide show
  1. package/AGENTS.md +1 -1
  2. package/CHANGELOG.md +19 -0
  3. package/docs/agent-fabric.md +252 -0
  4. package/package.json +7 -4
  5. package/schemas/agent-fabric/v0.1/control-event.schema.json +281 -0
  6. package/scripts/field-test-forgeos.mjs +11 -2
  7. package/src/forge/_generated/releaseManifest.json +1 -1
  8. package/src/forge/_generated/releaseManifest.ts +3 -3
  9. package/src/forge/agent-adapters/index.ts +18 -14
  10. package/src/forge/agent-adapters/types.ts +1 -1
  11. package/src/forge/agent-fabric/adapter.ts +131 -0
  12. package/src/forge/agent-fabric/authority.ts +298 -0
  13. package/src/forge/agent-fabric/canonical.ts +135 -0
  14. package/src/forge/agent-fabric/conductor.ts +754 -0
  15. package/src/forge/agent-fabric/dictionary.ts +12 -0
  16. package/src/forge/agent-fabric/errors.ts +32 -0
  17. package/src/forge/agent-fabric/hardened-conductor.ts +485 -0
  18. package/src/forge/agent-fabric/hardened-reducer.ts +211 -0
  19. package/src/forge/agent-fabric/index.ts +18 -0
  20. package/src/forge/agent-fabric/journal.ts +116 -0
  21. package/src/forge/agent-fabric/local-approval-window.ts +169 -0
  22. package/src/forge/agent-fabric/local-coding-worker.ts +190 -0
  23. package/src/forge/agent-fabric/local-control-store.ts +239 -0
  24. package/src/forge/agent-fabric/local-paths.ts +18 -0
  25. package/src/forge/agent-fabric/local-task-contract.ts +142 -0
  26. package/src/forge/agent-fabric/local-task-inbox.ts +199 -0
  27. package/src/forge/agent-fabric/local-task-server.ts +207 -0
  28. package/src/forge/agent-fabric/local-task-service.ts +494 -0
  29. package/src/forge/agent-fabric/p0a.ts +104 -0
  30. package/src/forge/agent-fabric/p0b-model-adapter.ts +404 -0
  31. package/src/forge/agent-fabric/planning.ts +155 -0
  32. package/src/forge/agent-fabric/reducer.ts +863 -0
  33. package/src/forge/agent-fabric/resource-ledger.ts +266 -0
  34. package/src/forge/agent-fabric/serialized-local-adapter.ts +60 -0
  35. package/src/forge/agent-fabric/types.ts +424 -0
  36. package/src/forge/agent-fabric/validation.ts +500 -0
  37. package/src/forge/agent-memory/bridge.ts +82 -52
  38. package/src/forge/agent-memory/mcp.ts +59 -9
  39. package/src/forge/agent-memory/sources/codex-hook-runner.mjs +12 -113
  40. package/src/forge/cli/commands.ts +4 -0
  41. package/src/forge/cli/fabric.ts +83 -0
  42. package/src/forge/cli/main.ts +7 -0
  43. package/src/forge/cli/new.ts +16 -4
  44. package/src/forge/cli/parse.ts +21 -0
  45. package/src/forge/cli/verify.ts +4 -1
  46. package/src/forge/compiler/app-graph/tsconfig-hash.ts +41 -4
  47. package/src/forge/compiler/package-graph/dts-extractor.ts +14 -1
  48. package/src/forge/impact/index.ts +18 -21
  49. package/src/forge/runtime/ai/providers.ts +19 -4
  50. package/src/forge/version.ts +1 -1
  51. package/templates/agent-workroom/package.json +1 -0
  52. package/templates/agent-workroom/pnpm-workspace.yaml +2 -0
  53. package/templates/b2b-support-web/package.json +1 -0
  54. package/templates/b2b-support-web/pnpm-workspace.yaml +2 -0
  55. package/templates/b2b-support-web/web/package.json +1 -1
  56. package/templates/minimal-web/package.json +1 -0
  57. package/templates/minimal-web/pnpm-workspace.yaml +2 -0
  58. package/templates/nuxt-web/.yarnrc.yml +1 -0
  59. package/templates/nuxt-web/package.json +1 -0
  60. package/templates/nuxt-web/pnpm-workspace.yaml +2 -0
  61. package/templates/nuxt-web/web/package.json +1 -0
  62. package/templates/vendor-access/package.json +1 -0
  63. package/templates/vendor-access/pnpm-workspace.yaml +2 -0
@@ -0,0 +1,754 @@
1
+ import { getOwn } from "./dictionary.ts";
2
+ import {
3
+ assertAuthorizationCurrent,
4
+ assertGrantLineageCurrent,
5
+ assertRootGrantAuthorized,
6
+ deriveExecutionGrant,
7
+ } from "./authority.ts";
8
+ import { digestCanonical, sha256Digest, stableStringify } from "./canonical.ts";
9
+ import { AgentFabricError } from "./errors.ts";
10
+ import type { ControlJournal } from "./journal.ts";
11
+ import { applyPlanDelta, computeRunPlanContentDigest, validateWorkflowNodes } from "./planning.ts";
12
+ import { replayControlState } from "./reducer.ts";
13
+ import type { ResourceLedger } from "./resource-ledger.ts";
14
+ import {
15
+ validateWorkerResultReport,
16
+ } from "./validation.ts";
17
+ import type {
18
+ AttemptExecutionPermit,
19
+ AttemptUncertaintyObservation,
20
+ AuthoritativeOutcomeCommit,
21
+ AuthorityResolution,
22
+ Clock,
23
+ DerivedGrantRequest,
24
+ DigestFunction,
25
+ DispatchIntent,
26
+ DispatchOffer,
27
+ ExecutionGrant,
28
+ ExecutorStartupReport,
29
+ GoalContract,
30
+ OwnerAuthorization,
31
+ OwnerAuthorizationVerifier,
32
+ PlanDelta,
33
+ ResourceDefinition,
34
+ ResourceReservation,
35
+ RunPlanRevision,
36
+ SchedulingClaim,
37
+ WorkerResultReport,
38
+ } from "./types.ts";
39
+
40
+ export interface ClaimDispatchInput {
41
+ claimId: string;
42
+ intentId: string;
43
+ workerId: string;
44
+ attemptId: string;
45
+ leaseDurationMs: number;
46
+ }
47
+
48
+ export interface IssuePermitInput {
49
+ permitId: string;
50
+ claimId: string;
51
+ grantId: string;
52
+ maximumValidityMs: number;
53
+ }
54
+
55
+ function sortedDefinitions(definitions: Readonly<Record<string, ResourceDefinition>>): ResourceDefinition[] {
56
+ return Object.values(definitions)
57
+ .map((definition) => ({ ...definition }))
58
+ .sort((left, right) => left.resource.localeCompare(right.resource));
59
+ }
60
+
61
+ export class ForgeAgentConductor {
62
+ constructor(
63
+ private readonly rootExecutionId: string,
64
+ private readonly journal: ControlJournal,
65
+ private readonly clock: Clock,
66
+ // Retained for the draft P0a constructor shape. Normative P0a digests are fixed to SHA-256.
67
+ _digest: DigestFunction,
68
+ private readonly ownerAuthorizationVerifier: OwnerAuthorizationVerifier,
69
+ private readonly resourceLedger?: ResourceLedger,
70
+ ) {}
71
+
72
+ state() {
73
+ return replayControlState(this.journal.readAll(), {
74
+ ownerAuthorizationVerifier: this.ownerAuthorizationVerifier,
75
+ resourceDefinitions: this.resourceLedger
76
+ ? sortedDefinitions(this.resourceLedger.snapshot().definitions)
77
+ : undefined,
78
+ });
79
+ }
80
+
81
+ events() {
82
+ return this.journal.readAll();
83
+ }
84
+
85
+ registerOwnerAuthorization(authorization: OwnerAuthorization): void {
86
+ if (authorization.rootExecutionId !== this.rootExecutionId) {
87
+ throw new AgentFabricError("AF_GRANT_REJECTED", "Owner authorization belongs to another execution");
88
+ }
89
+ assertAuthorizationCurrent(authorization, this.clock.now());
90
+ const authorizationDigest = digestCanonical(authorization, sha256Digest);
91
+ const verification = this.ownerAuthorizationVerifier.verify(authorization, authorizationDigest);
92
+ if (
93
+ verification.authorizationDigest !== authorizationDigest ||
94
+ !this.ownerAuthorizationVerifier.verifyRecorded(authorization, verification)
95
+ ) {
96
+ throw new AgentFabricError(
97
+ "AF_GRANT_REJECTED",
98
+ "Owner authorization verification is not trusted for these authorization bytes",
99
+ );
100
+ }
101
+ this.append(`owner-authorization:${authorization.authorizationId}`, {
102
+ type: "owner_authorization_registered",
103
+ authorization,
104
+ verification,
105
+ });
106
+ }
107
+
108
+ revokeOwnerAuthorization(authorizationId: string, reason: string): void {
109
+ const state = this.state();
110
+ if (!getOwn(state.authorizations, authorizationId)) {
111
+ throw new AgentFabricError("AF_NOT_FOUND", `Unknown owner authorization: ${authorizationId}`);
112
+ }
113
+ this.append(`owner-authorization-revocation:${authorizationId}`, {
114
+ type: "owner_authorization_revoked",
115
+ authorizationId,
116
+ reason,
117
+ });
118
+ }
119
+
120
+ registerGoal(goal: GoalContract): void {
121
+ const state = this.state();
122
+ const authorization = getOwn(state.authorizations, goal.authorityInvocationId);
123
+ if (!authorization) {
124
+ throw new AgentFabricError(
125
+ "AF_GRANT_REJECTED",
126
+ `Goal ${goal.goalId} has no registered owner authorization`,
127
+ );
128
+ }
129
+ assertAuthorizationCurrent(
130
+ authorization,
131
+ this.clock.now(),
132
+ getOwn(state.revokedAuthorizations, authorization.authorizationId),
133
+ );
134
+ if (!authorization.goalIds.includes(goal.goalId)) {
135
+ throw new AgentFabricError("AF_GRANT_REJECTED", "Owner authorization does not cover this goal");
136
+ }
137
+ this.append(`goal:${goal.goalId}`, { type: "goal_registered", goal });
138
+ }
139
+
140
+ /** Registers only a root grant. Derived grants must use deriveAndRegisterGrant(). */
141
+ registerGrant(grant: ExecutionGrant): void {
142
+ if (grant.parentGrantId !== null) {
143
+ throw new AgentFabricError(
144
+ "AF_GRANT_REJECTED",
145
+ "Derived grants must be atomically reserved and registered",
146
+ );
147
+ }
148
+ const state = this.state();
149
+ const authorization = getOwn(state.authorizations, grant.rootAuthorizationId);
150
+ if (!authorization) {
151
+ throw new AgentFabricError(
152
+ "AF_GRANT_REJECTED",
153
+ `Root grant ${grant.grantId} has no registered owner authorization`,
154
+ );
155
+ }
156
+ assertAuthorizationCurrent(
157
+ authorization,
158
+ this.clock.now(),
159
+ getOwn(state.revokedAuthorizations, authorization.authorizationId),
160
+ );
161
+ assertRootGrantAuthorized(authorization, grant);
162
+
163
+ const siblingRoots = Object.values(state.grants).filter(
164
+ (candidate) => candidate.parentGrantId === null &&
165
+ candidate.rootAuthorizationId === authorization.authorizationId,
166
+ );
167
+ const allocatedAttempts = siblingRoots.reduce(
168
+ (total, candidate) => total + candidate.maximumAttempts,
169
+ 0,
170
+ );
171
+ if (allocatedAttempts + grant.maximumAttempts > authorization.maximumAttempts) {
172
+ throw new AgentFabricError(
173
+ "AF_RESOURCE_EXHAUSTED",
174
+ "Root grants exceed the owner authorization attempt budget",
175
+ );
176
+ }
177
+ for (const [resource, amount] of Object.entries(grant.resourceCeilings)) {
178
+ const allocated = siblingRoots.reduce(
179
+ (total, candidate) => total + (getOwn(candidate.resourceCeilings, resource) ?? 0),
180
+ 0,
181
+ );
182
+ if (allocated + amount > (getOwn(authorization.resourceCeilings, resource) ?? -1)) {
183
+ throw new AgentFabricError(
184
+ "AF_RESOURCE_EXHAUSTED",
185
+ `Root grants exceed owner authorization resource ${resource}`,
186
+ );
187
+ }
188
+ }
189
+
190
+ this.append(`grant:${grant.grantId}`, {
191
+ type: "grant_registered",
192
+ grant,
193
+ reservation: null,
194
+ });
195
+ }
196
+
197
+ deriveAndRegisterGrant(
198
+ parentGrantId: string,
199
+ request: DerivedGrantRequest,
200
+ ledger: ResourceLedger = this.requireResourceLedger(),
201
+ ): AuthorityResolution {
202
+ this.ensureResourceLedgerInitialized(ledger);
203
+ const state = this.state();
204
+ const parent = getOwn(state.grants, parentGrantId);
205
+ if (!parent) {
206
+ throw new AgentFabricError("AF_NOT_FOUND", `Unknown parent grant: ${parentGrantId}`);
207
+ }
208
+ const now = this.clock.now();
209
+ assertGrantLineageCurrent(state, parent.grantId, now);
210
+
211
+ const parentIssuedAttempts = Object.values(state.permits).filter(
212
+ (permit) => permit.grantId === parent.grantId,
213
+ ).length;
214
+ const delegatedAttemptBudget = Object.values(state.grants)
215
+ .filter((candidate) => candidate.parentGrantId === parent.grantId)
216
+ .reduce((total, candidate) => total + candidate.maximumAttempts, 0);
217
+ if (
218
+ parentIssuedAttempts + delegatedAttemptBudget + request.maximumAttempts >
219
+ parent.maximumAttempts
220
+ ) {
221
+ return {
222
+ outcome: "rejected",
223
+ reasonCodes: ["attempt_budget_exhausted"],
224
+ limitations: [],
225
+ };
226
+ }
227
+
228
+ return ledger.transaction(() => {
229
+ const resolution = deriveExecutionGrant(parent, request, ledger, now);
230
+ if (resolution.outcome !== "allowed" || !resolution.grant) return resolution;
231
+ const reservation = getOwn(ledger.snapshot().reservations, request.reservationId);
232
+ if (!reservation) {
233
+ throw new AgentFabricError(
234
+ "AF_RESOURCE_EXHAUSTED",
235
+ `Reservation ${request.reservationId} was not materialized`,
236
+ );
237
+ }
238
+ this.append(`grant:${resolution.grant.grantId}`, {
239
+ type: "grant_registered",
240
+ grant: resolution.grant,
241
+ reservation,
242
+ });
243
+ return resolution;
244
+ });
245
+ }
246
+
247
+ consumeResourceReservation(
248
+ reservationId: string,
249
+ ledger: ResourceLedger = this.requireResourceLedger(),
250
+ ): ResourceReservation {
251
+ this.ensureResourceLedgerInitialized(ledger);
252
+ return ledger.transaction(() => {
253
+ const reservation = ledger.consume(reservationId);
254
+ this.append(`resource-reservation-consume:${reservationId}`, {
255
+ type: "resource_reservation_consumed",
256
+ reservationId,
257
+ });
258
+ return reservation;
259
+ });
260
+ }
261
+
262
+ releaseResourceReservation(
263
+ reservationId: string,
264
+ ledger: ResourceLedger = this.requireResourceLedger(),
265
+ ): ResourceReservation {
266
+ this.ensureResourceLedgerInitialized(ledger);
267
+ return ledger.transaction(() => {
268
+ const reservation = ledger.release(reservationId);
269
+ this.append(`resource-reservation-release:${reservationId}`, {
270
+ type: "resource_reservation_released",
271
+ reservationId,
272
+ });
273
+ return reservation;
274
+ });
275
+ }
276
+
277
+ revokeGrant(grantId: string, reason: string): void {
278
+ const state = this.state();
279
+ if (!getOwn(state.grants, grantId)) {
280
+ throw new AgentFabricError("AF_NOT_FOUND", `Unknown grant: ${grantId}`);
281
+ }
282
+ this.append(`grant-revocation:${grantId}`, { type: "grant_revoked", grantId, reason });
283
+ }
284
+
285
+ registerPlanDelta(delta: PlanDelta): void {
286
+ const state = this.state();
287
+ if (delta.rootExecutionId !== this.rootExecutionId) {
288
+ throw new AgentFabricError("AF_INVALID_PLAN", "PlanDelta belongs to another execution");
289
+ }
290
+ if (getOwn(state.activePlanRevisionByExecution, this.rootExecutionId) !== delta.baseRevisionId) {
291
+ throw new AgentFabricError("AF_INVALID_PLAN", "PlanDelta is stale");
292
+ }
293
+ this.append(`plan-delta:${delta.deltaId}`, { type: "plan_delta_registered", delta });
294
+ }
295
+
296
+ activatePlan(revision: RunPlanRevision, expectedCurrentRevisionId: string | null): void {
297
+ const state = this.state();
298
+ const current = getOwn(state.activePlanRevisionByExecution, this.rootExecutionId) ?? null;
299
+ if (current !== expectedCurrentRevisionId) {
300
+ throw new AgentFabricError(
301
+ "AF_CONFLICT",
302
+ "Plan activation compare-and-swap failed",
303
+ { actualRevisionId: current, expectedCurrentRevisionId },
304
+ );
305
+ }
306
+ if (revision.rootExecutionId !== this.rootExecutionId) {
307
+ throw new AgentFabricError("AF_INVALID_PLAN", "Plan revision belongs to another execution");
308
+ }
309
+ validateWorkflowNodes(revision.nodes);
310
+ const expectedDigest = computeRunPlanContentDigest(
311
+ revision.programVersionId,
312
+ revision.nodes,
313
+ sha256Digest,
314
+ );
315
+ if (revision.contentDigest !== expectedDigest) {
316
+ throw new AgentFabricError("AF_INVALID_PLAN", "Plan revision content digest does not match");
317
+ }
318
+
319
+ if (expectedCurrentRevisionId === null) {
320
+ if (
321
+ revision.parentRevisionId !== null ||
322
+ revision.sourcePlanDeltaId !== null ||
323
+ revision.revisionNumber !== 1
324
+ ) {
325
+ throw new AgentFabricError("AF_INVALID_PLAN", "Initial plan revision has invalid lineage");
326
+ }
327
+ } else {
328
+ const parent = getOwn(state.planRevisions, expectedCurrentRevisionId);
329
+ if (!parent || revision.parentRevisionId !== parent.revisionId) {
330
+ throw new AgentFabricError(
331
+ "AF_INVALID_PLAN",
332
+ "Plan revision parent does not match the active revision",
333
+ );
334
+ }
335
+ if (revision.goalId !== parent.goalId || revision.programVersionId !== parent.programVersionId) {
336
+ throw new AgentFabricError(
337
+ "AF_INVALID_PLAN",
338
+ "Plan revision cannot silently change goal or workflow program",
339
+ );
340
+ }
341
+ if (revision.revisionNumber !== parent.revisionNumber + 1 || !revision.sourcePlanDeltaId) {
342
+ throw new AgentFabricError("AF_INVALID_PLAN", "Plan revision has invalid revision lineage");
343
+ }
344
+ const delta = getOwn(state.planDeltas, revision.sourcePlanDeltaId);
345
+ if (
346
+ !delta ||
347
+ delta.baseRevisionId !== parent.revisionId ||
348
+ delta.nextRevisionId !== revision.revisionId ||
349
+ delta.rootExecutionId !== this.rootExecutionId
350
+ ) {
351
+ throw new AgentFabricError(
352
+ "AF_INVALID_PLAN",
353
+ "Plan revision is not backed by its registered PlanDelta",
354
+ );
355
+ }
356
+ const derived = applyPlanDelta(parent, delta, sha256Digest);
357
+ if (stableStringify(derived) !== stableStringify(revision)) {
358
+ throw new AgentFabricError(
359
+ "AF_INVALID_PLAN",
360
+ "Plan revision content does not match its registered PlanDelta",
361
+ );
362
+ }
363
+ }
364
+
365
+ this.append(`plan-activation:${revision.revisionId}`, {
366
+ type: "plan_revision_activated",
367
+ revision,
368
+ });
369
+ }
370
+
371
+ commitDispatchIntent(intent: DispatchIntent): void {
372
+ const state = this.state();
373
+ const activeRevision = getOwn(state.activePlanRevisionByExecution, this.rootExecutionId);
374
+ if (intent.rootExecutionId !== this.rootExecutionId || intent.planRevisionId !== activeRevision) {
375
+ throw new AgentFabricError(
376
+ "AF_INVALID_STATE",
377
+ "Dispatch intent is not bound to the active plan revision",
378
+ );
379
+ }
380
+ if (intent.createdAt > this.clock.now()) {
381
+ throw new AgentFabricError("AF_INVALID_STATE", "Dispatch intent cannot be created in the future");
382
+ }
383
+ const revision = getOwn(state.planRevisions, intent.planRevisionId);
384
+ const goal = revision ? getOwn(state.goals, revision.goalId) : undefined;
385
+ if (!revision || !goal) {
386
+ throw new AgentFabricError("AF_INVALID_PLAN", "Dispatch intent has no active GoalContract");
387
+ }
388
+ if (
389
+ !goal.allowedEffectClasses.includes(intent.effectClass) ||
390
+ goal.prohibitedEffectClasses.includes(intent.effectClass)
391
+ ) {
392
+ throw new AgentFabricError("AF_GRANT_REJECTED", "GoalContract prohibits the intent effect class");
393
+ }
394
+ if (
395
+ !goal.sourceBoundary.allowExpansion &&
396
+ !intent.sourceIds.every((sourceId) => goal.sourceBoundary.sourceIds.includes(sourceId))
397
+ ) {
398
+ throw new AgentFabricError("AF_GRANT_REJECTED", "Intent expands the GoalContract source boundary");
399
+ }
400
+ if (!revision.nodes.some((node) => node.nodeId === intent.taskNodeId)) {
401
+ throw new AgentFabricError(
402
+ "AF_INVALID_PLAN",
403
+ `Dispatch intent references missing plan node ${intent.taskNodeId}`,
404
+ );
405
+ }
406
+ this.append(`dispatch-intent:${intent.intentId}`, {
407
+ type: "dispatch_intent_committed",
408
+ intent,
409
+ });
410
+ }
411
+
412
+ createDispatchOffer(
413
+ intentId: string,
414
+ audiencePool: string,
415
+ offerId: string,
416
+ expiresAt: number,
417
+ ): DispatchOffer {
418
+ if (!getOwn(this.state().dispatchIntents, intentId)) {
419
+ throw new AgentFabricError("AF_NOT_FOUND", `Unknown dispatch intent: ${intentId}`);
420
+ }
421
+ if (expiresAt <= this.clock.now()) {
422
+ throw new AgentFabricError("AF_INVALID_STATE", "Dispatch offer must expire in the future");
423
+ }
424
+ return { offerId, intentId, audiencePool, expiresAt, nonAuthoritative: true };
425
+ }
426
+
427
+ claimDispatch(input: ClaimDispatchInput): SchedulingClaim {
428
+ if (input.leaseDurationMs <= 0) {
429
+ throw new AgentFabricError("AF_INVALID_STATE", "Lease duration must be positive");
430
+ }
431
+ const state = this.state();
432
+ const intent = getOwn(state.dispatchIntents, input.intentId);
433
+ if (!intent) {
434
+ throw new AgentFabricError("AF_NOT_FOUND", `Unknown dispatch intent: ${input.intentId}`);
435
+ }
436
+ if (getOwn(state.claimByAttemptId, input.attemptId)) {
437
+ throw new AgentFabricError("AF_DUPLICATE_ID", `Attempt ID already claimed: ${input.attemptId}`);
438
+ }
439
+ if (getOwn(state.activePlanRevisionByExecution, this.rootExecutionId) !== intent.planRevisionId) {
440
+ throw new AgentFabricError("AF_STALE_ATTEMPT", "Cannot claim an intent from a superseded plan");
441
+ }
442
+ const hasOutcome = Object.values(state.outcomes).some(
443
+ (outcome) => outcome.intentId === input.intentId,
444
+ );
445
+ if (hasOutcome) {
446
+ throw new AgentFabricError(
447
+ "AF_CONFLICT",
448
+ `Dispatch intent ${input.intentId} already has an outcome`,
449
+ );
450
+ }
451
+ const currentClaimId = getOwn(state.activeClaimByIntent, input.intentId);
452
+ const currentClaim = currentClaimId ? getOwn(state.claims, currentClaimId) : undefined;
453
+ const now = this.clock.now();
454
+ if (currentClaim && currentClaim.leaseExpiresAt > now) {
455
+ throw new AgentFabricError(
456
+ "AF_CONFLICT",
457
+ `Dispatch intent ${input.intentId} already has a current claim`,
458
+ );
459
+ }
460
+ const claim: SchedulingClaim = {
461
+ claimId: input.claimId,
462
+ intentId: input.intentId,
463
+ workerId: input.workerId,
464
+ attemptId: input.attemptId,
465
+ leaseExpiresAt: now + input.leaseDurationMs,
466
+ fencingToken: (currentClaim?.fencingToken ?? 0) + 1,
467
+ committedAt: now,
468
+ };
469
+ this.append(`claim:${input.intentId}:${claim.fencingToken}`, {
470
+ type: "scheduling_claim_committed",
471
+ claim,
472
+ });
473
+ return claim;
474
+ }
475
+
476
+ issuePermit(input: IssuePermitInput): AttemptExecutionPermit {
477
+ if (input.maximumValidityMs <= 0) {
478
+ throw new AgentFabricError("AF_PERMIT_REJECTED", "Permit validity must be positive");
479
+ }
480
+ const state = this.state();
481
+ const claim = getOwn(state.claims, input.claimId);
482
+ const grant = getOwn(state.grants, input.grantId);
483
+ if (!claim) throw new AgentFabricError("AF_NOT_FOUND", `Unknown claim: ${input.claimId}`);
484
+ if (!grant) throw new AgentFabricError("AF_NOT_FOUND", `Unknown grant: ${input.grantId}`);
485
+ const intent = getOwn(state.dispatchIntents, claim.intentId);
486
+ if (!intent) throw new AgentFabricError("AF_NOT_FOUND", `Unknown intent: ${claim.intentId}`);
487
+ if (Object.values(state.permits).some((permit) => permit.attemptId === claim.attemptId)) {
488
+ throw new AgentFabricError("AF_CONFLICT", `Attempt ${claim.attemptId} already has a permit`);
489
+ }
490
+ const activeClaimId = getOwn(state.activeClaimByIntent, claim.intentId);
491
+ const now = this.clock.now();
492
+ if (getOwn(state.activePlanRevisionByExecution, this.rootExecutionId) !== intent.planRevisionId) {
493
+ throw new AgentFabricError("AF_STALE_ATTEMPT", "Intent references a superseded plan revision");
494
+ }
495
+ if (activeClaimId !== claim.claimId || claim.leaseExpiresAt <= now) {
496
+ throw new AgentFabricError("AF_STALE_ATTEMPT", "Claim is no longer current");
497
+ }
498
+ assertGrantLineageCurrent(state, grant.grantId, now);
499
+ if (!grant.capabilities.includes(intent.requiredCapability)) {
500
+ throw new AgentFabricError("AF_GRANT_REJECTED", "Grant lacks the required capability");
501
+ }
502
+ if (!intent.sourceIds.every((sourceId) => grant.sourceIds.includes(sourceId))) {
503
+ throw new AgentFabricError("AF_GRANT_REJECTED", "Grant does not cover the intent source scope");
504
+ }
505
+ if (!grant.targetIds.includes(intent.targetId)) {
506
+ throw new AgentFabricError("AF_GRANT_REJECTED", "Grant does not cover the intent target");
507
+ }
508
+ if (!grant.effectClasses.includes(intent.effectClass)) {
509
+ throw new AgentFabricError("AF_GRANT_REJECTED", "Grant lacks the required effect class");
510
+ }
511
+ if (grant.subjectId !== claim.workerId) {
512
+ throw new AgentFabricError("AF_GRANT_REJECTED", "Grant is bound to another worker");
513
+ }
514
+ const issuedAttempts = Object.values(state.permits).filter(
515
+ (permit) => permit.grantId === grant.grantId,
516
+ ).length;
517
+ const delegatedAttemptBudget = Object.values(state.grants)
518
+ .filter((candidate) => candidate.parentGrantId === grant.grantId)
519
+ .reduce((total, candidate) => total + candidate.maximumAttempts, 0);
520
+ if (issuedAttempts + delegatedAttemptBudget >= grant.maximumAttempts) {
521
+ throw new AgentFabricError("AF_GRANT_REJECTED", "Grant attempt budget is exhausted");
522
+ }
523
+ const expiresAt = Math.min(claim.leaseExpiresAt, grant.expiresAt, now + input.maximumValidityMs);
524
+ const permit: AttemptExecutionPermit = {
525
+ permitId: input.permitId,
526
+ intentId: intent.intentId,
527
+ claimId: claim.claimId,
528
+ attemptId: claim.attemptId,
529
+ workerId: claim.workerId,
530
+ planRevisionId: intent.planRevisionId,
531
+ effectiveRunSpecDigest: intent.effectiveRunSpecDigest,
532
+ grantId: grant.grantId,
533
+ fencingToken: claim.fencingToken,
534
+ notBefore: now,
535
+ expiresAt,
536
+ };
537
+ this.append(`permit:${permit.permitId}`, {
538
+ type: "attempt_execution_permit_issued",
539
+ permit,
540
+ });
541
+ return permit;
542
+ }
543
+
544
+ authorizeAttemptDispatch(permit: AttemptExecutionPermit): void {
545
+ this.assertPermitCurrent(permit);
546
+ }
547
+
548
+ acceptStartupReport(permit: AttemptExecutionPermit, report: ExecutorStartupReport): void {
549
+ this.assertPermitCurrent(permit);
550
+ const now = this.clock.now();
551
+ if (report.attemptId !== permit.attemptId) {
552
+ throw new AgentFabricError("AF_PERMIT_REJECTED", "Startup report is bound to another attempt");
553
+ }
554
+ if (report.observedSpecDigest !== permit.effectiveRunSpecDigest) {
555
+ throw new AgentFabricError("AF_PERMIT_REJECTED", "Adapter started a different EffectiveRunSpec");
556
+ }
557
+ if (
558
+ report.startedAt < permit.notBefore ||
559
+ report.startedAt >= permit.expiresAt ||
560
+ report.startedAt > now
561
+ ) {
562
+ throw new AgentFabricError("AF_PERMIT_REJECTED", "Startup report has an invalid timestamp");
563
+ }
564
+ this.append(`attempt-start:${permit.attemptId}`, {
565
+ type: "attempt_started",
566
+ attemptId: permit.attemptId,
567
+ permitId: permit.permitId,
568
+ startupReportId: report.startupReportId,
569
+ startedAt: report.startedAt,
570
+ });
571
+ }
572
+
573
+ recordAttemptUncertainty(
574
+ permit: AttemptExecutionPermit,
575
+ phase: "startup" | "outcome",
576
+ reason: string,
577
+ ): AttemptUncertaintyObservation {
578
+ const state = this.state();
579
+ const recorded = getOwn(state.permits, permit.permitId);
580
+ if (!recorded || stableStringify(recorded) !== stableStringify(permit)) {
581
+ throw new AgentFabricError("AF_PERMIT_REJECTED", "Permit is not the recorded permit");
582
+ }
583
+ const observedAt = this.clock.now();
584
+ const observation: AttemptUncertaintyObservation = {
585
+ observationId: `uncertainty:${permit.attemptId}:${phase}:${observedAt}:${sha256Digest(reason).slice(7, 19)}`,
586
+ attemptId: permit.attemptId,
587
+ permitId: permit.permitId,
588
+ phase,
589
+ reason,
590
+ observedAt,
591
+ };
592
+ this.append(`attempt-uncertainty:${observation.observationId}`, {
593
+ type: "attempt_uncertainty_observed",
594
+ observation,
595
+ });
596
+ return observation;
597
+ }
598
+
599
+ commitOutcome(report: WorkerResultReport): AuthoritativeOutcomeCommit {
600
+ validateWorkerResultReport(report);
601
+ const state = this.state();
602
+ const attempt = getOwn(state.attempts, report.attemptId);
603
+ if (!attempt) {
604
+ throw new AgentFabricError("AF_NOT_FOUND", `Unknown started attempt: ${report.attemptId}`);
605
+ }
606
+ const permit = getOwn(state.permits, attempt.permitId);
607
+ if (!permit) {
608
+ throw new AgentFabricError("AF_NOT_FOUND", `Unknown permit: ${attempt.permitId}`);
609
+ }
610
+ const claim = getOwn(state.claims, permit.claimId);
611
+ const intent = getOwn(state.dispatchIntents, permit.intentId);
612
+ if (!claim || !intent) {
613
+ throw new AgentFabricError("AF_INVALID_STATE", "Outcome has incomplete execution lineage");
614
+ }
615
+ if (
616
+ report.permitId !== permit.permitId ||
617
+ report.intentId !== permit.intentId ||
618
+ report.planRevisionId !== permit.planRevisionId ||
619
+ report.effectiveRunSpecDigest !== permit.effectiveRunSpecDigest ||
620
+ report.fencingToken !== permit.fencingToken ||
621
+ getOwn(state.claimByAttemptId, report.attemptId) !== claim.claimId
622
+ ) {
623
+ throw new AgentFabricError(
624
+ "AF_CONFLICT",
625
+ "Worker result report does not match its recorded attempt/permit lineage",
626
+ );
627
+ }
628
+
629
+ const reportDigest = digestCanonical(report, sha256Digest);
630
+ const existing = getOwn(state.outcomes, report.attemptId);
631
+ if (existing) {
632
+ if (existing.status === report.status && existing.reportDigest === reportDigest) return existing;
633
+ throw new AgentFabricError(
634
+ "AF_CONFLICT",
635
+ `Attempt ${report.attemptId} already has a different authoritative outcome`,
636
+ );
637
+ }
638
+
639
+ const now = this.clock.now();
640
+ if (report.reportedAt < attempt.startedAt || report.reportedAt > now) {
641
+ throw new AgentFabricError("AF_INVALID_STATE", "Worker result report has an invalid timestamp");
642
+ }
643
+ if (now < permit.notBefore || now >= permit.expiresAt) {
644
+ throw new AgentFabricError("AF_STALE_ATTEMPT", "Expired permit cannot commit an outcome");
645
+ }
646
+ if (
647
+ getOwn(state.activeClaimByIntent, permit.intentId) !== claim.claimId ||
648
+ claim.fencingToken !== permit.fencingToken ||
649
+ claim.leaseExpiresAt <= now
650
+ ) {
651
+ throw new AgentFabricError("AF_STALE_ATTEMPT", "Stale attempt cannot commit an outcome");
652
+ }
653
+ if (getOwn(state.activePlanRevisionByExecution, this.rootExecutionId) !== permit.planRevisionId) {
654
+ throw new AgentFabricError("AF_STALE_ATTEMPT", "Superseded plan attempt cannot commit an outcome");
655
+ }
656
+ assertGrantLineageCurrent(state, permit.grantId, now);
657
+
658
+ const outcome: AuthoritativeOutcomeCommit = {
659
+ outcomeId: `outcome:${report.attemptId}`,
660
+ attemptId: report.attemptId,
661
+ permitId: permit.permitId,
662
+ intentId: permit.intentId,
663
+ planRevisionId: permit.planRevisionId,
664
+ effectiveRunSpecDigest: permit.effectiveRunSpecDigest,
665
+ fencingToken: permit.fencingToken,
666
+ status: report.status,
667
+ resultDigest: report.resultDigest,
668
+ reportId: report.reportId,
669
+ reportDigest,
670
+ evidenceDigests: [...report.evidenceDigests],
671
+ reportedAt: report.reportedAt,
672
+ committedAt: now,
673
+ };
674
+ this.append(`attempt-outcome:${report.attemptId}`, {
675
+ type: "attempt_outcome_committed",
676
+ outcome,
677
+ });
678
+ return outcome;
679
+ }
680
+
681
+ private assertPermitCurrent(permit: AttemptExecutionPermit): void {
682
+ const state = this.state();
683
+ const recorded = getOwn(state.permits, permit.permitId);
684
+ if (!recorded || stableStringify(recorded) !== stableStringify(permit)) {
685
+ throw new AgentFabricError("AF_PERMIT_REJECTED", "Permit is not the recorded permit");
686
+ }
687
+ const now = this.clock.now();
688
+ if (now < permit.notBefore || now >= permit.expiresAt) {
689
+ throw new AgentFabricError("AF_PERMIT_REJECTED", "Permit is outside its validity window");
690
+ }
691
+ const claim = getOwn(state.claims, permit.claimId);
692
+ const activeClaimId = getOwn(state.activeClaimByIntent, permit.intentId);
693
+ if (
694
+ !claim ||
695
+ getOwn(state.claimByAttemptId, permit.attemptId) !== claim.claimId ||
696
+ activeClaimId !== claim.claimId ||
697
+ claim.fencingToken !== permit.fencingToken ||
698
+ claim.workerId !== permit.workerId ||
699
+ claim.leaseExpiresAt <= now
700
+ ) {
701
+ throw new AgentFabricError("AF_STALE_ATTEMPT", "Permit is fenced by a newer claim");
702
+ }
703
+ assertGrantLineageCurrent(state, permit.grantId, now);
704
+ if (getOwn(state.activePlanRevisionByExecution, this.rootExecutionId) !== permit.planRevisionId) {
705
+ throw new AgentFabricError("AF_STALE_ATTEMPT", "Permit references a superseded plan revision");
706
+ }
707
+ }
708
+
709
+ private ensureResourceLedgerInitialized(ledger: ResourceLedger): void {
710
+ const trustedDefinitions = sortedDefinitions(ledger.snapshot().definitions);
711
+ const state = this.state();
712
+ const recordedDefinitions = sortedDefinitions(state.resourceDefinitions);
713
+ if (recordedDefinitions.length === 0) {
714
+ this.append("resource-ledger:initialize", {
715
+ type: "resource_ledger_initialized",
716
+ definitions: trustedDefinitions,
717
+ });
718
+ return;
719
+ }
720
+ if (stableStringify(recordedDefinitions) !== stableStringify(trustedDefinitions)) {
721
+ throw new AgentFabricError(
722
+ "AF_INVALID_STATE",
723
+ "ResourceLedger definitions differ from the journaled configuration",
724
+ );
725
+ }
726
+ }
727
+
728
+ private requireResourceLedger(): ResourceLedger {
729
+ if (!this.resourceLedger) {
730
+ throw new AgentFabricError(
731
+ "AF_INVALID_STATE",
732
+ "Derived grant registration requires a ResourceLedger",
733
+ );
734
+ }
735
+ return this.resourceLedger;
736
+ }
737
+
738
+ private append(
739
+ idempotencyKey: string,
740
+ payload: Parameters<ControlJournal["append"]>[0]["event"]["payload"],
741
+ ): void {
742
+ const state = this.state();
743
+ this.journal.append({
744
+ expectedSequence: state.lastSequence,
745
+ event: {
746
+ eventId: `event:${idempotencyKey}`,
747
+ rootExecutionId: this.rootExecutionId,
748
+ occurredAt: this.clock.now(),
749
+ idempotencyKey,
750
+ payload,
751
+ },
752
+ });
753
+ }
754
+ }