@gajae-code/agent-core 0.10.1 → 0.11.0
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 +35 -3
- package/dist/types/compaction/entries.d.ts +11 -1
- package/dist/types/types.d.ts +93 -3
- package/package.json +4 -4
- package/src/agent-loop.ts +299 -17
- package/src/agent.ts +227 -22
- package/src/compaction/entries.ts +12 -0
- package/src/compaction/openai.ts +16 -5
- package/src/types.ts +103 -3
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":
|
|
@@ -830,7 +863,7 @@ export class Agent {
|
|
|
830
863
|
this.#contextRevision++;
|
|
831
864
|
}
|
|
832
865
|
|
|
833
|
-
setModel(m: Model) {
|
|
866
|
+
setModel(m: Model | undefined) {
|
|
834
867
|
this.#state.model = m;
|
|
835
868
|
this.#contextRevision++;
|
|
836
869
|
}
|
|
@@ -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;
|
|
@@ -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
|
@@ -24,6 +24,8 @@ import type { AssistantMessage, Message, Model } from "@gajae-code/ai/types";
|
|
|
24
24
|
import {
|
|
25
25
|
getOpenAIResponsesHistoryItems,
|
|
26
26
|
getOpenAIResponsesHistoryPayload,
|
|
27
|
+
neutralizeReservedControlTokens,
|
|
28
|
+
neutralizeResponsesInputControlTokens,
|
|
27
29
|
normalizeResponsesToolCallId,
|
|
28
30
|
} from "@gajae-code/ai/utils";
|
|
29
31
|
import { $env, logger } from "@gajae-code/utils";
|
|
@@ -479,10 +481,12 @@ export async function requestOpenAiRemoteCompaction(
|
|
|
479
481
|
const endpoint = resolveOpenAiCompactEndpoint(model, options?.authCredentialType);
|
|
480
482
|
const request: OpenAiRemoteCompactionRequest = {
|
|
481
483
|
model: model.id,
|
|
482
|
-
input:
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
484
|
+
input: neutralizeResponsesInputControlTokens(
|
|
485
|
+
trimOpenAiCompactInput(
|
|
486
|
+
compactInput,
|
|
487
|
+
resolveOpenAiCompactInputBudget(model.contextWindow, model.maxTokens),
|
|
488
|
+
instructions,
|
|
489
|
+
),
|
|
486
490
|
),
|
|
487
491
|
instructions,
|
|
488
492
|
};
|
|
@@ -553,10 +557,17 @@ export async function requestRemoteCompaction(
|
|
|
553
557
|
request: RemoteCompactionRequest,
|
|
554
558
|
signal?: AbortSignal,
|
|
555
559
|
): Promise<RemoteCompactionResponse> {
|
|
560
|
+
// The prompt embeds the serialized transcript, which can carry leaked Harmony
|
|
561
|
+
// control-token markers (e.g. `<|channel|>analysis`) from model output; a
|
|
562
|
+
// gpt-5.6-backed summarization endpoint rejects those with `Request blocked`.
|
|
563
|
+
const sanitizedRequest: RemoteCompactionRequest = {
|
|
564
|
+
systemPrompt: neutralizeReservedControlTokens(request.systemPrompt),
|
|
565
|
+
prompt: neutralizeReservedControlTokens(request.prompt),
|
|
566
|
+
};
|
|
556
567
|
const response = await fetch(endpoint, {
|
|
557
568
|
method: "POST",
|
|
558
569
|
headers: { "content-type": "application/json" },
|
|
559
|
-
body: JSON.stringify(
|
|
570
|
+
body: JSON.stringify(sanitizedRequest),
|
|
560
571
|
signal,
|
|
561
572
|
});
|
|
562
573
|
|
package/src/types.ts
CHANGED
|
@@ -25,11 +25,82 @@ export type StreamFn = (
|
|
|
25
25
|
...args: Parameters<typeof streamSimple>
|
|
26
26
|
) => AssistantMessageEventStream | Promise<AssistantMessageEventStream>;
|
|
27
27
|
|
|
28
|
+
/** Stable identifier for a managed logical run, shared by all of its retry attempts. */
|
|
29
|
+
export type ManagedLogicalRunId = number;
|
|
30
|
+
|
|
31
|
+
/** Terminal completion requested for a logical run. */
|
|
32
|
+
export interface RunTerminalRequest {
|
|
33
|
+
stopReason: "cancelled" | "error" | "exhausted";
|
|
34
|
+
messages?: AgentMessage[];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Ownership token supplied when Agent invokes a retry continuation.
|
|
39
|
+
*
|
|
40
|
+
* A continuation MUST verify `isCurrent()` immediately before starting a
|
|
41
|
+
* follow-up invocation and abandon the retry when it returns false. The token
|
|
42
|
+
* becomes invalid when its originating run is force-aborted or superseded.
|
|
43
|
+
* Coding-agent retry continuations must accept this argument and must not call
|
|
44
|
+
* `agent.continue()` after ownership has been lost.
|
|
45
|
+
*/
|
|
46
|
+
export interface ManagedAttemptContinuationOwnership {
|
|
47
|
+
/** Per-attempt run-loop id; use only for attempt-local ownership checks. */
|
|
48
|
+
readonly runId: number;
|
|
49
|
+
/** Stable managed logical-run id; use for all terminal completion requests. */
|
|
50
|
+
readonly logicalRunId: ManagedLogicalRunId;
|
|
51
|
+
readonly generation: number;
|
|
52
|
+
isCurrent(): boolean;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Runs after a discarded attempt is idle, only while its ownership token remains current. */
|
|
56
|
+
export type ManagedAttemptContinuation = (ownership: ManagedAttemptContinuationOwnership) => void | Promise<void>;
|
|
57
|
+
|
|
58
|
+
/** Decision returned by managed fallback policy for one provisional attempt. */
|
|
59
|
+
export type ManagedAttemptDecision =
|
|
60
|
+
| { type: "retry"; continuation: ManagedAttemptContinuation }
|
|
61
|
+
| { type: "terminal"; terminal: RunTerminalRequest };
|
|
62
|
+
|
|
63
|
+
/** Structured result for one managed upstream invocation. */
|
|
64
|
+
export type ManagedAttemptOutcome =
|
|
65
|
+
| {
|
|
66
|
+
type: "retryable_discarded";
|
|
67
|
+
failure: {
|
|
68
|
+
message: AssistantMessage;
|
|
69
|
+
/** Exact provider transport facts, including retry headers, for fallback policy. */
|
|
70
|
+
transportFailure?: import("@gajae-code/ai").TransportFailureFacts;
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
| { type: "run_terminal"; reason: "cancelled" | "error" | "exhausted" };
|
|
74
|
+
|
|
75
|
+
export type ManagedAttemptOutcomeHandler = (
|
|
76
|
+
outcome: ManagedAttemptOutcome,
|
|
77
|
+
) => ManagedAttemptDecision | Promise<ManagedAttemptDecision>;
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Outcome of a cooperative mid-run context-maintenance checkpoint (see
|
|
81
|
+
* {@link AgentLoopConfig.maintainContext}). Any value other than "not-needed"
|
|
82
|
+
* means the checkpoint mutated (or attempted to mutate) durable context, so the
|
|
83
|
+
* loop ends the current run without the lossy `agent_end` finalization and the
|
|
84
|
+
* maintenance owner resumes the run on the rewritten context.
|
|
85
|
+
*/
|
|
86
|
+
export type MidRunMaintenanceOutcome = "not-needed" | "pruned" | "compacted" | "promoted" | "failed" | "aborted";
|
|
87
|
+
|
|
28
88
|
/**
|
|
29
89
|
* Configuration for the agent loop.
|
|
30
90
|
*/
|
|
31
91
|
export interface AgentLoopConfig extends SimpleStreamOptions {
|
|
32
92
|
model: Model;
|
|
93
|
+
/**
|
|
94
|
+
* Supplies a fresh opaque token at each concrete managed transport invocation.
|
|
95
|
+
* The callback runs at the stream boundary so controller accounting matches
|
|
96
|
+
* upstream request count, including multi-step tool turns.
|
|
97
|
+
*/
|
|
98
|
+
nextFallbackAttempt?: (model: Model) => SimpleStreamOptions["fallbackAttempt"];
|
|
99
|
+
/** Called after a managed upstream request is accepted and committed. */
|
|
100
|
+
onManagedAttemptAccepted?: () => void | Promise<void>;
|
|
101
|
+
|
|
102
|
+
/** Receives a managed invocation outcome without publishing provisional lifecycle events. */
|
|
103
|
+
onManagedAttemptOutcome?: ManagedAttemptOutcomeHandler;
|
|
33
104
|
|
|
34
105
|
/**
|
|
35
106
|
* When to interrupt tool execution for steering messages.
|
|
@@ -161,6 +232,33 @@ export interface AgentLoopConfig extends SimpleStreamOptions {
|
|
|
161
232
|
*/
|
|
162
233
|
syncContextBeforeModelCall?: (context: AgentContext) => void | Promise<void>;
|
|
163
234
|
|
|
235
|
+
/**
|
|
236
|
+
* Cooperative mid-run context-maintenance checkpoint.
|
|
237
|
+
*
|
|
238
|
+
* Invoked at the top of every loop iteration AFTER pending tool-result /
|
|
239
|
+
* steering messages have been materialized into durable context and BEFORE
|
|
240
|
+
* {@link syncContextBeforeModelCall} and the model call. This is the only
|
|
241
|
+
* boundary where the full unsent context (tool results + dequeued steering)
|
|
242
|
+
* is already durable, so a long uninterrupted tool loop can be bounded here
|
|
243
|
+
* before it grows past the provider window.
|
|
244
|
+
*
|
|
245
|
+
* The callback owns the maintenance decision (prune / compact / promote) and
|
|
246
|
+
* receives the minimal cancellation-aware lifecycle: `signal` is the
|
|
247
|
+
* non-optional loop signal, and `awaitEventDrain(invocationSignal)` waits for
|
|
248
|
+
* prior event consumer bodies with loop and invocation cancellation composed.
|
|
249
|
+
* Any outcome other than "not-needed" ends the current run with
|
|
250
|
+
* `agent_end.stopReason === "maintenance"` (NOT the lossy pause / completed
|
|
251
|
+
* finalization); the callback's continuation owner resumes the run on the
|
|
252
|
+
* rewritten context.
|
|
253
|
+
*/
|
|
254
|
+
maintainContext?: (
|
|
255
|
+
context: AgentContext,
|
|
256
|
+
lifecycle: {
|
|
257
|
+
signal: AbortSignal;
|
|
258
|
+
awaitEventDrain: (invocationSignal: AbortSignal) => Promise<void>;
|
|
259
|
+
},
|
|
260
|
+
) => Promise<MidRunMaintenanceOutcome> | MidRunMaintenanceOutcome;
|
|
261
|
+
|
|
164
262
|
/**
|
|
165
263
|
* Optional transform applied to tool call arguments before execution.
|
|
166
264
|
* Use for deobfuscating secrets or rewriting arguments.
|
|
@@ -357,7 +455,7 @@ export type AgentMessage = Message | CustomAgentMessages[keyof CustomAgentMessag
|
|
|
357
455
|
*/
|
|
358
456
|
export interface AgentState {
|
|
359
457
|
systemPrompt: string[];
|
|
360
|
-
model: Model;
|
|
458
|
+
model: Model | undefined;
|
|
361
459
|
thinkingLevel?: Effort;
|
|
362
460
|
tools: AgentTool<any>[];
|
|
363
461
|
messages: AgentMessage[]; // Can include attachments + custom message types
|
|
@@ -470,8 +568,10 @@ export type AgentEvent =
|
|
|
470
568
|
| {
|
|
471
569
|
type: "agent_end";
|
|
472
570
|
messages: AgentMessage[];
|
|
473
|
-
/** Indicates whether the loop ended normally
|
|
474
|
-
stopReason?: "completed" | "paused";
|
|
571
|
+
/** Indicates whether the loop ended normally, suspended, cancelled, or entered maintenance. */
|
|
572
|
+
stopReason?: "completed" | "paused" | "cancelled" | "maintenance";
|
|
573
|
+
/** Present iff `stopReason === "maintenance"`; the maintenance outcome. */
|
|
574
|
+
maintenanceOutcome?: MidRunMaintenanceOutcome;
|
|
475
575
|
/** Present iff `AgentTelemetryConfig` was supplied on this run. */
|
|
476
576
|
telemetry?: AgentRunSummary;
|
|
477
577
|
coverage?: AgentRunCoverage;
|