@dreb/coding-agent 2.64.3 → 2.64.4
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/README.md +1 -1
- package/dist/core/agent-session.d.ts +34 -0
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +185 -29
- package/dist/core/agent-session.js.map +1 -1
- package/dist/modes/interactive/components/settings-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/settings-selector.js +1 -1
- package/dist/modes/interactive/components/settings-selector.js.map +1 -1
- package/dist/modes/rpc/rpc-types.d.ts +1 -1
- package/dist/modes/rpc/rpc-types.d.ts.map +1 -1
- package/dist/modes/rpc/rpc-types.js.map +1 -1
- package/docs/compaction.md +9 -3
- package/docs/dashboard.md +1 -1
- package/docs/json.md +1 -1
- package/docs/rpc.md +5 -5
- package/docs/settings.md +1 -1
- package/package.json +1 -1
|
@@ -95,6 +95,10 @@ export class AgentSession {
|
|
|
95
95
|
// Compaction state
|
|
96
96
|
_compactionAbortController = undefined;
|
|
97
97
|
_autoCompactionAbortController = undefined;
|
|
98
|
+
/** The in-flight auto-compaction run (any start path), if any. */
|
|
99
|
+
_autoCompactionInFlight = undefined;
|
|
100
|
+
/** Serializes auto-compaction checks so two never run concurrently. */
|
|
101
|
+
_autoCompactionChain = Promise.resolve();
|
|
98
102
|
_overflowRecoveryAttempted = false;
|
|
99
103
|
// Branch summarization state
|
|
100
104
|
_branchSummaryAbortController = undefined;
|
|
@@ -234,6 +238,7 @@ export class AgentSession {
|
|
|
234
238
|
this._unsubscribeAgent = this.agent.subscribe(this._handleAgentEvent);
|
|
235
239
|
this._installAgentToolHooks();
|
|
236
240
|
this._installBackgroundAgentGuardrails();
|
|
241
|
+
this._installMidTurnCompactionHook();
|
|
237
242
|
this._buildRuntime({
|
|
238
243
|
activeToolNames: this._initialActiveToolNames,
|
|
239
244
|
includeAllExtensionTools: true,
|
|
@@ -546,6 +551,64 @@ export class AgentSession {
|
|
|
546
551
|
return true;
|
|
547
552
|
});
|
|
548
553
|
}
|
|
554
|
+
/** Install the settled pre-request hook used for mid-turn compaction. */
|
|
555
|
+
_installMidTurnCompactionHook() {
|
|
556
|
+
this.agent.setBeforeLlmCall((context, signal) => this._prepareForLlmCall(context, signal));
|
|
557
|
+
}
|
|
558
|
+
/**
|
|
559
|
+
* Compact between tool-loop requests without starting a second Agent loop.
|
|
560
|
+
* Session persistence is asynchronous, so wait for prior message events before
|
|
561
|
+
* preparing a summary from the current branch.
|
|
562
|
+
*/
|
|
563
|
+
async _prepareForLlmCall(context, signal) {
|
|
564
|
+
await this._agentEventQueue.catch(() => undefined);
|
|
565
|
+
if (signal?.aborted)
|
|
566
|
+
return undefined;
|
|
567
|
+
const loopTail = context.messages[context.messages.length - 1];
|
|
568
|
+
if (loopTail?.role !== "user" && loopTail?.role !== "toolResult") {
|
|
569
|
+
return undefined;
|
|
570
|
+
}
|
|
571
|
+
const messages = this.agent.state.messages;
|
|
572
|
+
const lastMessage = messages[messages.length - 1];
|
|
573
|
+
if (lastMessage?.role !== "user" && lastMessage?.role !== "toolResult") {
|
|
574
|
+
return undefined;
|
|
575
|
+
}
|
|
576
|
+
const settings = this.settingsManager.getCompactionSettings();
|
|
577
|
+
let contextWindow = this.model?.contextWindow ?? 0;
|
|
578
|
+
const estimate = estimateContextTokens(messages);
|
|
579
|
+
let contextTokens = estimate.tokens;
|
|
580
|
+
// A retained assistant message can still carry pre-compaction usage. For the
|
|
581
|
+
// first request after compaction, estimate the rebuilt context from message
|
|
582
|
+
// content instead of reusing that stale high-water mark.
|
|
583
|
+
const compactionEntry = getLatestCompactionEntry(this.sessionManager.getBranch());
|
|
584
|
+
if (compactionEntry && estimate.lastUsageIndex !== null) {
|
|
585
|
+
const usageMessage = messages[estimate.lastUsageIndex];
|
|
586
|
+
if (usageMessage.role === "assistant" &&
|
|
587
|
+
usageMessage.timestamp <= new Date(compactionEntry.timestamp).getTime()) {
|
|
588
|
+
contextTokens = messages.reduce((total, message) => total + estimateTokens(message), 0);
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
// Preserve K3's tier behavior at the new mid-turn check. If the tier changes,
|
|
592
|
+
// the hook result refreshes the active loop model before its next request.
|
|
593
|
+
if (shouldUpgradeK3Tier(this.model, contextTokens)) {
|
|
594
|
+
const userCompactPoint = K3_256K_CONTEXT_WINDOW - settings.reserveTokens;
|
|
595
|
+
const userThresholdPreempts = settings.enabled && userCompactPoint < K3_UPGRADE_CUTOFF_TOKENS && contextTokens > userCompactPoint;
|
|
596
|
+
if (!userThresholdPreempts) {
|
|
597
|
+
this._tryUpgradeK3ContextTier();
|
|
598
|
+
contextWindow = this.model?.contextWindow ?? contextWindow;
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
if (!shouldCompact(contextTokens, contextWindow, settings)) {
|
|
602
|
+
return this.model ? { model: this.model } : undefined;
|
|
603
|
+
}
|
|
604
|
+
// `willRetry: true` tells event consumers that another request is imminent;
|
|
605
|
+
// `requestWillFollow` prevents a re-entrant agent.continue() call.
|
|
606
|
+
await this._trackAutoCompaction("threshold", true, true);
|
|
607
|
+
return {
|
|
608
|
+
messages: this.agent.state.messages,
|
|
609
|
+
...(this.model ? { model: this.model } : {}),
|
|
610
|
+
};
|
|
611
|
+
}
|
|
549
612
|
/**
|
|
550
613
|
* Reset the background-agent guardrail counter and the pause-notified flag together.
|
|
551
614
|
* These two fields are one logical unit — they must always reset in lockstep so a new
|
|
@@ -1039,8 +1102,9 @@ export class AgentSession {
|
|
|
1039
1102
|
this._unsubscribeGuardrailSentinel = undefined;
|
|
1040
1103
|
this._unsubscribeGuardrailCounter?.();
|
|
1041
1104
|
this._unsubscribeGuardrailCounter = undefined;
|
|
1042
|
-
// Clear
|
|
1105
|
+
// Clear callbacks so the agent doesn't hold a reference to a disconnected session.
|
|
1043
1106
|
this.agent.setShouldContinue(undefined);
|
|
1107
|
+
this.agent.setBeforeLlmCall(undefined);
|
|
1044
1108
|
}
|
|
1045
1109
|
/**
|
|
1046
1110
|
* Reconnect to agent events after _disconnectFromAgent().
|
|
@@ -1051,6 +1115,7 @@ export class AgentSession {
|
|
|
1051
1115
|
return; // Already connected
|
|
1052
1116
|
this._unsubscribeAgent = this.agent.subscribe(this._handleAgentEvent);
|
|
1053
1117
|
this._installBackgroundAgentGuardrails();
|
|
1118
|
+
this._installMidTurnCompactionHook();
|
|
1054
1119
|
}
|
|
1055
1120
|
/**
|
|
1056
1121
|
* Remove all listeners and disconnect from agent.
|
|
@@ -1348,10 +1413,12 @@ export class AgentSession {
|
|
|
1348
1413
|
throw new Error(`No API key found for ${this.model.provider}.\n\n` +
|
|
1349
1414
|
`Use /login or set an API key environment variable. See ${join(getDocsPath(), "providers.md")}`);
|
|
1350
1415
|
}
|
|
1351
|
-
// Check if we need to compact before sending (catches aborted responses)
|
|
1416
|
+
// Check if we need to compact before sending (catches aborted responses).
|
|
1417
|
+
// The prompt below is already the next request, so compaction must not
|
|
1418
|
+
// schedule a competing agent.continue().
|
|
1352
1419
|
const lastAssistant = this._findLastAssistantMessage();
|
|
1353
1420
|
if (lastAssistant) {
|
|
1354
|
-
await this._checkCompaction(lastAssistant, false);
|
|
1421
|
+
await this._checkCompaction(lastAssistant, false, true);
|
|
1355
1422
|
}
|
|
1356
1423
|
// Build messages array (custom message if any, then user message)
|
|
1357
1424
|
const messages = [];
|
|
@@ -2087,10 +2154,43 @@ export class AgentSession {
|
|
|
2087
2154
|
* 1. Overflow: LLM returned context overflow error, remove error message from agent state, compact, auto-retry
|
|
2088
2155
|
* 2. Threshold: Context over threshold, compact, NO auto-retry (user continues manually)
|
|
2089
2156
|
*
|
|
2157
|
+
* Checks run on a serialization chain: a check that arrives while another
|
|
2158
|
+
* check's compaction is still in flight (e.g. a prompt submitted during an
|
|
2159
|
+
* agent_end compaction) waits for it, then evaluates instead of starting a
|
|
2160
|
+
* competing compaction. If an earlier queued check compacted or rebuilt
|
|
2161
|
+
* context first, the captured message is pre-compaction and the
|
|
2162
|
+
* pre-compaction staleness guard inside _doCheckCompaction skips it.
|
|
2163
|
+
*
|
|
2164
|
+
* @param assistantMessage The assistant message to check
|
|
2165
|
+
* @param skipAbortedCheck If false, include aborted messages (for pre-prompt check). Default: true
|
|
2166
|
+
* @param requestWillFollow Whether the caller will issue the next request without agent.continue().
|
|
2167
|
+
*/
|
|
2168
|
+
_checkCompaction(assistantMessage, skipAbortedCheck = true, requestWillFollow = false) {
|
|
2169
|
+
const work = async () => {
|
|
2170
|
+
// A run started outside this chain (the mid-turn hook) may still
|
|
2171
|
+
// be in flight; wait for it before evaluating.
|
|
2172
|
+
if (this._autoCompactionInFlight) {
|
|
2173
|
+
await this._autoCompactionInFlight.catch(() => undefined);
|
|
2174
|
+
}
|
|
2175
|
+
await this._doCheckCompaction(assistantMessage, skipAbortedCheck, requestWillFollow);
|
|
2176
|
+
};
|
|
2177
|
+
const run = this._autoCompactionChain.then(work);
|
|
2178
|
+
this._autoCompactionChain = run.catch(() => undefined);
|
|
2179
|
+
return run;
|
|
2180
|
+
}
|
|
2181
|
+
/**
|
|
2182
|
+
* Evaluate compaction needs and run a tracked compaction if warranted.
|
|
2183
|
+
* Runs inside the _checkCompaction serialization chain.
|
|
2184
|
+
*
|
|
2185
|
+
* Two cases:
|
|
2186
|
+
* 1. Overflow: LLM returned context overflow error, remove error message from agent state, compact, auto-retry
|
|
2187
|
+
* 2. Threshold: Context over threshold, compact, NO auto-retry (user continues manually)
|
|
2188
|
+
*
|
|
2090
2189
|
* @param assistantMessage The assistant message to check
|
|
2091
2190
|
* @param skipAbortedCheck If false, include aborted messages (for pre-prompt check). Default: true
|
|
2191
|
+
* @param requestWillFollow Whether the caller will issue the next request without agent.continue().
|
|
2092
2192
|
*/
|
|
2093
|
-
async
|
|
2193
|
+
async _doCheckCompaction(assistantMessage, skipAbortedCheck = true, requestWillFollow = false) {
|
|
2094
2194
|
const settings = this.settingsManager.getCompactionSettings();
|
|
2095
2195
|
// Skip if message was aborted (user cancelled) - unless skipAbortedCheck is false
|
|
2096
2196
|
if (skipAbortedCheck && assistantMessage.stopReason === "aborted")
|
|
@@ -2119,11 +2219,13 @@ export class AgentSession {
|
|
|
2119
2219
|
// Remove the error message from agent state (it IS saved to session
|
|
2120
2220
|
// for history, but we don't want it in context for the retry)
|
|
2121
2221
|
this._removeLastAssistantMessage();
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
this.
|
|
2125
|
-
|
|
2126
|
-
|
|
2222
|
+
if (!requestWillFollow) {
|
|
2223
|
+
setTimeout(() => {
|
|
2224
|
+
this.agent.continue().catch((err) => {
|
|
2225
|
+
this.warnInSession(`Agent failed to continue after context window upgrade: ${err instanceof Error ? err.message : String(err)}`);
|
|
2226
|
+
});
|
|
2227
|
+
}, 100);
|
|
2228
|
+
}
|
|
2127
2229
|
return;
|
|
2128
2230
|
}
|
|
2129
2231
|
if (!settings.enabled)
|
|
@@ -2133,7 +2235,7 @@ export class AgentSession {
|
|
|
2133
2235
|
type: "auto_compaction_end",
|
|
2134
2236
|
result: undefined,
|
|
2135
2237
|
aborted: false,
|
|
2136
|
-
willRetry:
|
|
2238
|
+
willRetry: requestWillFollow,
|
|
2137
2239
|
errorMessage: "Context overflow recovery failed after one compact-and-retry attempt. Try reducing context or switching to a larger-context model.",
|
|
2138
2240
|
});
|
|
2139
2241
|
return;
|
|
@@ -2142,7 +2244,7 @@ export class AgentSession {
|
|
|
2142
2244
|
// Remove the error message from agent state (it IS saved to session for history,
|
|
2143
2245
|
// but we don't want it in context for the retry)
|
|
2144
2246
|
this._removeLastAssistantMessage();
|
|
2145
|
-
await this.
|
|
2247
|
+
await this._trackAutoCompaction("overflow", true, requestWillFollow);
|
|
2146
2248
|
return;
|
|
2147
2249
|
}
|
|
2148
2250
|
// Case 2: Threshold - context is getting large
|
|
@@ -2188,7 +2290,7 @@ export class AgentSession {
|
|
|
2188
2290
|
if (!settings.enabled)
|
|
2189
2291
|
return;
|
|
2190
2292
|
if (shouldCompact(contextTokens, contextWindow, settings)) {
|
|
2191
|
-
await this.
|
|
2293
|
+
await this._trackAutoCompaction("threshold", false, requestWillFollow);
|
|
2192
2294
|
}
|
|
2193
2295
|
}
|
|
2194
2296
|
/**
|
|
@@ -2227,27 +2329,60 @@ export class AgentSession {
|
|
|
2227
2329
|
});
|
|
2228
2330
|
return true;
|
|
2229
2331
|
}
|
|
2332
|
+
/**
|
|
2333
|
+
* Start an auto-compaction while tracking it, so a concurrent check that
|
|
2334
|
+
* arrives during the run can await it instead of starting a competing one.
|
|
2335
|
+
* The tracking flag clears when this specific run settles (even on error,
|
|
2336
|
+
* since _runAutoCompaction swallows its own failures).
|
|
2337
|
+
*/
|
|
2338
|
+
_trackAutoCompaction(reason, willRetry, requestWillFollow = false) {
|
|
2339
|
+
const run = this._runAutoCompaction(reason, willRetry, requestWillFollow);
|
|
2340
|
+
this._autoCompactionInFlight = run;
|
|
2341
|
+
void run
|
|
2342
|
+
.catch(() => undefined)
|
|
2343
|
+
.finally(() => {
|
|
2344
|
+
if (this._autoCompactionInFlight === run) {
|
|
2345
|
+
this._autoCompactionInFlight = undefined;
|
|
2346
|
+
}
|
|
2347
|
+
});
|
|
2348
|
+
return run;
|
|
2349
|
+
}
|
|
2230
2350
|
/**
|
|
2231
2351
|
* Internal: Run auto-compaction with events.
|
|
2232
2352
|
*/
|
|
2233
|
-
async _runAutoCompaction(reason, willRetry) {
|
|
2353
|
+
async _runAutoCompaction(reason, willRetry, requestWillFollow = false) {
|
|
2234
2354
|
const settings = this.settingsManager.getCompactionSettings();
|
|
2235
2355
|
this._emit({ type: "auto_compaction_start", reason });
|
|
2236
2356
|
this._autoCompactionAbortController = new AbortController();
|
|
2237
2357
|
try {
|
|
2238
2358
|
if (!this.model) {
|
|
2239
|
-
this._emit({
|
|
2359
|
+
this._emit({
|
|
2360
|
+
type: "auto_compaction_end",
|
|
2361
|
+
result: undefined,
|
|
2362
|
+
aborted: false,
|
|
2363
|
+
willRetry: requestWillFollow,
|
|
2364
|
+
});
|
|
2240
2365
|
return;
|
|
2241
2366
|
}
|
|
2242
2367
|
const apiKey = await this._modelRegistry.getApiKey(this.model);
|
|
2243
2368
|
if (!apiKey) {
|
|
2244
|
-
this._emit({
|
|
2369
|
+
this._emit({
|
|
2370
|
+
type: "auto_compaction_end",
|
|
2371
|
+
result: undefined,
|
|
2372
|
+
aborted: false,
|
|
2373
|
+
willRetry: requestWillFollow,
|
|
2374
|
+
});
|
|
2245
2375
|
return;
|
|
2246
2376
|
}
|
|
2247
2377
|
const pathEntries = this.sessionManager.getBranch();
|
|
2248
2378
|
const preparation = prepareCompaction(pathEntries, settings);
|
|
2249
2379
|
if (!preparation) {
|
|
2250
|
-
this._emit({
|
|
2380
|
+
this._emit({
|
|
2381
|
+
type: "auto_compaction_end",
|
|
2382
|
+
result: undefined,
|
|
2383
|
+
aborted: false,
|
|
2384
|
+
willRetry: requestWillFollow,
|
|
2385
|
+
});
|
|
2251
2386
|
return;
|
|
2252
2387
|
}
|
|
2253
2388
|
let extensionCompaction;
|
|
@@ -2261,7 +2396,12 @@ export class AgentSession {
|
|
|
2261
2396
|
signal: this._autoCompactionAbortController.signal,
|
|
2262
2397
|
}));
|
|
2263
2398
|
if (extensionResult?.cancel) {
|
|
2264
|
-
this._emit({
|
|
2399
|
+
this._emit({
|
|
2400
|
+
type: "auto_compaction_end",
|
|
2401
|
+
result: undefined,
|
|
2402
|
+
aborted: true,
|
|
2403
|
+
willRetry: requestWillFollow,
|
|
2404
|
+
});
|
|
2265
2405
|
return;
|
|
2266
2406
|
}
|
|
2267
2407
|
if (extensionResult?.compaction) {
|
|
@@ -2289,7 +2429,12 @@ export class AgentSession {
|
|
|
2289
2429
|
details = compactResult.details;
|
|
2290
2430
|
}
|
|
2291
2431
|
if (this._autoCompactionAbortController.signal.aborted) {
|
|
2292
|
-
this._emit({
|
|
2432
|
+
this._emit({
|
|
2433
|
+
type: "auto_compaction_end",
|
|
2434
|
+
result: undefined,
|
|
2435
|
+
aborted: true,
|
|
2436
|
+
willRetry: requestWillFollow,
|
|
2437
|
+
});
|
|
2293
2438
|
return;
|
|
2294
2439
|
}
|
|
2295
2440
|
this.sessionManager.appendCompaction(summary, firstKeptEntryId, tokensBefore, details, fromExtension);
|
|
@@ -2316,17 +2461,28 @@ export class AgentSession {
|
|
|
2316
2461
|
tokensBefore,
|
|
2317
2462
|
details,
|
|
2318
2463
|
};
|
|
2319
|
-
|
|
2320
|
-
|
|
2321
|
-
|
|
2322
|
-
|
|
2323
|
-
|
|
2324
|
-
|
|
2325
|
-
|
|
2464
|
+
// Rebuilding from persisted entries can reintroduce the assistant error
|
|
2465
|
+
// that triggered compaction. It is historical evidence, not valid retry
|
|
2466
|
+
// context, regardless of how the compaction was classified.
|
|
2467
|
+
let removedTrailingError = false;
|
|
2468
|
+
const rebuiltMessages = this.agent.state.messages;
|
|
2469
|
+
const rebuiltTail = rebuiltMessages[rebuiltMessages.length - 1];
|
|
2470
|
+
if (rebuiltTail?.role === "assistant" && rebuiltTail.stopReason === "error") {
|
|
2471
|
+
this.agent.replaceMessages(rebuiltMessages.slice(0, -1));
|
|
2472
|
+
removedTrailingError = true;
|
|
2326
2473
|
}
|
|
2327
|
-
|
|
2328
|
-
|
|
2329
|
-
const
|
|
2474
|
+
const messages = this.agent.state.messages;
|
|
2475
|
+
const tail = messages[messages.length - 1];
|
|
2476
|
+
const hasQueuedMessages = this.agent.hasQueuedMessages();
|
|
2477
|
+
const hasResumableTail = tail !== undefined && tail.role !== "assistant";
|
|
2478
|
+
const shouldContinue = !requestWillFollow &&
|
|
2479
|
+
messages.length > 0 &&
|
|
2480
|
+
(hasQueuedMessages ||
|
|
2481
|
+
(hasResumableTail && (removedTrailingError || willRetry || settings.continueAfterAutoCompaction)));
|
|
2482
|
+
const nextRequestWillFollow = requestWillFollow || shouldContinue;
|
|
2483
|
+
// Emit after deciding continuation so frontends can attach input queued
|
|
2484
|
+
// during compaction to the imminent request instead of starting a rival run.
|
|
2485
|
+
this._emit({ type: "auto_compaction_end", result, aborted: false, willRetry: nextRequestWillFollow });
|
|
2330
2486
|
if (shouldContinue) {
|
|
2331
2487
|
setTimeout(() => {
|
|
2332
2488
|
this.agent.continue().catch((err) => {
|
|
@@ -2342,7 +2498,7 @@ export class AgentSession {
|
|
|
2342
2498
|
type: "auto_compaction_end",
|
|
2343
2499
|
result: undefined,
|
|
2344
2500
|
aborted: false,
|
|
2345
|
-
willRetry:
|
|
2501
|
+
willRetry: requestWillFollow,
|
|
2346
2502
|
errorMessage: reason === "overflow"
|
|
2347
2503
|
? `Context overflow recovery failed: ${errorMessage}`
|
|
2348
2504
|
: `Auto-compaction failed: ${errorMessage}`,
|