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.
- package/AGENTS.md +1 -1
- package/CHANGELOG.md +19 -0
- package/docs/agent-fabric.md +252 -0
- package/package.json +7 -4
- package/schemas/agent-fabric/v0.1/control-event.schema.json +281 -0
- package/scripts/field-test-forgeos.mjs +11 -2
- package/src/forge/_generated/releaseManifest.json +1 -1
- package/src/forge/_generated/releaseManifest.ts +3 -3
- package/src/forge/agent-adapters/index.ts +18 -14
- package/src/forge/agent-adapters/types.ts +1 -1
- package/src/forge/agent-fabric/adapter.ts +131 -0
- package/src/forge/agent-fabric/authority.ts +298 -0
- package/src/forge/agent-fabric/canonical.ts +135 -0
- package/src/forge/agent-fabric/conductor.ts +754 -0
- package/src/forge/agent-fabric/dictionary.ts +12 -0
- package/src/forge/agent-fabric/errors.ts +32 -0
- package/src/forge/agent-fabric/hardened-conductor.ts +485 -0
- package/src/forge/agent-fabric/hardened-reducer.ts +211 -0
- package/src/forge/agent-fabric/index.ts +18 -0
- package/src/forge/agent-fabric/journal.ts +116 -0
- package/src/forge/agent-fabric/local-approval-window.ts +169 -0
- package/src/forge/agent-fabric/local-coding-worker.ts +190 -0
- package/src/forge/agent-fabric/local-control-store.ts +239 -0
- package/src/forge/agent-fabric/local-paths.ts +18 -0
- package/src/forge/agent-fabric/local-task-contract.ts +142 -0
- package/src/forge/agent-fabric/local-task-inbox.ts +199 -0
- package/src/forge/agent-fabric/local-task-server.ts +207 -0
- package/src/forge/agent-fabric/local-task-service.ts +494 -0
- package/src/forge/agent-fabric/p0a.ts +104 -0
- package/src/forge/agent-fabric/p0b-model-adapter.ts +404 -0
- package/src/forge/agent-fabric/planning.ts +155 -0
- package/src/forge/agent-fabric/reducer.ts +863 -0
- package/src/forge/agent-fabric/resource-ledger.ts +266 -0
- package/src/forge/agent-fabric/serialized-local-adapter.ts +60 -0
- package/src/forge/agent-fabric/types.ts +424 -0
- package/src/forge/agent-fabric/validation.ts +500 -0
- package/src/forge/agent-memory/bridge.ts +82 -52
- package/src/forge/agent-memory/mcp.ts +59 -9
- package/src/forge/agent-memory/sources/codex-hook-runner.mjs +12 -113
- package/src/forge/cli/commands.ts +4 -0
- package/src/forge/cli/fabric.ts +83 -0
- package/src/forge/cli/main.ts +7 -0
- package/src/forge/cli/new.ts +16 -4
- package/src/forge/cli/parse.ts +21 -0
- package/src/forge/cli/verify.ts +4 -1
- package/src/forge/compiler/app-graph/tsconfig-hash.ts +41 -4
- package/src/forge/compiler/package-graph/dts-extractor.ts +14 -1
- package/src/forge/impact/index.ts +18 -21
- package/src/forge/runtime/ai/providers.ts +19 -4
- package/src/forge/version.ts +1 -1
- package/templates/agent-workroom/package.json +1 -0
- package/templates/agent-workroom/pnpm-workspace.yaml +2 -0
- package/templates/b2b-support-web/package.json +1 -0
- package/templates/b2b-support-web/pnpm-workspace.yaml +2 -0
- package/templates/b2b-support-web/web/package.json +1 -1
- package/templates/minimal-web/package.json +1 -0
- package/templates/minimal-web/pnpm-workspace.yaml +2 -0
- package/templates/nuxt-web/.yarnrc.yml +1 -0
- package/templates/nuxt-web/package.json +1 -0
- package/templates/nuxt-web/pnpm-workspace.yaml +2 -0
- package/templates/nuxt-web/web/package.json +1 -0
- package/templates/vendor-access/package.json +1 -0
- package/templates/vendor-access/pnpm-workspace.yaml +2 -0
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
// Identifiers are arbitrary non-empty strings, including Object.prototype names.
|
|
2
|
+
// Keep own-data semantics even for ordinary objects produced by clone or spread.
|
|
3
|
+
export function getOwn<T>(record: Readonly<Record<string, T>>, key: string): T | undefined {
|
|
4
|
+
return Object.hasOwn(record, key) ? record[key] : undefined;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function setOwn<T>(record: Record<string, T>, key: string, value: T): T {
|
|
8
|
+
Object.defineProperty(record, key, {
|
|
9
|
+
value, enumerable: true, configurable: true, writable: true,
|
|
10
|
+
});
|
|
11
|
+
return value;
|
|
12
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export type AgentFabricErrorCode =
|
|
2
|
+
| "AF_CANONICALIZATION_FAILED"
|
|
3
|
+
| "AF_CONFLICT"
|
|
4
|
+
| "AF_DUPLICATE_ID"
|
|
5
|
+
| "AF_GRANT_REJECTED"
|
|
6
|
+
| "AF_INVALID_EVENT"
|
|
7
|
+
| "AF_INVALID_PLAN"
|
|
8
|
+
| "AF_INVALID_STATE"
|
|
9
|
+
| "AF_NOT_FOUND"
|
|
10
|
+
| "AF_PERMIT_REJECTED"
|
|
11
|
+
| "AF_RESOURCE_EXHAUSTED"
|
|
12
|
+
| "AF_STALE_ATTEMPT";
|
|
13
|
+
|
|
14
|
+
export class AgentFabricError extends Error {
|
|
15
|
+
readonly code: AgentFabricErrorCode;
|
|
16
|
+
readonly details: Readonly<Record<string, unknown>>;
|
|
17
|
+
|
|
18
|
+
constructor(
|
|
19
|
+
code: AgentFabricErrorCode,
|
|
20
|
+
message: string,
|
|
21
|
+
details: Readonly<Record<string, unknown>> = {},
|
|
22
|
+
) {
|
|
23
|
+
super(message);
|
|
24
|
+
this.name = "AgentFabricError";
|
|
25
|
+
this.code = code;
|
|
26
|
+
this.details = details;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function isAgentFabricError(error: unknown): error is AgentFabricError {
|
|
31
|
+
return error instanceof AgentFabricError;
|
|
32
|
+
}
|
|
@@ -0,0 +1,485 @@
|
|
|
1
|
+
import { getOwn } from "./dictionary.ts";
|
|
2
|
+
import { ForgeAgentConductor as LegacyForgeAgentConductor } from "./conductor.ts";
|
|
3
|
+
import { stableStringify } from "./canonical.ts";
|
|
4
|
+
import { AgentFabricError } from "./errors.ts";
|
|
5
|
+
import {
|
|
6
|
+
computeControlEventDigest,
|
|
7
|
+
type AppendControlEventInput,
|
|
8
|
+
type ControlJournal,
|
|
9
|
+
} from "./journal.ts";
|
|
10
|
+
import { replayControlState } from "./hardened-reducer.ts";
|
|
11
|
+
import { ResourceLedger } from "./resource-ledger.ts";
|
|
12
|
+
import type {
|
|
13
|
+
AttemptExecutionPermit,
|
|
14
|
+
AttemptUncertaintyObservation,
|
|
15
|
+
AuthoritativeOutcomeCommit,
|
|
16
|
+
AuthorityResolution,
|
|
17
|
+
Clock,
|
|
18
|
+
ControlEventEnvelope,
|
|
19
|
+
ControlState,
|
|
20
|
+
DerivedGrantRequest,
|
|
21
|
+
DigestFunction,
|
|
22
|
+
DispatchIntent,
|
|
23
|
+
DispatchOffer,
|
|
24
|
+
ExecutionGrant,
|
|
25
|
+
ExecutorStartupReport,
|
|
26
|
+
GoalContract,
|
|
27
|
+
OwnerAuthorization,
|
|
28
|
+
OwnerAuthorizationVerifier,
|
|
29
|
+
PlanDelta,
|
|
30
|
+
ReplayTrustContext,
|
|
31
|
+
ResourceDefinition,
|
|
32
|
+
ResourceLedgerSnapshot,
|
|
33
|
+
ResourceReservation,
|
|
34
|
+
RunPlanRevision,
|
|
35
|
+
SchedulingClaim,
|
|
36
|
+
WorkerResultReport,
|
|
37
|
+
} from "./types.ts";
|
|
38
|
+
|
|
39
|
+
export interface ClaimDispatchInput {
|
|
40
|
+
claimId: string;
|
|
41
|
+
intentId: string;
|
|
42
|
+
workerId: string;
|
|
43
|
+
attemptId: string;
|
|
44
|
+
leaseDurationMs: number;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface IssuePermitInput {
|
|
48
|
+
permitId: string;
|
|
49
|
+
claimId: string;
|
|
50
|
+
grantId: string;
|
|
51
|
+
maximumValidityMs: number;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function sortedDefinitions(
|
|
55
|
+
definitions: Readonly<Record<string, ResourceDefinition>>,
|
|
56
|
+
): ResourceDefinition[] {
|
|
57
|
+
return Object.values(definitions)
|
|
58
|
+
.map((definition) => ({ ...definition }))
|
|
59
|
+
.sort((left, right) => left.resource.localeCompare(right.resource));
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function ledgerProjection(snapshot: ResourceLedgerSnapshot) {
|
|
63
|
+
return {
|
|
64
|
+
definitions: snapshot.definitions,
|
|
65
|
+
reserved: snapshot.reserved,
|
|
66
|
+
consumed: snapshot.consumed,
|
|
67
|
+
ownerReserved: snapshot.ownerReserved,
|
|
68
|
+
ownerConsumed: snapshot.ownerConsumed,
|
|
69
|
+
reservations: snapshot.reservations,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function stateLedgerProjection(state: ControlState) {
|
|
74
|
+
return {
|
|
75
|
+
definitions: state.resourceDefinitions,
|
|
76
|
+
reserved: state.resourceReserved,
|
|
77
|
+
consumed: state.resourceConsumed,
|
|
78
|
+
ownerReserved: state.resourceOwnerReserved,
|
|
79
|
+
ownerConsumed: state.resourceOwnerConsumed,
|
|
80
|
+
reservations: state.resourceReservations,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function snapshotFromState(state: ControlState): ResourceLedgerSnapshot {
|
|
85
|
+
return {
|
|
86
|
+
definitions: structuredClone(state.resourceDefinitions),
|
|
87
|
+
reserved: structuredClone(state.resourceReserved),
|
|
88
|
+
consumed: structuredClone(state.resourceConsumed),
|
|
89
|
+
ownerReserved: structuredClone(state.resourceOwnerReserved),
|
|
90
|
+
ownerConsumed: structuredClone(state.resourceOwnerConsumed),
|
|
91
|
+
reservations: structuredClone(state.resourceReservations),
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function snapshotHasUsage(snapshot: ResourceLedgerSnapshot): boolean {
|
|
96
|
+
return Object.values(snapshot.reserved).some((value) => value !== 0) ||
|
|
97
|
+
Object.values(snapshot.consumed).some((value) => value !== 0) ||
|
|
98
|
+
Object.keys(snapshot.ownerReserved).length > 0 ||
|
|
99
|
+
Object.keys(snapshot.ownerConsumed).length > 0 ||
|
|
100
|
+
Object.keys(snapshot.reservations).length > 0;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
class PinnedClock implements Clock {
|
|
104
|
+
private pinned: number | null = null;
|
|
105
|
+
|
|
106
|
+
constructor(private readonly source: Clock) {}
|
|
107
|
+
|
|
108
|
+
now(): number {
|
|
109
|
+
return this.pinned ?? this.source.now();
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
run<T>(operation: () => T): T {
|
|
113
|
+
if (this.pinned !== null) return operation();
|
|
114
|
+
this.pinned = this.source.now();
|
|
115
|
+
try {
|
|
116
|
+
return operation();
|
|
117
|
+
} finally {
|
|
118
|
+
this.pinned = null;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
class ReplayValidatingJournal implements ControlJournal {
|
|
124
|
+
constructor(
|
|
125
|
+
private readonly inner: ControlJournal,
|
|
126
|
+
private readonly rootExecutionId: string,
|
|
127
|
+
private readonly trust: () => ReplayTrustContext,
|
|
128
|
+
) {}
|
|
129
|
+
|
|
130
|
+
append(input: AppendControlEventInput): ControlEventEnvelope {
|
|
131
|
+
const current = this.inner.readAll();
|
|
132
|
+
|
|
133
|
+
if (input.event.rootExecutionId !== this.rootExecutionId) {
|
|
134
|
+
throw new AgentFabricError(
|
|
135
|
+
"AF_INVALID_EVENT",
|
|
136
|
+
`Event ${input.event.eventId} belongs to another root execution`,
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const duplicate = current.find(
|
|
141
|
+
(event) =>
|
|
142
|
+
event.eventId === input.event.eventId ||
|
|
143
|
+
(input.event.idempotencyKey !== undefined &&
|
|
144
|
+
event.idempotencyKey === input.event.idempotencyKey),
|
|
145
|
+
);
|
|
146
|
+
if (duplicate || input.expectedSequence !== current.length) {
|
|
147
|
+
return this.inner.append(input);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const predecessor = current.at(-1);
|
|
151
|
+
const withoutDigest: Omit<ControlEventEnvelope, "eventDigest"> = {
|
|
152
|
+
...structuredClone(input.event),
|
|
153
|
+
sequence: current.length + 1,
|
|
154
|
+
predecessorEventId: predecessor?.eventId ?? null,
|
|
155
|
+
predecessorEventDigest: predecessor?.eventDigest ?? null,
|
|
156
|
+
};
|
|
157
|
+
const candidate: ControlEventEnvelope = {
|
|
158
|
+
...withoutDigest,
|
|
159
|
+
eventDigest: computeControlEventDigest(withoutDigest),
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
replayControlState([...current, candidate], this.trust());
|
|
163
|
+
return this.inner.append(input);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
readAll(): readonly ControlEventEnvelope[] {
|
|
167
|
+
return this.inner.readAll();
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export class ForgeAgentConductor {
|
|
172
|
+
private readonly pinnedClock: PinnedClock;
|
|
173
|
+
private readonly validatingJournal: ControlJournal;
|
|
174
|
+
private readonly inner: LegacyForgeAgentConductor;
|
|
175
|
+
private readonly resourceLedger?: ResourceLedger;
|
|
176
|
+
private readonly sourceLedger?: ResourceLedger;
|
|
177
|
+
private readonly sourceLedgerBaseline?: ResourceLedgerSnapshot;
|
|
178
|
+
|
|
179
|
+
constructor(
|
|
180
|
+
private readonly rootExecutionId: string,
|
|
181
|
+
private readonly journal: ControlJournal,
|
|
182
|
+
clock: Clock,
|
|
183
|
+
digest: DigestFunction,
|
|
184
|
+
private readonly ownerAuthorizationVerifier: OwnerAuthorizationVerifier,
|
|
185
|
+
resourceLedger?: ResourceLedger,
|
|
186
|
+
) {
|
|
187
|
+
this.pinnedClock = new PinnedClock(clock);
|
|
188
|
+
this.sourceLedger = resourceLedger;
|
|
189
|
+
this.sourceLedgerBaseline = resourceLedger?.snapshot();
|
|
190
|
+
|
|
191
|
+
const existing = journal.readAll();
|
|
192
|
+
if (existing.some((event) => event.rootExecutionId !== rootExecutionId)) {
|
|
193
|
+
throw new AgentFabricError(
|
|
194
|
+
"AF_INVALID_EVENT",
|
|
195
|
+
`Journal contains events outside root execution ${rootExecutionId}`,
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
if (this.sourceLedgerBaseline) {
|
|
200
|
+
const trustedDefinitions = sortedDefinitions(this.sourceLedgerBaseline.definitions);
|
|
201
|
+
if (existing.length === 0) {
|
|
202
|
+
if (snapshotHasUsage(this.sourceLedgerBaseline)) {
|
|
203
|
+
throw new AgentFabricError(
|
|
204
|
+
"AF_INVALID_STATE",
|
|
205
|
+
"A fresh Conductor requires an unused ResourceLedger seed",
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
this.resourceLedger = ResourceLedger.fromSnapshot(this.sourceLedgerBaseline);
|
|
209
|
+
} else {
|
|
210
|
+
const replayed = replayControlState(existing, {
|
|
211
|
+
ownerAuthorizationVerifier,
|
|
212
|
+
resourceDefinitions: trustedDefinitions,
|
|
213
|
+
});
|
|
214
|
+
if (Object.keys(replayed.resourceDefinitions).length === 0) {
|
|
215
|
+
if (snapshotHasUsage(this.sourceLedgerBaseline)) {
|
|
216
|
+
throw new AgentFabricError(
|
|
217
|
+
"AF_INVALID_STATE",
|
|
218
|
+
"ResourceLedger seed contains usage absent from the journal",
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
this.resourceLedger = ResourceLedger.fromSnapshot(this.sourceLedgerBaseline);
|
|
222
|
+
} else {
|
|
223
|
+
const replaySnapshot = snapshotFromState(replayed);
|
|
224
|
+
if (
|
|
225
|
+
snapshotHasUsage(this.sourceLedgerBaseline) &&
|
|
226
|
+
stableStringify(ledgerProjection(this.sourceLedgerBaseline)) !==
|
|
227
|
+
stableStringify(ledgerProjection(replaySnapshot))
|
|
228
|
+
) {
|
|
229
|
+
throw new AgentFabricError(
|
|
230
|
+
"AF_INVALID_STATE",
|
|
231
|
+
"ResourceLedger seed conflicts with the authoritative replay projection",
|
|
232
|
+
);
|
|
233
|
+
}
|
|
234
|
+
this.resourceLedger = ResourceLedger.fromSnapshot(replaySnapshot);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
} else if (existing.length > 0) {
|
|
238
|
+
replayControlState(existing, { ownerAuthorizationVerifier });
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
this.validatingJournal = new ReplayValidatingJournal(
|
|
242
|
+
journal,
|
|
243
|
+
rootExecutionId,
|
|
244
|
+
() => this.replayTrust(),
|
|
245
|
+
);
|
|
246
|
+
this.inner = new LegacyForgeAgentConductor(
|
|
247
|
+
rootExecutionId,
|
|
248
|
+
this.validatingJournal,
|
|
249
|
+
this.pinnedClock,
|
|
250
|
+
digest,
|
|
251
|
+
ownerAuthorizationVerifier,
|
|
252
|
+
this.resourceLedger,
|
|
253
|
+
);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
state(): ControlState {
|
|
257
|
+
return replayControlState(this.journal.readAll(), this.replayTrust());
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
events(): readonly ControlEventEnvelope[] {
|
|
261
|
+
return this.journal.readAll();
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
registerOwnerAuthorization(authorization: OwnerAuthorization): void {
|
|
265
|
+
this.pinnedClock.run(() => this.inner.registerOwnerAuthorization(authorization));
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
revokeOwnerAuthorization(authorizationId: string, reason: string): void {
|
|
269
|
+
this.pinnedClock.run(() => this.inner.revokeOwnerAuthorization(authorizationId, reason));
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
registerGoal(goal: GoalContract): void {
|
|
273
|
+
this.pinnedClock.run(() => this.inner.registerGoal(goal));
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
registerGrant(grant: ExecutionGrant): void {
|
|
277
|
+
this.pinnedClock.run(() => this.inner.registerGrant(grant));
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
deriveAndRegisterGrant(
|
|
281
|
+
parentGrantId: string,
|
|
282
|
+
request: DerivedGrantRequest,
|
|
283
|
+
ledger?: ResourceLedger,
|
|
284
|
+
): AuthorityResolution {
|
|
285
|
+
const canonicalLedger = this.requireCanonicalLedger(ledger);
|
|
286
|
+
this.assertLedgerSynchronized(canonicalLedger);
|
|
287
|
+
return this.pinnedClock.run(() =>
|
|
288
|
+
this.inner.deriveAndRegisterGrant(parentGrantId, request, canonicalLedger)
|
|
289
|
+
);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
consumeResourceReservation(
|
|
293
|
+
reservationId: string,
|
|
294
|
+
ledger?: ResourceLedger,
|
|
295
|
+
): ResourceReservation {
|
|
296
|
+
const canonicalLedger = this.requireCanonicalLedger(ledger);
|
|
297
|
+
this.assertLedgerSynchronized(canonicalLedger);
|
|
298
|
+
return this.pinnedClock.run(() =>
|
|
299
|
+
this.inner.consumeResourceReservation(reservationId, canonicalLedger)
|
|
300
|
+
);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
releaseResourceReservation(
|
|
304
|
+
reservationId: string,
|
|
305
|
+
ledger?: ResourceLedger,
|
|
306
|
+
): ResourceReservation {
|
|
307
|
+
const canonicalLedger = this.requireCanonicalLedger(ledger);
|
|
308
|
+
this.assertLedgerSynchronized(canonicalLedger);
|
|
309
|
+
return this.pinnedClock.run(() =>
|
|
310
|
+
this.inner.releaseResourceReservation(reservationId, canonicalLedger)
|
|
311
|
+
);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
revokeGrant(grantId: string, reason: string): void {
|
|
315
|
+
this.pinnedClock.run(() => this.inner.revokeGrant(grantId, reason));
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
registerPlanDelta(delta: PlanDelta): void {
|
|
319
|
+
this.pinnedClock.run(() => this.inner.registerPlanDelta(delta));
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
activatePlan(revision: RunPlanRevision, expectedCurrentRevisionId: string | null): void {
|
|
323
|
+
this.pinnedClock.run(() => this.inner.activatePlan(revision, expectedCurrentRevisionId));
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
commitDispatchIntent(intent: DispatchIntent): void {
|
|
327
|
+
this.pinnedClock.run(() => this.inner.commitDispatchIntent(intent));
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
createDispatchOffer(
|
|
331
|
+
intentId: string,
|
|
332
|
+
audiencePool: string,
|
|
333
|
+
offerId: string,
|
|
334
|
+
expiresAt: number,
|
|
335
|
+
): DispatchOffer {
|
|
336
|
+
return this.pinnedClock.run(() =>
|
|
337
|
+
this.inner.createDispatchOffer(intentId, audiencePool, offerId, expiresAt)
|
|
338
|
+
);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
claimDispatch(input: ClaimDispatchInput): SchedulingClaim {
|
|
342
|
+
return this.pinnedClock.run(() => this.inner.claimDispatch(input));
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
issuePermit(input: IssuePermitInput): AttemptExecutionPermit {
|
|
346
|
+
return this.pinnedClock.run(() => {
|
|
347
|
+
const state = this.state();
|
|
348
|
+
const claim = getOwn(state.claims, input.claimId);
|
|
349
|
+
if (!claim) {
|
|
350
|
+
throw new AgentFabricError("AF_NOT_FOUND", `Unknown claim: ${input.claimId}`);
|
|
351
|
+
}
|
|
352
|
+
this.assertGoalAuthorityBinding(state, claim.intentId, input.grantId);
|
|
353
|
+
return this.inner.issuePermit(input);
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
authorizeAttemptDispatch(permit: AttemptExecutionPermit): void {
|
|
358
|
+
this.pinnedClock.run(() => {
|
|
359
|
+
this.assertGoalAuthorityBinding(this.state(), permit.intentId, permit.grantId);
|
|
360
|
+
this.inner.authorizeAttemptDispatch(permit);
|
|
361
|
+
});
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
acceptStartupReport(permit: AttemptExecutionPermit, report: ExecutorStartupReport): void {
|
|
365
|
+
this.pinnedClock.run(() => {
|
|
366
|
+
this.assertGoalAuthorityBinding(this.state(), permit.intentId, permit.grantId);
|
|
367
|
+
this.inner.acceptStartupReport(permit, report);
|
|
368
|
+
});
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
recordAttemptUncertainty(
|
|
372
|
+
permit: AttemptExecutionPermit,
|
|
373
|
+
phase: "startup" | "outcome",
|
|
374
|
+
reason: string,
|
|
375
|
+
): AttemptUncertaintyObservation {
|
|
376
|
+
return this.pinnedClock.run(() => {
|
|
377
|
+
const state = this.state();
|
|
378
|
+
if (phase === "outcome" && !getOwn(state.attempts, permit.attemptId)) {
|
|
379
|
+
throw new AgentFabricError(
|
|
380
|
+
"AF_INVALID_STATE",
|
|
381
|
+
"Outcome uncertainty requires a confirmed startup",
|
|
382
|
+
);
|
|
383
|
+
}
|
|
384
|
+
return this.inner.recordAttemptUncertainty(permit, phase, reason);
|
|
385
|
+
});
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
commitOutcome(report: WorkerResultReport): AuthoritativeOutcomeCommit {
|
|
389
|
+
return this.pinnedClock.run(() => {
|
|
390
|
+
const state = this.state();
|
|
391
|
+
this.assertGoalAuthorityBinding(state, report.intentId, getOwn(state.permits, report.permitId)?.grantId);
|
|
392
|
+
return this.inner.commitOutcome(report);
|
|
393
|
+
});
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
private replayTrust(): ReplayTrustContext {
|
|
397
|
+
return {
|
|
398
|
+
ownerAuthorizationVerifier: this.ownerAuthorizationVerifier,
|
|
399
|
+
resourceDefinitions: this.resourceLedger
|
|
400
|
+
? sortedDefinitions(this.resourceLedger.snapshot().definitions)
|
|
401
|
+
: undefined,
|
|
402
|
+
};
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
private assertGoalAuthorityBinding(
|
|
406
|
+
state: ControlState,
|
|
407
|
+
intentId: string,
|
|
408
|
+
grantId: string | undefined,
|
|
409
|
+
): void {
|
|
410
|
+
const intent = getOwn(state.dispatchIntents, intentId);
|
|
411
|
+
const grant = grantId ? getOwn(state.grants, grantId) : undefined;
|
|
412
|
+
const revision = intent ? getOwn(state.planRevisions, intent.planRevisionId) : undefined;
|
|
413
|
+
const goal = revision ? getOwn(state.goals, revision.goalId) : undefined;
|
|
414
|
+
const authorization = grant ? getOwn(state.authorizations, grant.rootAuthorizationId) : undefined;
|
|
415
|
+
|
|
416
|
+
if (!intent || !grant || !revision || !goal || !authorization) {
|
|
417
|
+
throw new AgentFabricError(
|
|
418
|
+
"AF_GRANT_REJECTED",
|
|
419
|
+
"Execution authority has incomplete GoalContract lineage",
|
|
420
|
+
);
|
|
421
|
+
}
|
|
422
|
+
if (
|
|
423
|
+
intent.rootExecutionId !== this.rootExecutionId ||
|
|
424
|
+
revision.rootExecutionId !== this.rootExecutionId ||
|
|
425
|
+
authorization.rootExecutionId !== this.rootExecutionId ||
|
|
426
|
+
goal.authorityInvocationId !== grant.rootAuthorizationId ||
|
|
427
|
+
!authorization.goalIds.includes(goal.goalId)
|
|
428
|
+
) {
|
|
429
|
+
throw new AgentFabricError(
|
|
430
|
+
"AF_GRANT_REJECTED",
|
|
431
|
+
`Grant ${grant.grantId} is not authorized for goal ${goal.goalId}`,
|
|
432
|
+
);
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
private requireCanonicalLedger(candidate?: ResourceLedger): ResourceLedger {
|
|
437
|
+
if (!this.resourceLedger) {
|
|
438
|
+
throw new AgentFabricError(
|
|
439
|
+
"AF_INVALID_STATE",
|
|
440
|
+
"Resource operations require the ResourceLedger supplied to the Conductor constructor",
|
|
441
|
+
);
|
|
442
|
+
}
|
|
443
|
+
if (candidate) {
|
|
444
|
+
if (!this.sourceLedger || candidate !== this.sourceLedger) {
|
|
445
|
+
throw new AgentFabricError(
|
|
446
|
+
"AF_INVALID_STATE",
|
|
447
|
+
"ResourceLedger substitution is prohibited after Conductor construction",
|
|
448
|
+
);
|
|
449
|
+
}
|
|
450
|
+
if (
|
|
451
|
+
this.sourceLedgerBaseline &&
|
|
452
|
+
stableStringify(candidate.snapshot()) !== stableStringify(this.sourceLedgerBaseline)
|
|
453
|
+
) {
|
|
454
|
+
throw new AgentFabricError(
|
|
455
|
+
"AF_INVALID_STATE",
|
|
456
|
+
"The caller-owned ResourceLedger seed was mutated after Conductor construction",
|
|
457
|
+
);
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
return this.resourceLedger;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
private assertLedgerSynchronized(ledger: ResourceLedger): void {
|
|
464
|
+
const snapshot = ledger.snapshot();
|
|
465
|
+
const state = this.state();
|
|
466
|
+
|
|
467
|
+
if (Object.keys(state.resourceDefinitions).length === 0) {
|
|
468
|
+
const hasUsage = snapshotHasUsage(snapshot);
|
|
469
|
+
if (hasUsage) {
|
|
470
|
+
throw new AgentFabricError(
|
|
471
|
+
"AF_INVALID_STATE",
|
|
472
|
+
"ResourceLedger contains usage that is not represented in the journal",
|
|
473
|
+
);
|
|
474
|
+
}
|
|
475
|
+
return;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
if (stableStringify(ledgerProjection(snapshot)) !== stableStringify(stateLedgerProjection(state))) {
|
|
479
|
+
throw new AgentFabricError(
|
|
480
|
+
"AF_INVALID_STATE",
|
|
481
|
+
"ResourceLedger state diverges from the authoritative replay projection",
|
|
482
|
+
);
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
}
|