@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.
- package/CHANGELOG.md +7 -0
- package/dist/types/agent-loop.d.ts +3 -2
- package/dist/types/agent.d.ts +16 -4
- package/dist/types/attempt-scope.d.ts +84 -0
- package/dist/types/types.d.ts +111 -2
- package/package.json +4 -4
- package/src/agent-loop.ts +394 -104
- package/src/agent.ts +164 -31
- package/src/attempt-scope.ts +195 -0
- package/src/proxy.ts +1 -1
- package/src/run-resource-ledger.ts +233 -101
- package/src/telemetry.ts +2 -2
- package/src/types.ts +139 -15
|
@@ -1,7 +1,17 @@
|
|
|
1
|
-
import type {
|
|
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
|
|
33
|
-
|
|
34
|
-
|
|
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
|
|
48
|
-
|
|
49
|
-
state.
|
|
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 (
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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
|
|
66
|
-
|
|
67
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
242
|
+
resourceRunId,
|
|
243
|
+
domain: opened.domain,
|
|
244
|
+
domainBridge,
|
|
245
|
+
resources: new Map(),
|
|
108
246
|
tombstone: [],
|
|
109
|
-
waiters: new Set
|
|
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
|
-
|
|
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
|
-
|
|
201
|
-
|
|
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
|
|
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):
|
|
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
|
-
|
|
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
|
-
| {
|
|
619
|
-
|
|
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
|
-
| {
|
|
622
|
-
|
|
623
|
-
|
|
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
|
+
};
|