@gajae-code/agent-core 0.12.5 → 0.12.7
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
package/src/agent.ts
CHANGED
|
@@ -23,6 +23,8 @@ import {
|
|
|
23
23
|
import { extractHttpStatusFromError } from "@gajae-code/utils";
|
|
24
24
|
import { agentLoop, agentLoopContinue } from "./agent-loop";
|
|
25
25
|
import type { AppendOnlyContextManager } from "./append-only-context";
|
|
26
|
+
import type { AttemptRunHandle, AttemptScope } from "./attempt-scope";
|
|
27
|
+
import { createAttemptScopeAuthority } from "./attempt-scope";
|
|
26
28
|
import type { HarmonyAuditEvent } from "./harmony-leak";
|
|
27
29
|
import { assertImagePlaceholdersHavePayload } from "./image-placeholder-guard";
|
|
28
30
|
import { createRunResourceLedger } from "./run-resource-ledger";
|
|
@@ -39,11 +41,14 @@ import type {
|
|
|
39
41
|
ManagedAttemptDecision,
|
|
40
42
|
ManagedAttemptOutcome,
|
|
41
43
|
ManagedLogicalRunId,
|
|
44
|
+
RunCancellationDomain,
|
|
45
|
+
RunCancellationDomainBridge,
|
|
42
46
|
RunResourceLedger,
|
|
43
47
|
RunTerminalRequest,
|
|
44
48
|
StreamFn,
|
|
45
49
|
ToolCallContext,
|
|
46
50
|
} from "./types";
|
|
51
|
+
import { setAgentTerminalOwnerContext } from "./types";
|
|
47
52
|
|
|
48
53
|
function assertUserImagePlaceholdersHavePayload(messages: readonly AgentMessage[]): void {
|
|
49
54
|
for (const message of messages) {
|
|
@@ -125,7 +130,7 @@ export interface AgentOptions {
|
|
|
125
130
|
* Optional transform applied to context before convertToLlm.
|
|
126
131
|
* Use for context pruning, injecting external context, etc.
|
|
127
132
|
*/
|
|
128
|
-
transformContext?: (messages: AgentMessage[], signal?: AbortSignal) => Promise<AgentMessage[]>;
|
|
133
|
+
transformContext?: (messages: AgentMessage[], signal?: AbortSignal, scope?: AttemptScope) => Promise<AgentMessage[]>;
|
|
129
134
|
|
|
130
135
|
/**
|
|
131
136
|
* Steering mode: "all" = send all steering messages at once, "one-at-a-time" = one per turn
|
|
@@ -296,8 +301,11 @@ export interface AgentPromptOptions {
|
|
|
296
301
|
toolChoice?: ToolChoice;
|
|
297
302
|
/** Disable transport replay; fallback accounting is owned by the caller. */
|
|
298
303
|
fallbackManaged?: boolean;
|
|
304
|
+
/** Continue a cooperative maintenance checkpoint under its existing logical run and cancellation domain. */
|
|
305
|
+
maintenanceContinuation?: boolean;
|
|
299
306
|
/** Called synchronously after this invocation claims the agent run, before asynchronous provider work. */
|
|
300
|
-
|
|
307
|
+
/** Receives the immutable run handle as the first callback argument. */
|
|
308
|
+
onRunAccepted?: (...args: any[]) => void;
|
|
301
309
|
/** Called once immediately before every managed upstream request. */
|
|
302
310
|
nextFallbackAttempt?: AgentLoopConfig["nextFallbackAttempt"];
|
|
303
311
|
/** Called after a managed upstream request is accepted and committed. */
|
|
@@ -330,6 +338,8 @@ export class Agent {
|
|
|
330
338
|
error: undefined,
|
|
331
339
|
};
|
|
332
340
|
#contextRevision = 0;
|
|
341
|
+
#attemptAuthority = createAttemptScopeAuthority();
|
|
342
|
+
#runHandles = new Map<number | ManagedLogicalRunId, AttemptRunHandle>();
|
|
333
343
|
|
|
334
344
|
#listeners = new Set<(e: AgentEvent) => void>();
|
|
335
345
|
#abortController?: AbortController;
|
|
@@ -367,8 +377,8 @@ export class Agent {
|
|
|
367
377
|
#runSequence = 0;
|
|
368
378
|
#activeRunId?: number;
|
|
369
379
|
#activeResourceRunId?: string;
|
|
380
|
+
#activeResourceCancellationDomain?: RunCancellationDomain;
|
|
370
381
|
#continuationGeneration = 0;
|
|
371
|
-
#activeFallbackManaged = false;
|
|
372
382
|
#kimiApiFormat?: "openai" | "anthropic";
|
|
373
383
|
#preferWebsockets?: boolean;
|
|
374
384
|
#transformToolCallArguments?: (args: Record<string, unknown>, toolName: string) => Record<string, unknown>;
|
|
@@ -395,6 +405,20 @@ export class Agent {
|
|
|
395
405
|
#terminalizedLogicalRunIds = new Set<ManagedLogicalRunId>();
|
|
396
406
|
#managedLogicalRunOwner?: ManagedLogicalRunId;
|
|
397
407
|
readonly resourceLedger: RunResourceLedger = createRunResourceLedger();
|
|
408
|
+
bindRunCancellationDomainBridge(bridge: RunCancellationDomainBridge, agentSessionClaimKey?: object): void {
|
|
409
|
+
this.resourceLedger.bindCancellationDomainBridge(bridge);
|
|
410
|
+
if (agentSessionClaimKey) this.resourceLedger.bindAgentSessionClaimKey(agentSessionClaimKey);
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
/** Mint a side-attempt scope and its authority unregister function. */
|
|
414
|
+
mintSideAttemptScope(): { scope: AttemptScope; dispose: () => void } {
|
|
415
|
+
return this.#attemptAuthority.mintSide();
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
/** Return the Agent-owned attempt scope authority for session record injection. */
|
|
419
|
+
getAttemptScopeAuthority() {
|
|
420
|
+
return this.#attemptAuthority;
|
|
421
|
+
}
|
|
398
422
|
|
|
399
423
|
streamFn: StreamFn;
|
|
400
424
|
getApiKey?: (provider: string) => Promise<string | undefined> | string | undefined;
|
|
@@ -1155,20 +1179,32 @@ export class Agent {
|
|
|
1155
1179
|
* did not drain. The abandoned provider/tool stream may still settle later, so
|
|
1156
1180
|
* #runLoop guards every state mutation with a run id.
|
|
1157
1181
|
*/
|
|
1158
|
-
forceAbort(reason = "Force aborted"): boolean {
|
|
1182
|
+
forceAbort(reason = "Force aborted", logicalRunId?: ManagedLogicalRunId | number): boolean {
|
|
1183
|
+
const targetLogicalRunId = logicalRunId ?? this.#managedLogicalRunOwner ?? this.#activeRunId;
|
|
1184
|
+
const handle = targetLogicalRunId !== undefined ? this.#runHandles.get(targetLogicalRunId) : undefined;
|
|
1159
1185
|
const runId = this.#activeRunId;
|
|
1160
1186
|
const managedLogicalRunId = this.#managedLogicalRunOwner;
|
|
1187
|
+
const activeLogicalRunId = managedLogicalRunId ?? runId;
|
|
1188
|
+
if (
|
|
1189
|
+
targetLogicalRunId !== undefined &&
|
|
1190
|
+
activeLogicalRunId !== undefined &&
|
|
1191
|
+
activeLogicalRunId !== targetLogicalRunId
|
|
1192
|
+
) {
|
|
1193
|
+
throw new Error(`forceAbort: logicalRunId ${targetLogicalRunId} does not match the active run`);
|
|
1194
|
+
}
|
|
1195
|
+
const activeResourceDomain = this.#activeResourceCancellationDomain;
|
|
1196
|
+
const activeResourceRunId = this.#activeResourceRunId;
|
|
1161
1197
|
const hadActiveRun = runId !== undefined && (this.#runningPrompt !== undefined || this.#state.isStreaming);
|
|
1162
1198
|
if (!hadActiveRun) return false;
|
|
1163
1199
|
|
|
1164
1200
|
this.#abortController?.abort(reason);
|
|
1165
1201
|
this.#continuationGeneration++;
|
|
1202
|
+
this.#attemptAuthority.advanceMain();
|
|
1166
1203
|
this.#state.isStreaming = false;
|
|
1167
1204
|
this.#state.streamMessage = null;
|
|
1168
1205
|
this.#state.pendingToolCalls = new Set<string>();
|
|
1169
1206
|
this.#abortController = undefined;
|
|
1170
1207
|
this.#cursorToolResultBuffer = [];
|
|
1171
|
-
this.resourceLedger.quarantine(this.#activeResourceRunId ?? String(managedLogicalRunId ?? runId));
|
|
1172
1208
|
this.#managedLogicalRunOwner = undefined;
|
|
1173
1209
|
|
|
1174
1210
|
const resolve = this.#resolveRunningPrompt;
|
|
@@ -1176,12 +1212,20 @@ export class Agent {
|
|
|
1176
1212
|
this.#resolveRunningPrompt = undefined;
|
|
1177
1213
|
this.#activeRunId = undefined;
|
|
1178
1214
|
this.#activeResourceRunId = undefined;
|
|
1215
|
+
this.#activeResourceCancellationDomain = undefined;
|
|
1179
1216
|
resolve?.();
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1217
|
+
this.#finalizeRun(
|
|
1218
|
+
activeLogicalRunId ?? runId!,
|
|
1219
|
+
{
|
|
1220
|
+
type: "agent_end",
|
|
1221
|
+
messages: [],
|
|
1222
|
+
stopReason: "cancelled",
|
|
1223
|
+
scope: handle?.scope,
|
|
1224
|
+
},
|
|
1225
|
+
undefined,
|
|
1226
|
+
activeResourceDomain,
|
|
1227
|
+
);
|
|
1228
|
+
if (activeResourceRunId) this.resourceLedger.quarantine(activeResourceRunId);
|
|
1185
1229
|
return true;
|
|
1186
1230
|
}
|
|
1187
1231
|
|
|
@@ -1218,6 +1262,8 @@ export class Agent {
|
|
|
1218
1262
|
*/
|
|
1219
1263
|
requestRunTerminal(logicalRunId: ManagedLogicalRunId, request: RunTerminalRequest): boolean {
|
|
1220
1264
|
if (this.#terminalizedLogicalRunIds.has(logicalRunId)) return false;
|
|
1265
|
+
const handle = this.#runHandles.get(logicalRunId);
|
|
1266
|
+
if (!handle) throw new Error(`requestRunTerminal: unknown logicalRunId ${logicalRunId} (no attempt handle)`);
|
|
1221
1267
|
if (this.#managedLogicalRunOwner === logicalRunId) {
|
|
1222
1268
|
this.#managedLogicalRunOwner = undefined;
|
|
1223
1269
|
}
|
|
@@ -1227,6 +1273,7 @@ export class Agent {
|
|
|
1227
1273
|
type: "agent_end",
|
|
1228
1274
|
messages: request.messages ?? [],
|
|
1229
1275
|
...(request.stopReason === "cancelled" ? { stopReason: "cancelled" as const } : {}),
|
|
1276
|
+
scope: handle.scope,
|
|
1230
1277
|
},
|
|
1231
1278
|
() => {
|
|
1232
1279
|
for (const message of request.messages ?? []) {
|
|
@@ -1350,6 +1397,10 @@ export class Agent {
|
|
|
1350
1397
|
const model = this.#state.model;
|
|
1351
1398
|
if (!model) throw new Error("No model configured");
|
|
1352
1399
|
|
|
1400
|
+
const maintenanceContinuation = options?.maintenanceContinuation === true;
|
|
1401
|
+
if (maintenanceContinuation && this.#managedLogicalRunOwner === undefined) {
|
|
1402
|
+
throw new Error("Maintenance continuation ownership is unavailable");
|
|
1403
|
+
}
|
|
1353
1404
|
let skipInitialSteeringPoll = options?.skipInitialSteeringPoll === true;
|
|
1354
1405
|
|
|
1355
1406
|
const { promise, resolve } = Promise.withResolvers<void>();
|
|
@@ -1366,14 +1417,34 @@ export class Agent {
|
|
|
1366
1417
|
this.#state.error = undefined;
|
|
1367
1418
|
|
|
1368
1419
|
const fallbackManaged = options?.fallbackManaged === true;
|
|
1369
|
-
const managedLogicalRunOwner = fallbackManaged
|
|
1420
|
+
const managedLogicalRunOwner = fallbackManaged
|
|
1421
|
+
? (this.#managedLogicalRunOwner ?? runId)
|
|
1422
|
+
: maintenanceContinuation
|
|
1423
|
+
? this.#managedLogicalRunOwner
|
|
1424
|
+
: undefined;
|
|
1425
|
+
const continuesLogicalRun = fallbackManaged || maintenanceContinuation;
|
|
1370
1426
|
const startsManagedLogicalRun = fallbackManaged && this.#managedLogicalRunOwner === undefined;
|
|
1371
1427
|
this.#activeResourceRunId = String(managedLogicalRunOwner ?? runId);
|
|
1372
|
-
this.resourceLedger.open(this.#activeResourceRunId);
|
|
1373
|
-
|
|
1428
|
+
this.#activeResourceCancellationDomain = this.resourceLedger.open(this.#activeResourceRunId);
|
|
1429
|
+
if (!this.#activeResourceCancellationDomain) {
|
|
1430
|
+
this.#state.isStreaming = false;
|
|
1431
|
+
this.#abortController = undefined;
|
|
1432
|
+
this.#activeRunId = undefined;
|
|
1433
|
+
this.#activeResourceRunId = undefined;
|
|
1434
|
+
this.#activeResourceCancellationDomain = undefined;
|
|
1435
|
+
this.#runningPrompt = undefined;
|
|
1436
|
+
this.#resolveRunningPrompt = undefined;
|
|
1437
|
+
resolve();
|
|
1438
|
+
throw new Error("Prompt resource cancellation domain is unavailable");
|
|
1439
|
+
}
|
|
1440
|
+
const logicalRunId = managedLogicalRunOwner ?? runId;
|
|
1441
|
+
const scope = this.#attemptAuthority.mintMain();
|
|
1442
|
+
const handle: AttemptRunHandle = { logicalRunId, scope };
|
|
1443
|
+
this.#runHandles.set(logicalRunId, handle);
|
|
1444
|
+
options?.onRunAccepted?.(handle);
|
|
1374
1445
|
if (startsManagedLogicalRun) {
|
|
1375
|
-
this.#managedLogicalRunOwner =
|
|
1376
|
-
this.#emit({ type: "agent_start" });
|
|
1446
|
+
this.#managedLogicalRunOwner = logicalRunId;
|
|
1447
|
+
this.#emit({ type: "agent_start", scope });
|
|
1377
1448
|
}
|
|
1378
1449
|
if (fallbackManaged && this.#cursorToolResultBuffer.length > 0) {
|
|
1379
1450
|
const error = new ManagedCursorInvariantError(
|
|
@@ -1383,6 +1454,7 @@ export class Agent {
|
|
|
1383
1454
|
this.#abortController = undefined;
|
|
1384
1455
|
this.#activeRunId = undefined;
|
|
1385
1456
|
this.#activeResourceRunId = undefined;
|
|
1457
|
+
this.#activeResourceCancellationDomain = undefined;
|
|
1386
1458
|
this.#runningPrompt = undefined;
|
|
1387
1459
|
this.#resolveRunningPrompt = undefined;
|
|
1388
1460
|
resolve();
|
|
@@ -1392,7 +1464,6 @@ export class Agent {
|
|
|
1392
1464
|
}
|
|
1393
1465
|
// Each run gets a fresh buffer only after managed stale-state validation.
|
|
1394
1466
|
this.#cursorToolResultBuffer = [];
|
|
1395
|
-
this.#activeFallbackManaged = fallbackManaged;
|
|
1396
1467
|
|
|
1397
1468
|
const reasoning = this.#state.thinkingLevel;
|
|
1398
1469
|
const context: AgentContext = {
|
|
@@ -1476,12 +1547,16 @@ export class Agent {
|
|
|
1476
1547
|
preferWebsockets: this.#preferWebsockets,
|
|
1477
1548
|
convertToLlm: this.#convertToLlm,
|
|
1478
1549
|
transformContext: this.#transformContext,
|
|
1550
|
+
attemptMinter: { mint: () => this.#attemptAuthority.mintMain() },
|
|
1551
|
+
initialScope: scope,
|
|
1479
1552
|
onPayload: this.#onPayload,
|
|
1480
1553
|
onResponse: this.#onResponse,
|
|
1481
1554
|
onSseEvent: this.#onSseEvent,
|
|
1482
1555
|
signal: abortController.signal,
|
|
1483
1556
|
resourceLedger: this.resourceLedger,
|
|
1484
1557
|
resourceRunId: this.#activeResourceRunId,
|
|
1558
|
+
resourceCancellationDomain: this.#activeResourceCancellationDomain,
|
|
1559
|
+
resourceSealOwner: "caller",
|
|
1485
1560
|
getApiKey: this.getApiKey,
|
|
1486
1561
|
getAuthCredentialType: this.getAuthCredentialType,
|
|
1487
1562
|
getToolContext: this.#getToolContext,
|
|
@@ -1575,8 +1650,8 @@ export class Agent {
|
|
|
1575
1650
|
|
|
1576
1651
|
try {
|
|
1577
1652
|
const stream = messages
|
|
1578
|
-
? agentLoop(messages, context, config, abortController.signal, this.streamFn, !
|
|
1579
|
-
: agentLoopContinue(context, config, abortController.signal, this.streamFn, !
|
|
1653
|
+
? agentLoop(messages, context, config, abortController.signal, this.streamFn, !continuesLogicalRun, scope)
|
|
1654
|
+
: agentLoopContinue(context, config, abortController.signal, this.streamFn, !continuesLogicalRun, scope);
|
|
1580
1655
|
|
|
1581
1656
|
for await (const event of stream) {
|
|
1582
1657
|
if (this.#activeRunId !== runId) {
|
|
@@ -1636,7 +1711,13 @@ export class Agent {
|
|
|
1636
1711
|
}
|
|
1637
1712
|
this.#state.isStreaming = false;
|
|
1638
1713
|
this.#state.streamMessage = null;
|
|
1639
|
-
|
|
1714
|
+
// A maintenance checkpoint is only non-terminal while a continuation will
|
|
1715
|
+
// follow. An aborted maintenance yields none, and because the loop runs with
|
|
1716
|
+
// `resourceSealOwner: "caller"` it deliberately leaves sealing to us, so
|
|
1717
|
+
// treating it as a checkpoint here would leave the run open forever and make
|
|
1718
|
+
// every cancel report `run_not_sealed`.
|
|
1719
|
+
if (event.stopReason === "maintenance" && event.maintenanceOutcome !== "aborted") {
|
|
1720
|
+
this.#managedLogicalRunOwner ??= managedLogicalRunOwner ?? runId;
|
|
1640
1721
|
maintenanceInterrupted = true;
|
|
1641
1722
|
this.#emit(event);
|
|
1642
1723
|
continue;
|
|
@@ -1716,12 +1797,32 @@ export class Agent {
|
|
|
1716
1797
|
) {
|
|
1717
1798
|
continuation = managedDecision.continuation;
|
|
1718
1799
|
}
|
|
1719
|
-
const
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1800
|
+
const domain = this.#activeResourceCancellationDomain;
|
|
1801
|
+
const continuationReservation =
|
|
1802
|
+
continuation && domain
|
|
1803
|
+
? this.resourceLedger.reserveProducer(
|
|
1804
|
+
String(managedLogicalRunOwner ?? runId),
|
|
1805
|
+
domain,
|
|
1806
|
+
"post_prompt",
|
|
1807
|
+
"managed-continuation",
|
|
1808
|
+
)
|
|
1809
|
+
: undefined;
|
|
1810
|
+
if (continuation && !continuationReservation?.ok) {
|
|
1811
|
+
this.requestRunTerminal(managedLogicalRunOwner ?? runId, { stopReason: "error" });
|
|
1812
|
+
continuation = undefined;
|
|
1813
|
+
}
|
|
1814
|
+
const ownership: ManagedAttemptContinuationOwnership | undefined = continuationReservation?.ok
|
|
1815
|
+
? {
|
|
1816
|
+
runId,
|
|
1817
|
+
logicalRunId: managedLogicalRunOwner ?? runId,
|
|
1818
|
+
generation: continuationGeneration,
|
|
1819
|
+
domain: continuationReservation.lease.domain,
|
|
1820
|
+
lease: continuationReservation.lease,
|
|
1821
|
+
handle,
|
|
1822
|
+
isCurrent: () =>
|
|
1823
|
+
this.#continuationGeneration === continuationGeneration && this.#activeRunId === undefined,
|
|
1824
|
+
}
|
|
1825
|
+
: undefined;
|
|
1725
1826
|
if (this.#activeRunId === runId) {
|
|
1726
1827
|
this.#state.isStreaming = false;
|
|
1727
1828
|
this.#state.streamMessage = null;
|
|
@@ -1729,20 +1830,20 @@ export class Agent {
|
|
|
1729
1830
|
this.#abortController = undefined;
|
|
1730
1831
|
this.#activeRunId = undefined;
|
|
1731
1832
|
this.#activeResourceRunId = undefined;
|
|
1732
|
-
this.#
|
|
1833
|
+
this.#activeResourceCancellationDomain = undefined;
|
|
1733
1834
|
this.#resolveRunningPrompt?.();
|
|
1734
1835
|
this.#runningPrompt = undefined;
|
|
1735
1836
|
this.#resolveRunningPrompt = undefined;
|
|
1736
1837
|
}
|
|
1737
1838
|
if (
|
|
1738
|
-
|
|
1839
|
+
continuesLogicalRun &&
|
|
1739
1840
|
!continuation &&
|
|
1740
1841
|
!maintenanceInterrupted &&
|
|
1741
1842
|
this.#managedLogicalRunOwner === managedLogicalRunOwner
|
|
1742
1843
|
) {
|
|
1743
1844
|
this.#managedLogicalRunOwner = undefined;
|
|
1744
1845
|
}
|
|
1745
|
-
if (continuation && ownership
|
|
1846
|
+
if (continuation && ownership?.isCurrent()) {
|
|
1746
1847
|
try {
|
|
1747
1848
|
await continuation(ownership);
|
|
1748
1849
|
if (
|
|
@@ -1765,6 +1866,8 @@ export class Agent {
|
|
|
1765
1866
|
this.requestRunTerminal(managedLogicalRunOwner ?? runId, { stopReason: "error" });
|
|
1766
1867
|
if (this.#managedLogicalRunOwner === managedLogicalRunOwner) this.#managedLogicalRunOwner = undefined;
|
|
1767
1868
|
}
|
|
1869
|
+
} finally {
|
|
1870
|
+
ownership.lease.closeDiscovery();
|
|
1768
1871
|
}
|
|
1769
1872
|
}
|
|
1770
1873
|
}
|
|
@@ -1781,18 +1884,48 @@ export class Agent {
|
|
|
1781
1884
|
logicalRunId: ManagedLogicalRunId,
|
|
1782
1885
|
event?: Extract<AgentEvent, { type: "agent_end" }>,
|
|
1783
1886
|
beforeEvent?: () => void,
|
|
1887
|
+
knownDomain?: RunCancellationDomain,
|
|
1784
1888
|
): void {
|
|
1785
1889
|
if (this.#terminalizedLogicalRunIds.has(logicalRunId)) return;
|
|
1890
|
+
const handle = this.#runHandles.get(logicalRunId);
|
|
1891
|
+
if (!handle && !event?.scope) {
|
|
1892
|
+
throw new Error(`finalizeRun: unknown logicalRunId ${logicalRunId} (no attempt handle)`);
|
|
1893
|
+
}
|
|
1894
|
+
const resourceRunId = String(logicalRunId);
|
|
1895
|
+
const boundDomain = this.resourceLedger.lookupDomain(resourceRunId);
|
|
1896
|
+
const domain = boundDomain ?? knownDomain;
|
|
1897
|
+
const terminalReservation = boundDomain
|
|
1898
|
+
? this.resourceLedger.reserveProducer(resourceRunId, boundDomain, "post_prompt", "terminal-publication")
|
|
1899
|
+
: undefined;
|
|
1786
1900
|
this.#terminalizedLogicalRunIds.add(logicalRunId);
|
|
1787
1901
|
if (this.#terminalizedLogicalRunIds.size > 256) {
|
|
1788
1902
|
this.#terminalizedLogicalRunIds.delete(this.#terminalizedLogicalRunIds.values().next().value!);
|
|
1789
1903
|
}
|
|
1904
|
+
const terminalEvent: Extract<AgentEvent, { type: "agent_end" }> = event ?? {
|
|
1905
|
+
type: "agent_end",
|
|
1906
|
+
messages: [],
|
|
1907
|
+
scope: handle?.scope,
|
|
1908
|
+
};
|
|
1909
|
+
if (handle) terminalEvent.scope = handle.scope;
|
|
1910
|
+
if (domain) {
|
|
1911
|
+
setAgentTerminalOwnerContext(terminalEvent, {
|
|
1912
|
+
resourceRunId,
|
|
1913
|
+
domain,
|
|
1914
|
+
});
|
|
1915
|
+
}
|
|
1790
1916
|
try {
|
|
1791
1917
|
beforeEvent?.();
|
|
1792
|
-
|
|
1918
|
+
this.#emit(terminalEvent);
|
|
1793
1919
|
} finally {
|
|
1794
|
-
|
|
1795
|
-
|
|
1920
|
+
try {
|
|
1921
|
+
terminalReservation?.ok && terminalReservation.lease.closeDiscovery();
|
|
1922
|
+
} finally {
|
|
1923
|
+
try {
|
|
1924
|
+
this.resourceLedger.seal(resourceRunId);
|
|
1925
|
+
} finally {
|
|
1926
|
+
this.#runHandles.delete(logicalRunId);
|
|
1927
|
+
}
|
|
1928
|
+
}
|
|
1796
1929
|
}
|
|
1797
1930
|
}
|
|
1798
1931
|
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-attempt scope identity for request-scoped execution attribution.
|
|
3
|
+
*
|
|
4
|
+
* An AttemptScope is an immutable, frozen value allocated before every
|
|
5
|
+
* observable lifecycle emission for a single provider/agent attempt.
|
|
6
|
+
* It carries a stable `attemptId`, a monotonic `generation` (per-lineage),
|
|
7
|
+
* and a `lineage` discriminator that distinguishes the main attempt from
|
|
8
|
+
* concurrent side attempts (IRC background, ephemeral/btw turns).
|
|
9
|
+
*
|
|
10
|
+
* The `attemptId` + `generation` + `lineage` form the comparable identity.
|
|
11
|
+
* AttemptScope is structurally assignable to AttemptScopeRef in
|
|
12
|
+
* `packages/ai` so it can be carried through `SimpleStreamOptions` and
|
|
13
|
+
* provider hook signatures without a reverse dependency.
|
|
14
|
+
*/
|
|
15
|
+
export type AttemptLineage = "main" | `side:${string}`;
|
|
16
|
+
|
|
17
|
+
export interface AttemptScope {
|
|
18
|
+
readonly attemptId: string;
|
|
19
|
+
readonly generation: number;
|
|
20
|
+
readonly lineage: AttemptLineage;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function attemptScopesEqual(a: AttemptScope, b: AttemptScope): boolean {
|
|
24
|
+
return a.attemptId === b.attemptId && a.generation === b.generation && a.lineage === b.lineage;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Per-lineage currentness authority. Main and side attempts have separate
|
|
29
|
+
* instances so a side attempt never invalidates the main scope, and
|
|
30
|
+
* `forceAbort` advances only the main lineage.
|
|
31
|
+
*/
|
|
32
|
+
export interface LineageCurrentness {
|
|
33
|
+
readonly lineage: AttemptLineage;
|
|
34
|
+
/** True iff no successor scope with a greater generation was allocated in this lineage. */
|
|
35
|
+
isCurrent(scope: AttemptScope): boolean;
|
|
36
|
+
/** Allocate the next generation for the given attempt identity in this lineage. */
|
|
37
|
+
advance(attemptId: string): number;
|
|
38
|
+
/** Allocate the next generation in this lineage. */
|
|
39
|
+
/** Current generation value for this lineage. */
|
|
40
|
+
readonly current: number;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function createLineageCurrentness(lineage: AttemptLineage): LineageCurrentness {
|
|
44
|
+
let current = 0;
|
|
45
|
+
let currentAttemptId: string | undefined;
|
|
46
|
+
return {
|
|
47
|
+
lineage,
|
|
48
|
+
get current() {
|
|
49
|
+
return current;
|
|
50
|
+
},
|
|
51
|
+
isCurrent(scope: AttemptScope): boolean {
|
|
52
|
+
return scope.lineage === lineage && scope.generation === current && scope.attemptId === currentAttemptId;
|
|
53
|
+
},
|
|
54
|
+
advance(attemptId: string): number {
|
|
55
|
+
currentAttemptId = attemptId;
|
|
56
|
+
return ++current;
|
|
57
|
+
},
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Agent-owned authority over all attempt lineages. Owns the main lineage;
|
|
63
|
+
* side lineages are registered/removed with bounded lifecycle.
|
|
64
|
+
*
|
|
65
|
+
* This is the SINGLE source of currentness truth injected into
|
|
66
|
+
* AttemptRecordStore (packages/coding-agent). Every store operation
|
|
67
|
+
* calls `authority.isCurrent(scope)` and fails closed when the authority
|
|
68
|
+
* is missing or the scope is superseded.
|
|
69
|
+
*/
|
|
70
|
+
export interface AttemptScopeAuthority {
|
|
71
|
+
/** Register a side-lineage authority. Returns an unregister function. */
|
|
72
|
+
registerSide(lineage: AttemptLineage, auth: LineageCurrentness): () => void;
|
|
73
|
+
/** True iff the scope's lineage is known and its generation is current. */
|
|
74
|
+
isCurrent(scope: AttemptScope): boolean;
|
|
75
|
+
/** Advance the main lineage (called by forceAbort). Returns the new generation. */
|
|
76
|
+
advanceMain(): number;
|
|
77
|
+
/** Mint the next main-lineage scope. */
|
|
78
|
+
mintMain(): AttemptScope;
|
|
79
|
+
/**
|
|
80
|
+
* Atomically register a fresh side lineage, mint a side scope, and return
|
|
81
|
+
* both the scope and a dispose function. The authority knows the lineage
|
|
82
|
+
* BEFORE the scope is returned, so `isCurrent` succeeds immediately.
|
|
83
|
+
*/
|
|
84
|
+
mintSide(): { scope: AttemptScope; dispose: () => void };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export interface AttemptMinter {
|
|
88
|
+
mint(lineage: AttemptLineage): AttemptScope;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function createAttemptMinter(): AttemptMinter {
|
|
92
|
+
const generations = new Map<AttemptLineage, number>();
|
|
93
|
+
return {
|
|
94
|
+
mint(lineage: AttemptLineage): AttemptScope {
|
|
95
|
+
const gen = (generations.get(lineage) ?? 0) + 1;
|
|
96
|
+
generations.set(lineage, gen);
|
|
97
|
+
return Object.freeze({
|
|
98
|
+
attemptId: crypto.randomUUID(),
|
|
99
|
+
generation: gen,
|
|
100
|
+
lineage,
|
|
101
|
+
});
|
|
102
|
+
},
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const SIDE_LRU_CAP = 1024;
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Create the Agent-owned authority. Owns the main lineage and a bounded
|
|
110
|
+
* (LRU-capped) map of side lineages. Only RETIRED side authorities are
|
|
111
|
+
* eligible for LRU eviction; a live side attempt is never silently
|
|
112
|
+
* invalidated by a newer side registration.
|
|
113
|
+
*/
|
|
114
|
+
export function createAttemptScopeAuthority(): AttemptScopeAuthority {
|
|
115
|
+
const mainAuth = createLineageCurrentness("main");
|
|
116
|
+
const sideAuths = new Map<AttemptLineage, LineageCurrentness>();
|
|
117
|
+
const sideOrder: AttemptLineage[] = [];
|
|
118
|
+
const retiredSet = new Set<AttemptLineage>();
|
|
119
|
+
|
|
120
|
+
function evictRetiredIfNeeded(): void {
|
|
121
|
+
// Only evict RETIRED side authorities. A live side attempt is never
|
|
122
|
+
// evicted by a newer registration.
|
|
123
|
+
while (sideOrder.length > SIDE_LRU_CAP) {
|
|
124
|
+
const retiredIdx = sideOrder.findIndex(l => retiredSet.has(l));
|
|
125
|
+
if (retiredIdx < 0) break;
|
|
126
|
+
const [removed] = sideOrder.splice(retiredIdx, 1);
|
|
127
|
+
if (removed) {
|
|
128
|
+
sideAuths.delete(removed);
|
|
129
|
+
retiredSet.delete(removed);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function mintFor(lineage: AttemptLineage, auth: LineageCurrentness): AttemptScope {
|
|
135
|
+
const attemptId = crypto.randomUUID();
|
|
136
|
+
return Object.freeze({
|
|
137
|
+
attemptId,
|
|
138
|
+
generation: auth.advance(attemptId),
|
|
139
|
+
lineage,
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return {
|
|
144
|
+
registerSide(lineage: AttemptLineage, auth: LineageCurrentness): () => void {
|
|
145
|
+
if (sideAuths.has(lineage)) {
|
|
146
|
+
const idx = sideOrder.indexOf(lineage);
|
|
147
|
+
if (idx >= 0) sideOrder.splice(idx, 1);
|
|
148
|
+
}
|
|
149
|
+
sideAuths.set(lineage, auth);
|
|
150
|
+
sideOrder.push(lineage);
|
|
151
|
+
evictRetiredIfNeeded();
|
|
152
|
+
return () => {
|
|
153
|
+
if (sideAuths.get(lineage) === auth) {
|
|
154
|
+
// Mark as retired but keep in maps until eviction.
|
|
155
|
+
// isCurrent returns false for retired lineages because
|
|
156
|
+
// the auth is still present but the scope is superseded
|
|
157
|
+
// by disposal (generation stays at its last value).
|
|
158
|
+
retiredSet.add(lineage);
|
|
159
|
+
evictRetiredIfNeeded();
|
|
160
|
+
}
|
|
161
|
+
};
|
|
162
|
+
},
|
|
163
|
+
isCurrent(scope: AttemptScope): boolean {
|
|
164
|
+
if (scope.lineage === "main") return mainAuth.isCurrent(scope);
|
|
165
|
+
if (retiredSet.has(scope.lineage)) return false;
|
|
166
|
+
const auth = sideAuths.get(scope.lineage);
|
|
167
|
+
return auth ? auth.isCurrent(scope) : false;
|
|
168
|
+
},
|
|
169
|
+
advanceMain(): number {
|
|
170
|
+
// Advance main lineage to a fresh attemptId so any previously-minted
|
|
171
|
+
// main scope becomes non-current. The next mintMain() will set the
|
|
172
|
+
// real attemptId for the new attempt.
|
|
173
|
+
return mainAuth.advance(crypto.randomUUID());
|
|
174
|
+
},
|
|
175
|
+
mintMain(): AttemptScope {
|
|
176
|
+
return mintFor("main", mainAuth);
|
|
177
|
+
},
|
|
178
|
+
mintSide(): { scope: AttemptScope; dispose: () => void } {
|
|
179
|
+
const lineage = `side:${crypto.randomUUID()}` as AttemptLineage;
|
|
180
|
+
const auth = createLineageCurrentness(lineage);
|
|
181
|
+
const unregister = this.registerSide(lineage, auth);
|
|
182
|
+
const scope = mintFor(lineage, auth);
|
|
183
|
+
return { scope, dispose: unregister };
|
|
184
|
+
},
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Immutable per-run attempt handle, carried through terminal/finalizer paths.
|
|
190
|
+
* Keyed by logicalRunId in the Agent's `#runHandles` map.
|
|
191
|
+
*/
|
|
192
|
+
export interface AttemptRunHandle {
|
|
193
|
+
readonly logicalRunId: number | import("./types.js").ManagedLogicalRunId;
|
|
194
|
+
readonly scope: AttemptScope;
|
|
195
|
+
}
|