@gajae-code/agent-core 0.12.4 → 0.12.6

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.
@@ -1,7 +1,17 @@
1
- import type { RunResourceEntry, RunResourceLedger, RunSettlementProof } from "./types";
1
+ import type {
2
+ ClaimProducerResult,
3
+ ForkProducerResult,
4
+ ReserveProducerResult,
5
+ RunCancellationDomain,
6
+ RunCancellationDomainBridge,
7
+ RunResourceEntry,
8
+ RunResourceKind,
9
+ RunResourceLedger,
10
+ RunResourceProducerLease,
11
+ RunSettlementProof,
12
+ } from "./types";
2
13
 
3
14
  const MAX_TOMBSTONE_ENTRIES = 256;
4
-
5
15
  type RunLifecycle = "open" | "sealed" | "quarantined";
6
16
 
7
17
  interface TrackedResource {
@@ -10,10 +20,14 @@ interface TrackedResource {
10
20
 
11
21
  interface RunState {
12
22
  lifecycle: RunLifecycle;
23
+ resourceRunId: string;
24
+ domain: RunCancellationDomain | undefined;
25
+ domainBridge: RunCancellationDomainBridge;
13
26
  resources: Map<string, TrackedResource>;
14
- /** Bounded public snapshot retained after quarantine. */
15
27
  tombstone: RunResourceEntry[];
16
28
  waiters: Set<SettlementWaiter>;
29
+ claimedOwners: Set<object>;
30
+ released: boolean;
17
31
  }
18
32
 
19
33
  interface SettlementWaiter {
@@ -27,63 +41,93 @@ function copyEntries(entries: readonly RunResourceEntry[]): RunResourceEntry[] {
27
41
 
28
42
  export function createRunResourceLedger(): RunResourceLedger {
29
43
  const runs = new Map<string, RunState>();
44
+ const standaloneDomains = new Map<string, { domain: RunCancellationDomain; controller: AbortController }>();
45
+ const standaloneReleased = new Set<string>();
46
+ const standaloneQuarantined = new Set<string>();
47
+ let bridge: RunCancellationDomainBridge | undefined;
48
+ let agentSessionClaimKey: object | undefined;
30
49
  let sequence = 0;
31
50
 
32
- const snapshot = (state: RunState): RunResourceEntry[] => {
33
- if (state.lifecycle === "quarantined") return copyEntries(state.tombstone);
34
- return [...state.resources.values()].map(resource => ({ ...resource.entry }));
51
+ const standaloneBridge: RunCancellationDomainBridge = {
52
+ open(resourceRunId) {
53
+ if (standaloneQuarantined.has(resourceRunId)) return { ok: false, reason: "quarantined" };
54
+ const existing = standaloneDomains.get(resourceRunId);
55
+ if (existing) return { ok: true, domain: existing.domain, created: false };
56
+ if (standaloneReleased.has(resourceRunId)) return { ok: false, reason: "duplicate_identity" };
57
+ const controller = new AbortController();
58
+ const domain: RunCancellationDomain = { resourceRunId, signal: controller.signal };
59
+ standaloneDomains.set(resourceRunId, { domain, controller });
60
+ return { ok: true, domain, created: true };
61
+ },
62
+ lookup(resourceRunId) {
63
+ return standaloneDomains.get(resourceRunId)?.domain;
64
+ },
65
+ abort(resourceRunId, reason) {
66
+ const record = standaloneDomains.get(resourceRunId);
67
+ if (!record)
68
+ return { ok: false, reason: standaloneQuarantined.has(resourceRunId) ? "quarantined" : "unknown_run" };
69
+ const newlyAborted = !record.controller.signal.aborted;
70
+ if (newlyAborted) record.controller.abort(reason);
71
+ return { ok: true, newlyAborted };
72
+ },
73
+ release(resourceRunId, disposition) {
74
+ const record = standaloneDomains.get(resourceRunId);
75
+ if (!record) return;
76
+ if (disposition === "quarantined") {
77
+ standaloneQuarantined.add(resourceRunId);
78
+ if (!record.controller.signal.aborted) record.controller.abort();
79
+ }
80
+ standaloneDomains.delete(resourceRunId);
81
+ standaloneReleased.add(resourceRunId);
82
+ },
35
83
  };
36
84
 
85
+ const snapshot = (state: RunState): RunResourceEntry[] =>
86
+ state.lifecycle === "quarantined"
87
+ ? copyEntries(state.tombstone)
88
+ : [...state.resources.values()].map(resource => ({ ...resource.entry }));
89
+
37
90
  const settlementProof = (state: RunState): RunSettlementProof | undefined => {
38
- if (state.lifecycle === "quarantined") {
39
- return { status: "unfenced", pending: copyEntries(state.tombstone) };
40
- }
41
- if (state.lifecycle === "sealed" && state.resources.size === 0) {
42
- return { status: "settled" };
43
- }
91
+ if (state.lifecycle === "quarantined")
92
+ return { status: "unfenced", reason: "quarantined", pending: copyEntries(state.tombstone) };
93
+ if (state.lifecycle === "sealed" && state.resources.size === 0) return { status: "settled" };
44
94
  return undefined;
45
95
  };
46
96
 
47
- const removeWaiter = (state: RunState, waiter: SettlementWaiter): void => {
48
- clearTimeout(waiter.timer);
49
- state.waiters.delete(waiter);
97
+ const releaseIfSettled = (state: RunState): void => {
98
+ if (state.released || state.lifecycle !== "sealed" || state.resources.size !== 0 || !state.domain) return;
99
+ state.released = true;
100
+ state.domainBridge.release(state.resourceRunId, "settled");
101
+ state.domain = undefined;
50
102
  };
51
103
 
52
104
  const notify = (state: RunState): void => {
53
105
  const proof = settlementProof(state);
54
- if (!proof) return;
55
- for (const waiter of state.waiters) {
56
- removeWaiter(state, waiter);
57
- waiter.resolve(
58
- proof.status === "unfenced"
59
- ? { status: "unfenced", pending: copyEntries(proof.pending) }
60
- : { status: "settled" },
61
- );
106
+ if (proof) {
107
+ for (const waiter of [...state.waiters]) {
108
+ clearTimeout(waiter.timer);
109
+ state.waiters.delete(waiter);
110
+ waiter.resolve(
111
+ proof.status === "settled"
112
+ ? proof
113
+ : { status: "unfenced", reason: proof.reason, pending: copyEntries(proof.pending) },
114
+ );
115
+ }
62
116
  }
117
+ releaseIfSettled(state);
63
118
  };
64
119
 
65
- const settleTracked = (state: RunState, id: string): void => {
66
- if (!state.resources.delete(id)) return;
67
- notify(state);
120
+ const appendTombstone = (state: RunState, entry: RunResourceEntry): void => {
121
+ state.tombstone.push({ ...entry });
122
+ if (state.tombstone.length > MAX_TOMBSTONE_ENTRIES)
123
+ state.tombstone.splice(0, state.tombstone.length - MAX_TOMBSTONE_ENTRIES);
68
124
  };
69
125
 
70
126
  const observeSettlement = (settled: PromiseLike<unknown>, onSettled: () => void): void => {
71
- // Assimilate the settlement promise once and consume both outcomes so a
72
- // rejected resource cannot become an unhandled rejection.
73
- let promise: Promise<unknown>;
74
127
  try {
75
- promise = Promise.resolve(settled);
128
+ void Promise.resolve(settled).then(onSettled, onSettled);
76
129
  } catch {
77
130
  onSettled();
78
- return;
79
- }
80
- void promise.then(onSettled, onSettled);
81
- };
82
-
83
- const appendTombstone = (state: RunState, entry: RunResourceEntry): void => {
84
- state.tombstone.push({ ...entry });
85
- if (state.tombstone.length > MAX_TOMBSTONE_ENTRIES) {
86
- state.tombstone.splice(0, state.tombstone.length - MAX_TOMBSTONE_ENTRIES);
87
131
  }
88
132
  };
89
133
 
@@ -93,87 +137,184 @@ export function createRunResourceLedger(): RunResourceLedger {
93
137
  state.tombstone = [];
94
138
  for (const resource of state.resources.values()) appendTombstone(state, resource.entry);
95
139
  state.resources.clear();
140
+ if (state.domain) {
141
+ state.domainBridge.abort(state.resourceRunId);
142
+ if (!state.released) {
143
+ state.released = true;
144
+ state.domainBridge.release(state.resourceRunId, "quarantined");
145
+ }
146
+ state.domain = undefined;
147
+ }
96
148
  }
97
149
  notify(state);
98
150
  return copyEntries(state.tombstone);
99
151
  };
100
152
 
153
+ const register = (
154
+ state: RunState,
155
+ kind: RunResourceKind,
156
+ label: string,
157
+ settled: PromiseLike<unknown>,
158
+ ): string | undefined => {
159
+ if (state.lifecycle === "quarantined") {
160
+ const entry: RunResourceEntry = { id: `${++sequence}`, kind, label, registeredAt: Date.now() };
161
+ appendTombstone(state, entry);
162
+ observeSettlement(settled, () => {});
163
+ return undefined;
164
+ }
165
+ // Sealing only freezes admission of genuinely *new* work through
166
+ // reserveProducer()/claimProducer(); it does not mean the run's resources have
167
+ // all been registered yet. `agent_end` is published before seal(), and its
168
+ // handlers register their own post-prompt work while the event is still
169
+ // draining, so this late registration is the normal lifecycle rather than an
170
+ // escaped resource. Admit it into ordinary settlement accounting so the run
171
+ // stays unsettled until it completes; quarantining here would make every
172
+ // cancel unfenced forever.
173
+ const entry: RunResourceEntry = { id: `${++sequence}`, kind, label, registeredAt: Date.now() };
174
+ state.resources.set(entry.id, { entry });
175
+ observeSettlement(settled, () => {
176
+ state.resources.delete(entry.id);
177
+ notify(state);
178
+ });
179
+ return entry.id;
180
+ };
181
+
182
+ const leaseFor = (state: RunState, kind: RunResourceKind, label: string): RunResourceProducerLease | undefined => {
183
+ const domain = state.domain;
184
+ if (!domain) return undefined;
185
+ const completion = Promise.withResolvers<void>();
186
+ if (!register(state, kind, label, completion.promise)) return undefined;
187
+ let closed = false;
188
+ const close = (): void => {
189
+ if (closed) return;
190
+ closed = true;
191
+ completion.resolve();
192
+ };
193
+ const lease: RunResourceProducerLease = {
194
+ resourceRunId: state.resourceRunId,
195
+ domain,
196
+ signal: domain.signal,
197
+ track(childKind, childLabel, settled) {
198
+ if (closed || state.lifecycle === "quarantined") {
199
+ quarantineState(state);
200
+ observeSettlement(settled, () => {});
201
+ return false;
202
+ }
203
+ return register(state, childKind, childLabel, settled) !== undefined;
204
+ },
205
+ fork(expectedDomain, childKind, childLabel): ForkProducerResult {
206
+ if (expectedDomain !== domain) {
207
+ quarantineState(state);
208
+ return { ok: false, reason: "domain_mismatch" };
209
+ }
210
+ const wasQuarantined = state.lifecycle === "quarantined";
211
+ if (closed || wasQuarantined) {
212
+ quarantineState(state);
213
+ return { ok: false, reason: wasQuarantined ? "quarantined" : "parent_closed" };
214
+ }
215
+ const child = leaseFor(state, childKind, childLabel);
216
+ return child ? { ok: true, lease: child } : { ok: false, reason: "quarantined" };
217
+ },
218
+ closeDiscovery: close,
219
+ };
220
+ return lease;
221
+ };
222
+
101
223
  return {
224
+ bindCancellationDomainBridge(nextBridge) {
225
+ if (bridge && bridge !== nextBridge) throw new Error("Run cancellation domain bridge is already bound");
226
+ bridge = nextBridge;
227
+ },
228
+ bindAgentSessionClaimKey(key) {
229
+ if (agentSessionClaimKey && agentSessionClaimKey !== key) {
230
+ throw new Error("AgentSession claim key is already bound");
231
+ }
232
+ agentSessionClaimKey = key;
233
+ },
102
234
  open(resourceRunId) {
103
235
  const existing = runs.get(resourceRunId);
104
- if (existing) return;
105
- runs.set(resourceRunId, {
236
+ if (existing) return existing.lifecycle === "open" ? existing.domain : undefined;
237
+ const domainBridge = bridge ?? standaloneBridge;
238
+ const opened = domainBridge.open(resourceRunId);
239
+ if (!opened.ok) return undefined;
240
+ const state: RunState = {
106
241
  lifecycle: "open",
107
- resources: new Map<string, TrackedResource>(),
242
+ resourceRunId,
243
+ domain: opened.domain,
244
+ domainBridge,
245
+ resources: new Map(),
108
246
  tombstone: [],
109
- waiters: new Set<SettlementWaiter>(),
110
- });
247
+ waiters: new Set(),
248
+ claimedOwners: new Set(),
249
+ released: false,
250
+ };
251
+ runs.set(resourceRunId, state);
252
+ return opened.domain;
253
+ },
254
+ lookupDomain(resourceRunId) {
255
+ return runs.get(resourceRunId)?.domain;
256
+ },
257
+ reserveProducer(resourceRunId, expectedDomain, kind, label): ReserveProducerResult {
258
+ const state = runs.get(resourceRunId);
259
+ if (!state) return { ok: false, reason: "unknown_run" };
260
+ if (state.lifecycle === "quarantined") return { ok: false, reason: "quarantined" };
261
+ if (state.lifecycle !== "open") {
262
+ quarantineState(state);
263
+ return { ok: false, reason: "sealed" };
264
+ }
265
+ if (expectedDomain && expectedDomain !== state.domain) {
266
+ quarantineState(state);
267
+ return { ok: false, reason: "domain_mismatch" };
268
+ }
269
+ const lease = leaseFor(state, kind, label);
270
+ return lease ? { ok: true, lease } : { ok: false, reason: "quarantined" };
271
+ },
272
+ claimProducer(resourceRunId, expectedDomain, ownerKey): ClaimProducerResult {
273
+ if (!agentSessionClaimKey || ownerKey !== agentSessionClaimKey) {
274
+ return { ok: false, reason: "closed" };
275
+ }
276
+ const state = runs.get(resourceRunId);
277
+ if (!state) return { ok: false, reason: "handle_mismatch" };
278
+ if (state.lifecycle === "quarantined") return { ok: false, reason: "quarantined" };
279
+ if (state.lifecycle !== "open") {
280
+ quarantineState(state);
281
+ return { ok: false, reason: "closed" };
282
+ }
283
+ if (expectedDomain && expectedDomain !== state.domain) {
284
+ quarantineState(state);
285
+ return { ok: false, reason: "domain_mismatch" };
286
+ }
287
+ if (state.claimedOwners.has(ownerKey)) {
288
+ quarantineState(state);
289
+ return { ok: false, reason: "already_claimed" };
290
+ }
291
+ state.claimedOwners.add(ownerKey);
292
+ const lease = leaseFor(state, "post_prompt", "agent-session");
293
+ return lease ? { ok: true, lease } : { ok: false, reason: "quarantined" };
111
294
  },
112
-
113
295
  track(resourceRunId, kind, label, settled) {
114
- let state = runs.get(resourceRunId);
296
+ const state = runs.get(resourceRunId);
115
297
  if (!state) {
116
- // Keep track() usable for low-level callers while making the lifecycle
117
- // explicit for settlement: an implicitly-created run is still open and
118
- // therefore cannot settle until seal() is called.
119
- state = {
120
- lifecycle: "open",
121
- resources: new Map<string, TrackedResource>(),
122
- tombstone: [],
123
- waiters: new Set<SettlementWaiter>(),
124
- };
125
- runs.set(resourceRunId, state);
126
- }
127
-
128
- const id = `${++sequence}`;
129
- const entry: RunResourceEntry = { id, kind, label, registeredAt: Date.now() };
130
-
131
- if (state.lifecycle === "quarantined") {
132
- // Quarantine is terminal: late work is retained only in the bounded
133
- // tombstone and never re-enters normal settlement accounting.
134
- appendTombstone(state, entry);
135
298
  observeSettlement(settled, () => {});
136
299
  return;
137
300
  }
138
-
139
- if (state.lifecycle === "sealed") {
140
- // Sealing only freezes admission of *new* work; it does not mean the run's
141
- // resources have all been registered yet. `agent_end` is published before
142
- // seal(), and its handlers register their own post-prompt work while the
143
- // event is still draining, so this late registration is the normal
144
- // lifecycle rather than an escaped resource. Admit it into ordinary
145
- // settlement accounting so the run stays unsettled until it completes;
146
- // quarantining here would make every cancel unfenced forever.
147
- state.resources.set(id, { entry });
148
- observeSettlement(settled, () => settleTracked(state!, id));
149
- return;
150
- }
151
-
152
- state.resources.set(id, { entry });
153
- observeSettlement(settled, () => settleTracked(state!, id));
301
+ register(state, kind, label, settled);
154
302
  },
155
-
156
303
  pending(resourceRunId) {
157
304
  const state = runs.get(resourceRunId);
158
305
  return state ? snapshot(state) : [];
159
306
  },
160
-
161
307
  seal(resourceRunId) {
162
308
  const state = runs.get(resourceRunId);
163
309
  if (state?.lifecycle !== "open") return;
164
310
  state.lifecycle = "sealed";
165
311
  notify(state);
166
312
  },
167
-
168
313
  waitForSettlement(resourceRunId, { graceMs }) {
169
314
  const state = runs.get(resourceRunId);
170
- if (!state) {
171
- return Promise.resolve({ status: "unfenced", pending: [] });
172
- }
173
-
315
+ if (!state) return Promise.resolve({ status: "unfenced", reason: "unknown_run", pending: [] });
174
316
  const immediate = settlementProof(state);
175
317
  if (immediate) return Promise.resolve(immediate);
176
-
177
318
  const { promise, resolve } = Promise.withResolvers<RunSettlementProof>();
178
319
  let waiter!: SettlementWaiter;
179
320
  waiter = {
@@ -185,6 +326,7 @@ export function createRunResourceLedger(): RunResourceLedger {
185
326
  resolve(
186
327
  settled ?? {
187
328
  status: "unfenced",
329
+ reason: state.lifecycle === "open" ? "run_not_sealed" : "resources_pending",
188
330
  pending: snapshot(state),
189
331
  },
190
332
  );
@@ -195,19 +337,9 @@ export function createRunResourceLedger(): RunResourceLedger {
195
337
  state.waiters.add(waiter);
196
338
  return promise;
197
339
  },
198
-
199
340
  quarantine(resourceRunId) {
200
- let state = runs.get(resourceRunId);
201
- if (!state) {
202
- state = {
203
- lifecycle: "quarantined",
204
- resources: new Map<string, TrackedResource>(),
205
- tombstone: [],
206
- waiters: new Set<SettlementWaiter>(),
207
- };
208
- runs.set(resourceRunId, state);
209
- }
210
- return quarantineState(state);
341
+ const state = runs.get(resourceRunId);
342
+ return state ? quarantineState(state) : [];
211
343
  },
212
344
  };
213
345
  }
package/src/telemetry.ts CHANGED
@@ -1687,9 +1687,9 @@ export async function instrumentedCompleteSimple<TApi extends Api>(
1687
1687
  // for the cost / gateway hooks without stealing them from the caller.
1688
1688
  let capturedHeaders: Readonly<Record<string, string>> | undefined;
1689
1689
  const userOnResponse = options.onResponse;
1690
- const captureOnResponse: NonNullable<SimpleStreamOptions["onResponse"]> = (response, modelInfo) => {
1690
+ const captureOnResponse: NonNullable<SimpleStreamOptions["onResponse"]> = (response, modelInfo, scope) => {
1691
1691
  capturedHeaders = response.headers;
1692
- return userOnResponse?.(response, modelInfo);
1692
+ return userOnResponse?.(response, modelInfo, scope);
1693
1693
  };
1694
1694
 
1695
1695
  try {
package/src/types.ts CHANGED
@@ -17,6 +17,7 @@ import type {
17
17
  TSchema,
18
18
  } from "@gajae-code/ai";
19
19
  import type { AppendOnlyContextManager } from "./append-only-context";
20
+ import type { AttemptMinter, AttemptRunHandle, AttemptScope } from "./attempt-scope";
20
21
  import type { HarmonyAuditEvent } from "./harmony-leak";
21
22
  import type { AgentRunCoverage, AgentRunSummary } from "./run-collector";
22
23
  import type { AgentTelemetryConfig } from "./telemetry";
@@ -38,11 +39,91 @@ export interface RunResourceEntry {
38
39
  registeredAt: number;
39
40
  }
40
41
 
41
- export type RunSettlementProof = { status: "settled" } | { status: "unfenced"; pending: RunResourceEntry[] };
42
+ export type RunSettlementReason = "unknown_run" | "run_not_sealed" | "resources_pending" | "quarantined";
43
+ export type RunSettlementProof =
44
+ | { status: "settled" }
45
+ | { status: "unfenced"; reason: RunSettlementReason; pending: RunResourceEntry[] };
46
+
47
+ export interface RunCancellationDomain {
48
+ readonly resourceRunId: string;
49
+ readonly signal: AbortSignal;
50
+ }
51
+
52
+ export interface RunCancellationDomainBridge {
53
+ open(
54
+ resourceRunId: string,
55
+ ):
56
+ | { ok: true; domain: RunCancellationDomain; created: boolean }
57
+ | { ok: false; reason: "duplicate_identity" | "quarantined" };
58
+ lookup(resourceRunId: string): RunCancellationDomain | undefined;
59
+ abort(
60
+ resourceRunId: string,
61
+ reason?: unknown,
62
+ ): { ok: true; newlyAborted: boolean } | { ok: false; reason: "unknown_run" | "quarantined" };
63
+ release(resourceRunId: string, disposition: "settled" | "quarantined"): void;
64
+ }
65
+
66
+ export type ReserveProducerResult =
67
+ | { ok: true; lease: RunResourceProducerLease }
68
+ | { ok: false; reason: "unknown_run" | "sealed" | "quarantined" | "domain_mismatch" };
69
+ export type ClaimProducerResult =
70
+ | { ok: true; lease: RunResourceProducerLease }
71
+ | { ok: false; reason: "already_claimed" | "handle_mismatch" | "domain_mismatch" | "closed" | "quarantined" };
72
+ export type ForkProducerResult =
73
+ | { ok: true; lease: RunResourceProducerLease }
74
+ | { ok: false; reason: "parent_closed" | "quarantined" | "domain_mismatch" };
75
+
76
+ export interface RunResourceProducerLease {
77
+ readonly resourceRunId: string;
78
+ readonly domain: RunCancellationDomain;
79
+ readonly signal: AbortSignal;
80
+ track(kind: RunResourceKind, label: string, settled: PromiseLike<unknown>): boolean;
81
+ fork(expectedDomain: RunCancellationDomain, kind: RunResourceKind, label: string): ForkProducerResult;
82
+ closeDiscovery(): void;
83
+ }
84
+
85
+ export interface AgentTerminalOwnerContext {
86
+ readonly resourceRunId: string;
87
+ readonly domain: RunCancellationDomain;
88
+ }
89
+
90
+ const terminalOwnerContexts = new WeakMap<object, AgentTerminalOwnerContext>();
91
+
92
+ export function setAgentTerminalOwnerContext(event: object, context: AgentTerminalOwnerContext): void {
93
+ terminalOwnerContexts.set(event, context);
94
+ }
95
+
96
+ export function getAgentTerminalOwnerContext(event: object): AgentTerminalOwnerContext | undefined {
97
+ return terminalOwnerContexts.get(event);
98
+ }
99
+ export interface StandaloneRunOwnership {
100
+ readonly resourceRunId: string;
101
+ readonly domain: RunCancellationDomain;
102
+ claimContinuation():
103
+ | { ok: true; ownership: StandaloneRunOwnership }
104
+ | { ok: false; reason: "already_claimed" | "terminal" | "quarantined" };
105
+ abandon(reason: "cancelled" | "error"): void;
106
+ }
42
107
 
43
108
  export interface RunResourceLedger {
109
+ /** Bind the bridge once, before any logical run may be opened. */
110
+ bindCancellationDomainBridge(bridge: RunCancellationDomainBridge): void;
111
+ /** Bind the unforgeable AgentSession claim key once, before terminal publication. */
112
+ bindAgentSessionClaimKey(key: object): void;
44
113
  /** Reserve a run handle before publishing its `agent_start` event. */
45
- open(resourceRunId: string): void;
114
+ open(resourceRunId: string): RunCancellationDomain | undefined;
115
+ lookupDomain(resourceRunId: string): RunCancellationDomain | undefined;
116
+ reserveProducer(
117
+ resourceRunId: string,
118
+ expectedDomain: RunCancellationDomain | undefined,
119
+ kind: RunResourceKind,
120
+ label: string,
121
+ ): ReserveProducerResult;
122
+ claimProducer(
123
+ resourceRunId: string,
124
+ expectedDomain: RunCancellationDomain | undefined,
125
+ ownerKey: object,
126
+ ): ClaimProducerResult;
46
127
  track(resourceRunId: string, kind: RunResourceKind, label: string, settled: PromiseLike<unknown>): void;
47
128
  pending(resourceRunId: string): RunResourceEntry[];
48
129
  /** Seal a run after terminal event publication; only sealed empty runs settle. */
@@ -73,6 +154,10 @@ export interface ManagedAttemptContinuationOwnership {
73
154
  /** Stable managed logical-run id; use for all terminal completion requests. */
74
155
  readonly logicalRunId: ManagedLogicalRunId;
75
156
  readonly generation: number;
157
+ readonly domain: RunCancellationDomain;
158
+ readonly lease: RunResourceProducerLease;
159
+ /** Immutable per-attempt handle used by terminalizers and continuations. */
160
+ readonly handle: AttemptRunHandle;
76
161
  isCurrent(): boolean;
77
162
  }
78
163
 
@@ -94,9 +179,10 @@ export type ManagedAttemptOutcome =
94
179
  /** Exact provider transport facts, including retry headers, for fallback policy. */
95
180
  transportFailure?: TransportFailureFacts;
96
181
  };
182
+ scope?: AttemptScope;
97
183
  }
98
- | { type: "context_overflow_discarded"; message: AssistantMessage }
99
- | { type: "run_terminal"; reason: "cancelled" | "error" | "exhausted" };
184
+ | { type: "context_overflow_discarded"; message: AssistantMessage; scope?: AttemptScope }
185
+ | { type: "run_terminal"; reason: "cancelled" | "error" | "exhausted"; scope?: AttemptScope };
100
186
 
101
187
  export type ManagedAttemptOutcomeHandler = (
102
188
  outcome: ManagedAttemptOutcome,
@@ -127,6 +213,10 @@ export interface AgentLoopConfig extends SimpleStreamOptions {
127
213
 
128
214
  /** Receives a managed invocation outcome without publishing provisional lifecycle events. */
129
215
  onManagedAttemptOutcome?: ManagedAttemptOutcomeHandler;
216
+ /** Per-attempt scope allocator for direct loop callers. */
217
+ attemptMinter?: AttemptMinter;
218
+ /** Scope allocated by the owning Agent for the first attempt in this loop. */
219
+ initialScope?: AttemptScope;
130
220
 
131
221
  /**
132
222
  * When to interrupt tool execution for steering messages.
@@ -199,7 +289,7 @@ export interface AgentLoopConfig extends SimpleStreamOptions {
199
289
  * }
200
290
  * ```
201
291
  */
202
- transformContext?: (messages: AgentMessage[], signal?: AbortSignal) => Promise<AgentMessage[]>;
292
+ transformContext?: (messages: AgentMessage[], signal?: AbortSignal, scope?: AttemptScope) => Promise<AgentMessage[]>;
203
293
 
204
294
  /**
205
295
  * Resolves an API key dynamically for each LLM call.
@@ -383,6 +473,12 @@ export interface AgentLoopConfig extends SimpleStreamOptions {
383
473
  resourceLedger?: RunResourceLedger;
384
474
  /** Stable resource ownership identifier for this prompt run. */
385
475
  resourceRunId?: string;
476
+ /** Immutable logical cancellation domain bound by the resource ledger. */
477
+ resourceCancellationDomain?: RunCancellationDomain;
478
+ /** Agent passes caller ownership; direct loop callers retain loop-owned sealing. */
479
+ resourceSealOwner?: "caller" | "loop";
480
+ /** Opaque ownership required to resume a standalone maintenance lifecycle. */
481
+ standaloneRunOwnership?: StandaloneRunOwnership;
386
482
  }
387
483
 
388
484
  /**
@@ -526,7 +622,8 @@ export interface RenderResultOptions {
526
622
  * Apps can extend via declaration merging.
527
623
  */
528
624
  export interface AgentToolContext {
529
- // Empty by default - apps extend via declaration merging
625
+ /** Per-attempt scope used to attribute tool lifecycle and extension delivery. */
626
+ attemptScope?: AttemptScope;
530
627
  }
531
628
 
532
629
  export type AgentToolExecFn<TParameters extends TSchema = TSchema, TDetails = any, TTheme = unknown> = (
@@ -597,7 +694,7 @@ export interface AgentContext {
597
694
  */
598
695
  export type AgentEvent =
599
696
  // Agent lifecycle
600
- | { type: "agent_start" }
697
+ | { type: "agent_start"; scope?: AttemptScope }
601
698
  | {
602
699
  type: "agent_end";
603
700
  messages: AgentMessage[];
@@ -608,16 +705,43 @@ export type AgentEvent =
608
705
  /** Present iff `AgentTelemetryConfig` was supplied on this run. */
609
706
  telemetry?: AgentRunSummary;
610
707
  coverage?: AgentRunCoverage;
708
+ scope?: AttemptScope;
611
709
  }
612
710
  // Turn lifecycle - a turn is one assistant response + any tool calls/results
613
- | { type: "turn_start" }
614
- | { type: "turn_end"; message: AgentMessage; toolResults: ToolResultMessage[] }
711
+ | { type: "turn_start"; scope?: AttemptScope }
712
+ | { type: "turn_end"; message: AgentMessage; toolResults: ToolResultMessage[]; scope?: AttemptScope }
615
713
  // Message lifecycle - emitted for user, assistant, and toolResult messages
616
- | { type: "message_start"; message: AgentMessage }
714
+ | { type: "message_start"; message: AgentMessage; scope?: AttemptScope }
617
715
  // Only emitted for assistant messages during streaming
618
- | { type: "message_update"; message: AgentMessage; assistantMessageEvent: AssistantMessageEvent }
619
- | { type: "message_end"; message: AgentMessage }
716
+ | {
717
+ type: "message_update";
718
+ message: AgentMessage;
719
+ assistantMessageEvent: AssistantMessageEvent;
720
+ scope?: AttemptScope;
721
+ }
722
+ | { type: "message_end"; message: AgentMessage; scope?: AttemptScope }
620
723
  // Tool execution lifecycle
621
- | { type: "tool_execution_start"; toolCallId: string; toolName: string; args: any; intent?: string }
622
- | { type: "tool_execution_update"; toolCallId: string; toolName: string; args: any; partialResult: any }
623
- | { type: "tool_execution_end"; toolCallId: string; toolName: string; result: any; isError?: boolean };
724
+ | {
725
+ type: "tool_execution_start";
726
+ toolCallId: string;
727
+ toolName: string;
728
+ args: any;
729
+ intent?: string;
730
+ scope?: AttemptScope;
731
+ }
732
+ | {
733
+ type: "tool_execution_update";
734
+ toolCallId: string;
735
+ toolName: string;
736
+ args: any;
737
+ partialResult: any;
738
+ scope?: AttemptScope;
739
+ }
740
+ | {
741
+ type: "tool_execution_end";
742
+ toolCallId: string;
743
+ toolName: string;
744
+ result: any;
745
+ isError?: boolean;
746
+ scope?: AttemptScope;
747
+ };