@gajae-code/agent-core 0.10.2 → 0.11.1
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 +10 -0
- package/dist/types/agent-loop.d.ts +10 -2
- package/dist/types/agent.d.ts +34 -2
- package/dist/types/compaction/compaction.d.ts +15 -0
- package/dist/types/compaction/entries.d.ts +11 -1
- package/dist/types/types.d.ts +92 -2
- package/package.json +4 -4
- package/src/agent-loop.ts +359 -17
- package/src/agent.ts +226 -21
- package/src/compaction/compaction.ts +34 -3
- package/src/compaction/entries.ts +12 -0
- package/src/compaction/openai.ts +8 -25
- package/src/types.ts +102 -2
package/src/agent.ts
CHANGED
|
@@ -20,6 +20,7 @@ import {
|
|
|
20
20
|
type ToolChoice,
|
|
21
21
|
type ToolResultMessage,
|
|
22
22
|
} from "@gajae-code/ai";
|
|
23
|
+
import { extractHttpStatusFromError } from "@gajae-code/utils";
|
|
23
24
|
import { agentLoop, agentLoopContinue } from "./agent-loop";
|
|
24
25
|
import type { AppendOnlyContextManager } from "./append-only-context";
|
|
25
26
|
import type { HarmonyAuditEvent } from "./harmony-leak";
|
|
@@ -32,6 +33,12 @@ import type {
|
|
|
32
33
|
AgentState,
|
|
33
34
|
AgentTool,
|
|
34
35
|
AgentToolContext,
|
|
36
|
+
ManagedAttemptContinuation,
|
|
37
|
+
ManagedAttemptContinuationOwnership,
|
|
38
|
+
ManagedAttemptDecision,
|
|
39
|
+
ManagedAttemptOutcome,
|
|
40
|
+
ManagedLogicalRunId,
|
|
41
|
+
RunTerminalRequest,
|
|
35
42
|
StreamFn,
|
|
36
43
|
ToolCallContext,
|
|
37
44
|
} from "./types";
|
|
@@ -88,6 +95,13 @@ function refreshToolChoiceForActiveTools(
|
|
|
88
95
|
return tools.some(tool => tool.name === toolName) ? toolChoice : undefined;
|
|
89
96
|
}
|
|
90
97
|
|
|
98
|
+
export class ManagedCursorInvariantError extends Error {
|
|
99
|
+
constructor(message: string = "Managed Cursor attempt received a provider-side tool result") {
|
|
100
|
+
super(message);
|
|
101
|
+
this.name = "ManagedCursorInvariantError";
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
91
105
|
export class AgentBusyError extends Error {
|
|
92
106
|
constructor(
|
|
93
107
|
message: string = "Agent is already processing. Use steer() or followUp() to queue messages, or wait for completion.",
|
|
@@ -276,6 +290,16 @@ export interface AgentOptions {
|
|
|
276
290
|
|
|
277
291
|
export interface AgentPromptOptions {
|
|
278
292
|
toolChoice?: ToolChoice;
|
|
293
|
+
/** Disable transport replay; fallback accounting is owned by the caller. */
|
|
294
|
+
fallbackManaged?: boolean;
|
|
295
|
+
/** Called synchronously after this invocation claims the agent run, before asynchronous provider work. */
|
|
296
|
+
onRunAccepted?: () => void;
|
|
297
|
+
/** Called once immediately before every managed upstream request. */
|
|
298
|
+
nextFallbackAttempt?: AgentLoopConfig["nextFallbackAttempt"];
|
|
299
|
+
/** Called after a managed upstream request is accepted and committed. */
|
|
300
|
+
onManagedAttemptAccepted?: AgentLoopConfig["onManagedAttemptAccepted"];
|
|
301
|
+
/** Receives a discarded managed attempt without exposing assistant lifecycle events. */
|
|
302
|
+
onManagedAttemptOutcome?: AgentLoopConfig["onManagedAttemptOutcome"];
|
|
279
303
|
}
|
|
280
304
|
|
|
281
305
|
/** Buffered Cursor tool result with text position at time of call */
|
|
@@ -332,6 +356,8 @@ export class Agent {
|
|
|
332
356
|
#resolveRunningPrompt?: () => void;
|
|
333
357
|
#runSequence = 0;
|
|
334
358
|
#activeRunId?: number;
|
|
359
|
+
#continuationGeneration = 0;
|
|
360
|
+
#activeFallbackManaged = false;
|
|
335
361
|
#kimiApiFormat?: "openai" | "anthropic";
|
|
336
362
|
#preferWebsockets?: boolean;
|
|
337
363
|
#transformToolCallArguments?: (args: Record<string, unknown>, toolName: string) => Record<string, unknown>;
|
|
@@ -345,6 +371,7 @@ export class Agent {
|
|
|
345
371
|
#onHarmonyLeak?: (event: HarmonyAuditEvent) => void | Promise<void>;
|
|
346
372
|
#onBeforeYield?: () => Promise<void> | void;
|
|
347
373
|
#shouldPause?: AgentLoopConfig["shouldPause"];
|
|
374
|
+
#maintainContext?: AgentLoopConfig["maintainContext"];
|
|
348
375
|
#telemetry?: AgentLoopConfig["telemetry"];
|
|
349
376
|
#appendOnlyContext?: AppendOnlyContextManager;
|
|
350
377
|
|
|
@@ -354,6 +381,8 @@ export class Agent {
|
|
|
354
381
|
|
|
355
382
|
/** Buffered Cursor tool results with text length at time of call (for correct ordering) */
|
|
356
383
|
#cursorToolResultBuffer: CursorToolResultEntry[] = [];
|
|
384
|
+
#terminalizedLogicalRunIds = new Set<ManagedLogicalRunId>();
|
|
385
|
+
#managedLogicalRunOwner?: ManagedLogicalRunId;
|
|
357
386
|
|
|
358
387
|
streamFn: StreamFn;
|
|
359
388
|
getApiKey?: (provider: string) => Promise<string | undefined> | string | undefined;
|
|
@@ -680,6 +709,10 @@ export class Agent {
|
|
|
680
709
|
this.#shouldPause = fn;
|
|
681
710
|
}
|
|
682
711
|
|
|
712
|
+
setMaintainContext(fn: AgentLoopConfig["maintainContext"] | undefined): void {
|
|
713
|
+
this.#maintainContext = fn;
|
|
714
|
+
}
|
|
715
|
+
|
|
683
716
|
emitExternalEvent(event: AgentEvent) {
|
|
684
717
|
switch (event.type) {
|
|
685
718
|
case "message_start":
|
|
@@ -1088,23 +1121,30 @@ export class Agent {
|
|
|
1088
1121
|
* #runLoop guards every state mutation with a run id.
|
|
1089
1122
|
*/
|
|
1090
1123
|
forceAbort(reason = "Force aborted"): boolean {
|
|
1091
|
-
const
|
|
1124
|
+
const runId = this.#activeRunId;
|
|
1125
|
+
const managedLogicalRunId = this.#managedLogicalRunOwner;
|
|
1126
|
+
const hadActiveRun = runId !== undefined && (this.#runningPrompt !== undefined || this.#state.isStreaming);
|
|
1092
1127
|
if (!hadActiveRun) return false;
|
|
1093
1128
|
|
|
1094
1129
|
this.#abortController?.abort(reason);
|
|
1095
|
-
this.#
|
|
1130
|
+
this.#continuationGeneration++;
|
|
1096
1131
|
this.#state.isStreaming = false;
|
|
1097
1132
|
this.#state.streamMessage = null;
|
|
1098
1133
|
this.#state.pendingToolCalls = new Set<string>();
|
|
1099
1134
|
this.#abortController = undefined;
|
|
1100
1135
|
this.#cursorToolResultBuffer = [];
|
|
1136
|
+
this.#managedLogicalRunOwner = undefined;
|
|
1101
1137
|
|
|
1102
1138
|
const resolve = this.#resolveRunningPrompt;
|
|
1103
1139
|
this.#runningPrompt = undefined;
|
|
1104
1140
|
this.#resolveRunningPrompt = undefined;
|
|
1141
|
+
this.#activeRunId = undefined;
|
|
1105
1142
|
resolve?.();
|
|
1106
|
-
|
|
1107
|
-
|
|
1143
|
+
if (this.#activeFallbackManaged) {
|
|
1144
|
+
this.requestRunTerminal(managedLogicalRunId ?? runId, { stopReason: "cancelled" });
|
|
1145
|
+
} else {
|
|
1146
|
+
this.#finalizeRun(runId, { type: "agent_end", messages: [] });
|
|
1147
|
+
}
|
|
1108
1148
|
return true;
|
|
1109
1149
|
}
|
|
1110
1150
|
|
|
@@ -1112,12 +1152,56 @@ export class Agent {
|
|
|
1112
1152
|
return this.#runningPrompt ?? Promise.resolve();
|
|
1113
1153
|
}
|
|
1114
1154
|
|
|
1155
|
+
/** The active per-attempt run identifier. */
|
|
1156
|
+
get activeRunId(): number | undefined {
|
|
1157
|
+
return this.#activeRunId;
|
|
1158
|
+
}
|
|
1159
|
+
|
|
1160
|
+
/**
|
|
1161
|
+
* Stable identifier for the active managed logical run, shared by every retry
|
|
1162
|
+
* attempt. Pass this value to requestRunTerminal(); never retain activeRunId
|
|
1163
|
+
* for managed terminal completion.
|
|
1164
|
+
*/
|
|
1165
|
+
get currentManagedLogicalRunId(): ManagedLogicalRunId | undefined {
|
|
1166
|
+
return this.#managedLogicalRunOwner;
|
|
1167
|
+
}
|
|
1168
|
+
|
|
1169
|
+
/**
|
|
1170
|
+
* Request terminal completion through the single logical-run keyed finalizer.
|
|
1171
|
+
*
|
|
1172
|
+
* For managed runs, logicalRunId must be currentManagedLogicalRunId from any
|
|
1173
|
+
* attempt in the retry chain. Non-managed runs use their activeRunId. Terminal
|
|
1174
|
+
* requests with messages emit a committed message_start/message_end lifecycle
|
|
1175
|
+
* for each diagnostic before agent_end. Requests without messages (such as
|
|
1176
|
+
* cancellation) emit only agent_end.
|
|
1177
|
+
*/
|
|
1178
|
+
requestRunTerminal(logicalRunId: ManagedLogicalRunId, request: RunTerminalRequest): boolean {
|
|
1179
|
+
if (this.#terminalizedLogicalRunIds.has(logicalRunId)) return false;
|
|
1180
|
+
this.#finalizeRun(
|
|
1181
|
+
logicalRunId,
|
|
1182
|
+
{
|
|
1183
|
+
type: "agent_end",
|
|
1184
|
+
messages: request.messages ?? [],
|
|
1185
|
+
...(request.stopReason === "cancelled" ? { stopReason: "cancelled" as const } : {}),
|
|
1186
|
+
},
|
|
1187
|
+
() => {
|
|
1188
|
+
for (const message of request.messages ?? []) {
|
|
1189
|
+
this.#emit({ type: "message_start", message });
|
|
1190
|
+
this.appendMessage(message);
|
|
1191
|
+
this.#emit({ type: "message_end", message });
|
|
1192
|
+
}
|
|
1193
|
+
},
|
|
1194
|
+
);
|
|
1195
|
+
return true;
|
|
1196
|
+
}
|
|
1197
|
+
|
|
1115
1198
|
reset() {
|
|
1116
1199
|
this.#state.messages = [];
|
|
1117
1200
|
this.#contextRevision++;
|
|
1118
1201
|
this.#state.isStreaming = false;
|
|
1119
1202
|
this.#state.streamMessage = null;
|
|
1120
1203
|
this.#state.pendingToolCalls = new Set<string>();
|
|
1204
|
+
this.#managedLogicalRunOwner = undefined;
|
|
1121
1205
|
this.#state.error = undefined;
|
|
1122
1206
|
this.#steeringQueue = [];
|
|
1123
1207
|
this.#followUpQueue = [];
|
|
@@ -1170,6 +1254,10 @@ export class Agent {
|
|
|
1170
1254
|
}
|
|
1171
1255
|
|
|
1172
1256
|
assertUserImagePlaceholdersHavePayload(msgs);
|
|
1257
|
+
if (this.#managedLogicalRunOwner !== undefined) {
|
|
1258
|
+
this.requestRunTerminal(this.#managedLogicalRunOwner, { stopReason: "cancelled" });
|
|
1259
|
+
this.#managedLogicalRunOwner = undefined;
|
|
1260
|
+
}
|
|
1173
1261
|
|
|
1174
1262
|
await this.#runLoop(msgs, promptOptions);
|
|
1175
1263
|
}
|
|
@@ -1177,7 +1265,7 @@ export class Agent {
|
|
|
1177
1265
|
/**
|
|
1178
1266
|
* Continue from current context (used for retries and resuming queued messages).
|
|
1179
1267
|
*/
|
|
1180
|
-
async continue() {
|
|
1268
|
+
async continue(options?: AgentPromptOptions) {
|
|
1181
1269
|
if (this.#state.isStreaming) {
|
|
1182
1270
|
throw new AgentBusyError();
|
|
1183
1271
|
}
|
|
@@ -1189,13 +1277,13 @@ export class Agent {
|
|
|
1189
1277
|
if (messages[messages.length - 1].role === "assistant") {
|
|
1190
1278
|
const queuedSteering = this.#dequeueSteeringMessages();
|
|
1191
1279
|
if (queuedSteering.length > 0) {
|
|
1192
|
-
await this.#runLoop(queuedSteering, { skipInitialSteeringPoll: true });
|
|
1280
|
+
await this.#runLoop(queuedSteering, { ...options, skipInitialSteeringPoll: true });
|
|
1193
1281
|
return;
|
|
1194
1282
|
}
|
|
1195
1283
|
|
|
1196
1284
|
const queuedFollowUp = this.#dequeueFollowUpMessages();
|
|
1197
1285
|
if (queuedFollowUp.length > 0) {
|
|
1198
|
-
await this.#runLoop(queuedFollowUp);
|
|
1286
|
+
await this.#runLoop(queuedFollowUp, options);
|
|
1199
1287
|
return;
|
|
1200
1288
|
}
|
|
1201
1289
|
|
|
@@ -1206,7 +1294,7 @@ export class Agent {
|
|
|
1206
1294
|
throw new Error("No messages to continue from");
|
|
1207
1295
|
}
|
|
1208
1296
|
|
|
1209
|
-
await this.#runLoop(undefined);
|
|
1297
|
+
await this.#runLoop(undefined, options);
|
|
1210
1298
|
}
|
|
1211
1299
|
|
|
1212
1300
|
/**
|
|
@@ -1225,18 +1313,41 @@ export class Agent {
|
|
|
1225
1313
|
this.#resolveRunningPrompt = resolve;
|
|
1226
1314
|
|
|
1227
1315
|
const runId = ++this.#runSequence;
|
|
1316
|
+
const continuationGeneration = ++this.#continuationGeneration;
|
|
1228
1317
|
this.#activeRunId = runId;
|
|
1229
1318
|
const abortController = new AbortController();
|
|
1230
1319
|
this.#abortController = abortController;
|
|
1231
1320
|
this.#state.isStreaming = true;
|
|
1232
1321
|
this.#state.streamMessage = null;
|
|
1233
1322
|
this.#state.error = undefined;
|
|
1234
|
-
|
|
1235
|
-
|
|
1323
|
+
options?.onRunAccepted?.();
|
|
1324
|
+
|
|
1325
|
+
const fallbackManaged = options?.fallbackManaged === true;
|
|
1326
|
+
const managedLogicalRunOwner = fallbackManaged ? (this.#managedLogicalRunOwner ?? runId) : undefined;
|
|
1327
|
+
const startsManagedLogicalRun = fallbackManaged && this.#managedLogicalRunOwner === undefined;
|
|
1328
|
+
if (startsManagedLogicalRun) {
|
|
1329
|
+
this.#managedLogicalRunOwner = managedLogicalRunOwner;
|
|
1330
|
+
this.#emit({ type: "agent_start" });
|
|
1331
|
+
}
|
|
1332
|
+
if (fallbackManaged && this.#cursorToolResultBuffer.length > 0) {
|
|
1333
|
+
const error = new ManagedCursorInvariantError(
|
|
1334
|
+
"Managed Cursor attempt started with buffered provider-side tool results",
|
|
1335
|
+
);
|
|
1336
|
+
this.#state.isStreaming = false;
|
|
1337
|
+
this.#abortController = undefined;
|
|
1338
|
+
this.#activeRunId = undefined;
|
|
1339
|
+
this.#runningPrompt = undefined;
|
|
1340
|
+
this.#resolveRunningPrompt = undefined;
|
|
1341
|
+
resolve();
|
|
1342
|
+
this.requestRunTerminal(managedLogicalRunOwner ?? runId, { stopReason: "error" });
|
|
1343
|
+
this.#managedLogicalRunOwner = undefined;
|
|
1344
|
+
throw error;
|
|
1345
|
+
}
|
|
1346
|
+
// Each run gets a fresh buffer only after managed stale-state validation.
|
|
1236
1347
|
this.#cursorToolResultBuffer = [];
|
|
1348
|
+
this.#activeFallbackManaged = fallbackManaged;
|
|
1237
1349
|
|
|
1238
1350
|
const reasoning = this.#state.thinkingLevel;
|
|
1239
|
-
|
|
1240
1351
|
const context: AgentContext = {
|
|
1241
1352
|
systemPrompt: this.#state.systemPrompt,
|
|
1242
1353
|
messages: this.#state.messages.slice(),
|
|
@@ -1244,7 +1355,7 @@ export class Agent {
|
|
|
1244
1355
|
};
|
|
1245
1356
|
|
|
1246
1357
|
const cursorOnToolResult =
|
|
1247
|
-
this.#cursorExecHandlers || this.#cursorOnToolResult
|
|
1358
|
+
!fallbackManaged && (this.#cursorExecHandlers || this.#cursorOnToolResult)
|
|
1248
1359
|
? async (message: ToolResultMessage) => {
|
|
1249
1360
|
let finalMessage = message;
|
|
1250
1361
|
if (this.#activeRunId !== runId) {
|
|
@@ -1261,7 +1372,6 @@ export class Agent {
|
|
|
1261
1372
|
}
|
|
1262
1373
|
} catch {}
|
|
1263
1374
|
}
|
|
1264
|
-
// Buffer tool result with current text length for correct ordering later.
|
|
1265
1375
|
// Cursor executes tools server-side during streaming, so the assistant message
|
|
1266
1376
|
// already incorporates results. We buffer here and emit in correct order
|
|
1267
1377
|
// when the assistant message ends.
|
|
@@ -1273,7 +1383,10 @@ export class Agent {
|
|
|
1273
1383
|
|
|
1274
1384
|
const getToolChoice = () =>
|
|
1275
1385
|
this.#getToolChoice?.() ?? refreshToolChoiceForActiveTools(options?.toolChoice, this.#state.tools);
|
|
1276
|
-
const cursorExecHandlers = this.#cursorExecHandlersForRun(runId);
|
|
1386
|
+
const cursorExecHandlers = fallbackManaged ? undefined : this.#cursorExecHandlersForRun(runId);
|
|
1387
|
+
let managedDecision: ManagedAttemptDecision | undefined;
|
|
1388
|
+
let managedOutcome: ManagedAttemptOutcome | undefined;
|
|
1389
|
+
let maintenanceInterrupted = false;
|
|
1277
1390
|
|
|
1278
1391
|
const config: AgentLoopConfig = {
|
|
1279
1392
|
model,
|
|
@@ -1296,6 +1409,21 @@ export class Agent {
|
|
|
1296
1409
|
maxRetryDelayMs: this.#maxRetryDelayMs,
|
|
1297
1410
|
requestMaxRetries: this.#requestMaxRetries,
|
|
1298
1411
|
streamMaxRetries: this.#streamMaxRetries,
|
|
1412
|
+
...(fallbackManaged
|
|
1413
|
+
? {
|
|
1414
|
+
fallbackManaged: true,
|
|
1415
|
+
nextFallbackAttempt: options?.nextFallbackAttempt,
|
|
1416
|
+
onManagedAttemptAccepted: options?.onManagedAttemptAccepted,
|
|
1417
|
+
onManagedAttemptOutcome: async outcome => {
|
|
1418
|
+
managedOutcome = outcome;
|
|
1419
|
+
managedDecision = (await options?.onManagedAttemptOutcome?.(outcome)) ?? {
|
|
1420
|
+
type: "terminal",
|
|
1421
|
+
terminal: { stopReason: outcome.type === "run_terminal" ? outcome.reason : "error" },
|
|
1422
|
+
};
|
|
1423
|
+
return managedDecision;
|
|
1424
|
+
},
|
|
1425
|
+
}
|
|
1426
|
+
: {}),
|
|
1299
1427
|
kimiApiFormat: this.#kimiApiFormat,
|
|
1300
1428
|
preferWebsockets: this.#preferWebsockets,
|
|
1301
1429
|
convertToLlm: this.#convertToLlm,
|
|
@@ -1314,8 +1442,8 @@ export class Agent {
|
|
|
1314
1442
|
context.systemPrompt = this.#state.systemPrompt;
|
|
1315
1443
|
context.tools = this.#state.tools;
|
|
1316
1444
|
},
|
|
1317
|
-
cursorExecHandlers,
|
|
1318
|
-
cursorOnToolResult,
|
|
1445
|
+
...(cursorExecHandlers ? { cursorExecHandlers } : {}),
|
|
1446
|
+
...(cursorOnToolResult ? { cursorOnToolResult } : {}),
|
|
1319
1447
|
transformToolCallArguments: this.#transformToolCallArguments,
|
|
1320
1448
|
intentTracing: this.#intentTracing,
|
|
1321
1449
|
appendOnlyContext: this.#appendOnlyContext,
|
|
@@ -1384,6 +1512,12 @@ export class Agent {
|
|
|
1384
1512
|
if (this.#activeRunId !== runId) return false;
|
|
1385
1513
|
return this.#shouldPause?.() === true;
|
|
1386
1514
|
},
|
|
1515
|
+
maintainContext: this.#maintainContext
|
|
1516
|
+
? async (context, lifecycle) => {
|
|
1517
|
+
if (this.#activeRunId !== runId) return "not-needed";
|
|
1518
|
+
return (await this.#maintainContext?.(context, lifecycle)) ?? "not-needed";
|
|
1519
|
+
}
|
|
1520
|
+
: undefined,
|
|
1387
1521
|
telemetry: this.#telemetry,
|
|
1388
1522
|
};
|
|
1389
1523
|
|
|
@@ -1391,8 +1525,8 @@ export class Agent {
|
|
|
1391
1525
|
|
|
1392
1526
|
try {
|
|
1393
1527
|
const stream = messages
|
|
1394
|
-
? agentLoop(messages, context, config, abortController.signal, this.streamFn)
|
|
1395
|
-
: agentLoopContinue(context, config, abortController.signal, this.streamFn);
|
|
1528
|
+
? agentLoop(messages, context, config, abortController.signal, this.streamFn, !fallbackManaged)
|
|
1529
|
+
: agentLoopContinue(context, config, abortController.signal, this.streamFn, !fallbackManaged);
|
|
1396
1530
|
|
|
1397
1531
|
for await (const event of stream) {
|
|
1398
1532
|
if (this.#activeRunId !== runId) {
|
|
@@ -1412,6 +1546,9 @@ export class Agent {
|
|
|
1412
1546
|
break;
|
|
1413
1547
|
|
|
1414
1548
|
case "message_end":
|
|
1549
|
+
if (fallbackManaged && this.#cursorToolResultBuffer.length > 0) {
|
|
1550
|
+
throw new ManagedCursorInvariantError();
|
|
1551
|
+
}
|
|
1415
1552
|
partial = null;
|
|
1416
1553
|
// Check if this is an assistant message with buffered Cursor tool results.
|
|
1417
1554
|
// If so, split the message to emit tool results at the correct position.
|
|
@@ -1444,9 +1581,18 @@ export class Agent {
|
|
|
1444
1581
|
break;
|
|
1445
1582
|
|
|
1446
1583
|
case "agent_end":
|
|
1584
|
+
if (fallbackManaged && managedOutcome) {
|
|
1585
|
+
continue;
|
|
1586
|
+
}
|
|
1447
1587
|
this.#state.isStreaming = false;
|
|
1448
1588
|
this.#state.streamMessage = null;
|
|
1449
|
-
|
|
1589
|
+
if (event.stopReason === "maintenance") {
|
|
1590
|
+
maintenanceInterrupted = true;
|
|
1591
|
+
this.#emit(event);
|
|
1592
|
+
continue;
|
|
1593
|
+
}
|
|
1594
|
+
this.#finalizeRun(managedLogicalRunOwner ?? runId, event);
|
|
1595
|
+
continue;
|
|
1450
1596
|
}
|
|
1451
1597
|
|
|
1452
1598
|
// Emit to listeners
|
|
@@ -1456,6 +1602,15 @@ export class Agent {
|
|
|
1456
1602
|
if (this.#activeRunId !== runId) {
|
|
1457
1603
|
return;
|
|
1458
1604
|
}
|
|
1605
|
+
if (managedOutcome) {
|
|
1606
|
+
if (managedDecision?.type === "terminal") {
|
|
1607
|
+
this.requestRunTerminal(managedLogicalRunOwner ?? runId, managedDecision.terminal);
|
|
1608
|
+
} else if (managedOutcome.type === "run_terminal") {
|
|
1609
|
+
this.requestRunTerminal(managedLogicalRunOwner ?? runId, { stopReason: managedOutcome.reason });
|
|
1610
|
+
} else if (managedDecision?.type !== "retry") {
|
|
1611
|
+
this.#finalizeRun(managedLogicalRunOwner ?? runId);
|
|
1612
|
+
}
|
|
1613
|
+
}
|
|
1459
1614
|
|
|
1460
1615
|
// Handle any remaining partial message
|
|
1461
1616
|
if (partial && partial.role === "assistant" && Array.isArray(partial.content) && partial.content.length > 0) {
|
|
@@ -1494,23 +1649,59 @@ export class Agent {
|
|
|
1494
1649
|
},
|
|
1495
1650
|
stopReason: abortController.signal.aborted ? "aborted" : "error",
|
|
1496
1651
|
errorMessage: err?.message || String(err),
|
|
1652
|
+
errorStatus: extractHttpStatusFromError({ status: err?.errorStatus }) ?? extractHttpStatusFromError(err),
|
|
1497
1653
|
timestamp: Date.now(),
|
|
1498
1654
|
} as AgentMessage;
|
|
1499
1655
|
|
|
1500
|
-
this.appendMessage(errorMsg);
|
|
1501
1656
|
this.#state.error = err?.message || String(err);
|
|
1502
|
-
this
|
|
1657
|
+
this.requestRunTerminal(managedLogicalRunOwner ?? runId, {
|
|
1658
|
+
stopReason: abortController.signal.aborted ? "cancelled" : "error",
|
|
1659
|
+
messages: [errorMsg],
|
|
1660
|
+
});
|
|
1503
1661
|
} finally {
|
|
1662
|
+
let continuation: ManagedAttemptContinuation | undefined;
|
|
1663
|
+
if (managedOutcome?.type === "retryable_discarded" && managedDecision?.type === "retry") {
|
|
1664
|
+
continuation = managedDecision.continuation;
|
|
1665
|
+
}
|
|
1666
|
+
const ownership: ManagedAttemptContinuationOwnership = {
|
|
1667
|
+
runId,
|
|
1668
|
+
logicalRunId: managedLogicalRunOwner ?? runId,
|
|
1669
|
+
generation: continuationGeneration,
|
|
1670
|
+
isCurrent: () => this.#continuationGeneration === continuationGeneration && this.#activeRunId === undefined,
|
|
1671
|
+
};
|
|
1504
1672
|
if (this.#activeRunId === runId) {
|
|
1505
1673
|
this.#state.isStreaming = false;
|
|
1506
1674
|
this.#state.streamMessage = null;
|
|
1507
1675
|
this.#state.pendingToolCalls = new Set<string>();
|
|
1508
1676
|
this.#abortController = undefined;
|
|
1509
1677
|
this.#activeRunId = undefined;
|
|
1678
|
+
this.#activeFallbackManaged = false;
|
|
1510
1679
|
this.#resolveRunningPrompt?.();
|
|
1511
1680
|
this.#runningPrompt = undefined;
|
|
1512
1681
|
this.#resolveRunningPrompt = undefined;
|
|
1513
1682
|
}
|
|
1683
|
+
if (
|
|
1684
|
+
fallbackManaged &&
|
|
1685
|
+
!continuation &&
|
|
1686
|
+
!maintenanceInterrupted &&
|
|
1687
|
+
this.#managedLogicalRunOwner === managedLogicalRunOwner
|
|
1688
|
+
) {
|
|
1689
|
+
this.#managedLogicalRunOwner = undefined;
|
|
1690
|
+
}
|
|
1691
|
+
if (continuation && ownership.isCurrent()) {
|
|
1692
|
+
try {
|
|
1693
|
+
await continuation(ownership);
|
|
1694
|
+
if (this.#activeRunId === undefined && this.#managedLogicalRunOwner === managedLogicalRunOwner) {
|
|
1695
|
+
this.#managedLogicalRunOwner = undefined;
|
|
1696
|
+
}
|
|
1697
|
+
} catch (err) {
|
|
1698
|
+
if (ownership.isCurrent()) {
|
|
1699
|
+
this.#state.error = err instanceof Error ? err.message : String(err);
|
|
1700
|
+
this.requestRunTerminal(managedLogicalRunOwner ?? runId, { stopReason: "error" });
|
|
1701
|
+
if (this.#managedLogicalRunOwner === managedLogicalRunOwner) this.#managedLogicalRunOwner = undefined;
|
|
1702
|
+
}
|
|
1703
|
+
}
|
|
1704
|
+
}
|
|
1514
1705
|
}
|
|
1515
1706
|
}
|
|
1516
1707
|
|
|
@@ -1521,6 +1712,20 @@ export class Agent {
|
|
|
1521
1712
|
}
|
|
1522
1713
|
|
|
1523
1714
|
/** Calculate total text length from an assistant message's content blocks */
|
|
1715
|
+
#finalizeRun(
|
|
1716
|
+
logicalRunId: ManagedLogicalRunId,
|
|
1717
|
+
event?: Extract<AgentEvent, { type: "agent_end" }>,
|
|
1718
|
+
beforeEvent?: () => void,
|
|
1719
|
+
): void {
|
|
1720
|
+
if (this.#terminalizedLogicalRunIds.has(logicalRunId)) return;
|
|
1721
|
+
this.#terminalizedLogicalRunIds.add(logicalRunId);
|
|
1722
|
+
if (this.#terminalizedLogicalRunIds.size > 256) {
|
|
1723
|
+
this.#terminalizedLogicalRunIds.delete(this.#terminalizedLogicalRunIds.values().next().value!);
|
|
1724
|
+
}
|
|
1725
|
+
beforeEvent?.();
|
|
1726
|
+
if (event) this.#emit(event);
|
|
1727
|
+
}
|
|
1728
|
+
|
|
1524
1729
|
#getAssistantTextLength(message: AgentMessage | null): number {
|
|
1525
1730
|
if (message?.role !== "assistant" || !Array.isArray(message.content)) {
|
|
1526
1731
|
return 0;
|
|
@@ -144,6 +144,18 @@ export interface CompactionSettings {
|
|
|
144
144
|
remoteEndpoint?: string;
|
|
145
145
|
}
|
|
146
146
|
|
|
147
|
+
export type RemoteCompactionFallbackHealthEvent =
|
|
148
|
+
| { kind: "success"; model: string; provider: string }
|
|
149
|
+
| { kind: "fallback"; model: string; provider: string; error: string };
|
|
150
|
+
|
|
151
|
+
export interface RemoteCompactionFallbackHealthHooks {
|
|
152
|
+
recordRemoteCompactionFallback(event: RemoteCompactionFallbackHealthEvent): void;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function isAbortError(error: unknown): boolean {
|
|
156
|
+
return error instanceof Error && error.name === "AbortError";
|
|
157
|
+
}
|
|
158
|
+
|
|
147
159
|
export const DEFAULT_COMPACTION_SETTINGS: CompactionSettings = {
|
|
148
160
|
enabled: true,
|
|
149
161
|
strategy: "context-full",
|
|
@@ -799,6 +811,8 @@ export interface SummaryOptions {
|
|
|
799
811
|
providerSessionState?: Map<string, ProviderSessionState>;
|
|
800
812
|
/** Hint that websocket transport should be preferred when supported by the provider implementation. */
|
|
801
813
|
preferWebsockets?: boolean;
|
|
814
|
+
/** Session-owned health sink for remote-compaction fallback transition logging. */
|
|
815
|
+
remoteCompactionFallbackHealth?: RemoteCompactionFallbackHealthHooks;
|
|
802
816
|
}
|
|
803
817
|
|
|
804
818
|
/**
|
|
@@ -1305,6 +1319,7 @@ export async function compact(
|
|
|
1305
1319
|
sessionId: options?.sessionId,
|
|
1306
1320
|
providerSessionState: options?.providerSessionState,
|
|
1307
1321
|
preferWebsockets: options?.preferWebsockets,
|
|
1322
|
+
remoteCompactionFallbackHealth: options?.remoteCompactionFallbackHealth,
|
|
1308
1323
|
};
|
|
1309
1324
|
|
|
1310
1325
|
let preserveData = withOpenAiRemoteCompactionPreserveData(previousPreserveData, undefined);
|
|
@@ -1331,12 +1346,28 @@ export async function compact(
|
|
|
1331
1346
|
{ authCredentialType: options?.authCredentialType },
|
|
1332
1347
|
);
|
|
1333
1348
|
preserveData = withOpenAiRemoteCompactionPreserveData(previousPreserveData, remote);
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
error: err instanceof Error ? err.message : String(err),
|
|
1349
|
+
summaryOptions.remoteCompactionFallbackHealth?.recordRemoteCompactionFallback({
|
|
1350
|
+
kind: "success",
|
|
1337
1351
|
model: model.id,
|
|
1338
1352
|
provider: model.provider,
|
|
1339
1353
|
});
|
|
1354
|
+
} catch (err) {
|
|
1355
|
+
if (signal?.aborted || isAbortError(err)) throw err;
|
|
1356
|
+
const error = err instanceof Error ? err.message : String(err);
|
|
1357
|
+
if (summaryOptions.remoteCompactionFallbackHealth) {
|
|
1358
|
+
summaryOptions.remoteCompactionFallbackHealth.recordRemoteCompactionFallback({
|
|
1359
|
+
kind: "fallback",
|
|
1360
|
+
error,
|
|
1361
|
+
model: model.id,
|
|
1362
|
+
provider: model.provider,
|
|
1363
|
+
});
|
|
1364
|
+
} else {
|
|
1365
|
+
logger.warn("OpenAI remote compaction failed, falling back to local summarization", {
|
|
1366
|
+
error,
|
|
1367
|
+
model: model.id,
|
|
1368
|
+
provider: model.provider,
|
|
1369
|
+
});
|
|
1370
|
+
}
|
|
1340
1371
|
}
|
|
1341
1372
|
}
|
|
1342
1373
|
}
|
|
@@ -115,6 +115,17 @@ export interface ModeChangeEntry extends SessionEntryBase {
|
|
|
115
115
|
data?: Record<string, unknown>;
|
|
116
116
|
}
|
|
117
117
|
|
|
118
|
+
export interface ConfiguredModelChainEntry extends SessionEntryBase {
|
|
119
|
+
type: "configured_model_chain";
|
|
120
|
+
role: string;
|
|
121
|
+
entries: readonly string[];
|
|
122
|
+
origin: string;
|
|
123
|
+
identity?: string;
|
|
124
|
+
explicitHead: boolean;
|
|
125
|
+
/** Whether this entry removes the configured chain for its role. */
|
|
126
|
+
cleared?: boolean;
|
|
127
|
+
}
|
|
128
|
+
|
|
118
129
|
export interface CustomCompactionSessionEntries {}
|
|
119
130
|
|
|
120
131
|
export type SessionEntry =
|
|
@@ -131,6 +142,7 @@ export type SessionEntry =
|
|
|
131
142
|
| MCPToolSelectionEntry
|
|
132
143
|
| SessionInitEntry
|
|
133
144
|
| ModeChangeEntry
|
|
145
|
+
| ConfiguredModelChainEntry
|
|
134
146
|
| CustomCompactionSessionEntries[keyof CustomCompactionSessionEntries];
|
|
135
147
|
|
|
136
148
|
export interface ReadonlySessionManager {
|
package/src/compaction/openai.ts
CHANGED
|
@@ -514,18 +514,14 @@ export async function requestOpenAiRemoteCompaction(
|
|
|
514
514
|
});
|
|
515
515
|
|
|
516
516
|
if (!response.ok) {
|
|
517
|
-
const errorText = await response.text().catch(() => "");
|
|
518
|
-
logger.warn("OpenAI remote compaction failed", {
|
|
519
|
-
endpoint,
|
|
520
|
-
status: response.status,
|
|
521
|
-
statusText: response.statusText,
|
|
522
|
-
errorText,
|
|
523
|
-
});
|
|
524
517
|
throw new Error(`Remote compaction failed (${response.status} ${response.statusText})`);
|
|
525
518
|
}
|
|
526
519
|
|
|
527
|
-
const data = (await response.json()) as { output?: unknown
|
|
528
|
-
|
|
520
|
+
const data = (await response.json()) as { output?: unknown } | undefined;
|
|
521
|
+
if (!Array.isArray(data?.output)) {
|
|
522
|
+
throw new Error(`Remote compaction response malformed output (outputType=${typeof data?.output})`);
|
|
523
|
+
}
|
|
524
|
+
const rawOutput = data.output;
|
|
529
525
|
const replacementHistory = rawOutput.filter(
|
|
530
526
|
(item): item is Record<string, unknown> =>
|
|
531
527
|
!!item && typeof item === "object" && shouldKeepOpenAiCompactOutputItem(item as Record<string, unknown>),
|
|
@@ -539,15 +535,9 @@ export async function requestOpenAiRemoteCompaction(
|
|
|
539
535
|
const outputTypes = rawOutput.map(item =>
|
|
540
536
|
typeof item === "object" && item !== null ? (item as Record<string, unknown>).type : typeof item,
|
|
541
537
|
);
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
provider: model.provider,
|
|
546
|
-
rawOutputLength: rawOutput.length,
|
|
547
|
-
outputTypes,
|
|
548
|
-
replacementHistoryLength: replacementHistory.length,
|
|
549
|
-
});
|
|
550
|
-
throw new Error("Remote compaction response missing compaction item");
|
|
538
|
+
throw new Error(
|
|
539
|
+
`Remote compaction response missing compaction item (rawOutputLength=${rawOutput.length}, outputTypes=${outputTypes.join(",")}, replacementHistoryLength=${replacementHistory.length})`,
|
|
540
|
+
);
|
|
551
541
|
}
|
|
552
542
|
return { provider: model.provider, replacementHistory, compactionItem };
|
|
553
543
|
}
|
|
@@ -572,13 +562,6 @@ export async function requestRemoteCompaction(
|
|
|
572
562
|
});
|
|
573
563
|
|
|
574
564
|
if (!response.ok) {
|
|
575
|
-
const errorText = await response.text().catch(() => "");
|
|
576
|
-
logger.warn("Remote compaction failed", {
|
|
577
|
-
endpoint,
|
|
578
|
-
status: response.status,
|
|
579
|
-
statusText: response.statusText,
|
|
580
|
-
errorText,
|
|
581
|
-
});
|
|
582
565
|
throw new Error(`Remote compaction failed (${response.status} ${response.statusText})`);
|
|
583
566
|
}
|
|
584
567
|
|