@code-yeongyu/senpi 2026.7.20-2 → 2026.7.20
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 +0 -18
- package/dist/core/agent-session.d.ts +0 -32
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +68 -368
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/compaction/compaction.d.ts +2 -2
- package/dist/core/compaction/compaction.d.ts.map +1 -1
- package/dist/core/compaction/compaction.js +11 -39
- package/dist/core/compaction/compaction.js.map +1 -1
- package/dist/core/extensions/builtin/compaction/index.d.ts.map +1 -1
- package/dist/core/extensions/builtin/compaction/index.js +8 -24
- package/dist/core/extensions/builtin/compaction/index.js.map +1 -1
- package/dist/core/extensions/builtin/goal/command-registration.d.ts +2 -2
- package/dist/core/extensions/builtin/goal/command-registration.d.ts.map +1 -1
- package/dist/core/extensions/builtin/goal/command-registration.js +4 -3
- package/dist/core/extensions/builtin/goal/command-registration.js.map +1 -1
- package/dist/core/extensions/builtin/goal/index.d.ts.map +1 -1
- package/dist/core/extensions/builtin/goal/index.js +27 -11
- package/dist/core/extensions/builtin/goal/index.js.map +1 -1
- package/dist/core/extensions/builtin/goal/tool-registration.d.ts +2 -2
- package/dist/core/extensions/builtin/goal/tool-registration.d.ts.map +1 -1
- package/dist/core/extensions/builtin/goal/tool-registration.js +3 -2
- package/dist/core/extensions/builtin/goal/tool-registration.js.map +1 -1
- package/dist/core/extensions/builtin/index.d.ts.map +1 -1
- package/dist/core/extensions/builtin/index.js +0 -2
- package/dist/core/extensions/builtin/index.js.map +1 -1
- package/dist/core/extensions/builtin/nested-agents-md/index.d.ts.map +1 -1
- package/dist/core/extensions/builtin/nested-agents-md/index.js +1 -4
- package/dist/core/extensions/builtin/nested-agents-md/index.js.map +1 -1
- package/dist/core/extensions/builtin/rules/index.d.ts.map +1 -1
- package/dist/core/extensions/builtin/rules/index.js +1 -4
- package/dist/core/extensions/builtin/rules/index.js.map +1 -1
- package/dist/core/extensions/builtin/websearch/websearch/renderers.d.ts.map +1 -1
- package/dist/core/extensions/builtin/websearch/websearch/renderers.js +4 -13
- package/dist/core/extensions/builtin/websearch/websearch/renderers.js.map +1 -1
- package/dist/core/extensions/builtin/websearch/websearch/search.d.ts +2 -3
- package/dist/core/extensions/builtin/websearch/websearch/search.d.ts.map +1 -1
- package/dist/core/extensions/builtin/websearch/websearch/search.js +1 -2
- package/dist/core/extensions/builtin/websearch/websearch/search.js.map +1 -1
- package/dist/core/extensions/builtin/websearch/websearch/tool.d.ts.map +1 -1
- package/dist/core/extensions/builtin/websearch/websearch/tool.js +1 -17
- package/dist/core/extensions/builtin/websearch/websearch/tool.js.map +1 -1
- package/dist/core/extensions/builtin/websearch/websearch/types.d.ts +0 -2
- package/dist/core/extensions/builtin/websearch/websearch/types.d.ts.map +1 -1
- package/dist/core/extensions/builtin/websearch/websearch/types.js.map +1 -1
- package/dist/core/extensions/runner.d.ts +0 -1
- package/dist/core/extensions/runner.d.ts.map +1 -1
- package/dist/core/extensions/runner.js +0 -31
- package/dist/core/extensions/runner.js.map +1 -1
- package/dist/core/extensions/types.d.ts +10 -67
- package/dist/core/extensions/types.d.ts.map +1 -1
- package/dist/core/extensions/types.js.map +1 -1
- package/dist/core/sdk.d.ts.map +1 -1
- package/dist/core/sdk.js +0 -1
- package/dist/core/sdk.js.map +1 -1
- package/dist/core/session-manager.d.ts +1 -6
- package/dist/core/session-manager.d.ts.map +1 -1
- package/dist/core/session-manager.js +3 -19
- package/dist/core/session-manager.js.map +1 -1
- package/dist/core/settings-manager.d.ts +0 -15
- package/dist/core/settings-manager.d.ts.map +1 -1
- package/dist/core/settings-manager.js +1 -80
- package/dist/core/settings-manager.js.map +1 -1
- package/dist/modes/interactive/components/footer.d.ts +1 -2
- package/dist/modes/interactive/components/footer.d.ts.map +1 -1
- package/dist/modes/interactive/components/footer.js +2 -21
- package/dist/modes/interactive/components/footer.js.map +1 -1
- package/dist/modes/interactive/interactive-mode.d.ts +0 -4
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +3 -78
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/docs/extensions.md +0 -28
- package/docs/settings.md +0 -35
- package/node_modules/@code-yeongyu/senpi-codemode/CHANGELOG.md +0 -12
- package/node_modules/@code-yeongyu/senpi-codemode/package.json +3 -3
- package/node_modules/@earendil-works/pi-agent-core/dist/agent-loop.d.ts.map +1 -1
- package/node_modules/@earendil-works/pi-agent-core/dist/agent-loop.js +8 -76
- package/node_modules/@earendil-works/pi-agent-core/dist/agent-loop.js.map +1 -1
- package/node_modules/@earendil-works/pi-agent-core/dist/agent.d.ts.map +1 -1
- package/node_modules/@earendil-works/pi-agent-core/dist/agent.js +1 -26
- package/node_modules/@earendil-works/pi-agent-core/dist/agent.js.map +1 -1
- package/node_modules/@earendil-works/pi-agent-core/dist/harness/agent-harness.d.ts.map +1 -1
- package/node_modules/@earendil-works/pi-agent-core/dist/harness/agent-harness.js +2 -24
- package/node_modules/@earendil-works/pi-agent-core/dist/harness/agent-harness.js.map +1 -1
- package/node_modules/@earendil-works/pi-agent-core/dist/types.d.ts +0 -5
- package/node_modules/@earendil-works/pi-agent-core/dist/types.d.ts.map +1 -1
- package/node_modules/@earendil-works/pi-agent-core/dist/types.js.map +1 -1
- package/node_modules/@earendil-works/pi-agent-core/package.json +2 -2
- package/node_modules/@earendil-works/pi-ai/dist/api/anthropic-messages.d.ts.map +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/api/anthropic-messages.js +25 -68
- package/node_modules/@earendil-works/pi-ai/dist/api/anthropic-messages.js.map +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/api/transform-messages.d.ts.map +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/api/transform-messages.js +52 -62
- package/node_modules/@earendil-works/pi-ai/dist/api/transform-messages.js.map +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/index.d.ts +0 -1
- package/node_modules/@earendil-works/pi-ai/dist/index.d.ts.map +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/index.js +0 -1
- package/node_modules/@earendil-works/pi-ai/dist/index.js.map +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/models.generated.d.ts +4 -0
- package/node_modules/@earendil-works/pi-ai/dist/models.generated.d.ts.map +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/providers/data/kimi-coding.json +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/providers/data/openrouter.json +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/providers/data/vercel-ai-gateway.json +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/providers/faux.d.ts +0 -1
- package/node_modules/@earendil-works/pi-ai/dist/providers/faux.d.ts.map +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/providers/faux.js +0 -1
- package/node_modules/@earendil-works/pi-ai/dist/providers/faux.js.map +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/providers/kimi-coding.models.d.ts +4 -0
- package/node_modules/@earendil-works/pi-ai/dist/providers/kimi-coding.models.d.ts.map +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/providers/kimi-coding.models.js.map +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/types.d.ts +0 -15
- package/node_modules/@earendil-works/pi-ai/dist/types.d.ts.map +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/types.js.map +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/utils/retry.d.ts.map +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/utils/retry.js +1 -5
- package/node_modules/@earendil-works/pi-ai/dist/utils/retry.js.map +1 -1
- package/node_modules/@earendil-works/pi-ai/package.json +1 -1
- package/node_modules/@earendil-works/pi-pty/package.json +1 -1
- package/node_modules/@earendil-works/pi-tui/package.json +1 -1
- package/package.json +6 -6
- package/dist/core/extensions/builtin/goal/turn-usage.d.ts +0 -20
- package/dist/core/extensions/builtin/goal/turn-usage.d.ts.map +0 -1
- package/dist/core/extensions/builtin/goal/turn-usage.js +0 -61
- package/dist/core/extensions/builtin/goal/turn-usage.js.map +0 -1
- package/dist/core/extensions/builtin/model-fallback/index.d.ts +0 -4
- package/dist/core/extensions/builtin/model-fallback/index.d.ts.map +0 -1
- package/dist/core/extensions/builtin/model-fallback/index.js +0 -55
- package/dist/core/extensions/builtin/model-fallback/index.js.map +0 -1
- package/dist/core/extensions/builtin/model-fallback/settings.d.ts +0 -6
- package/dist/core/extensions/builtin/model-fallback/settings.d.ts.map +0 -1
- package/dist/core/extensions/builtin/model-fallback/settings.js +0 -11
- package/dist/core/extensions/builtin/model-fallback/settings.js.map +0 -1
- package/dist/core/extensions/builtin/model-fallback/ui.d.ts +0 -12
- package/dist/core/extensions/builtin/model-fallback/ui.d.ts.map +0 -1
- package/dist/core/extensions/builtin/model-fallback/ui.js +0 -72
- package/dist/core/extensions/builtin/model-fallback/ui.js.map +0 -1
- package/dist/core/retry-fallback/chains.d.ts +0 -29
- package/dist/core/retry-fallback/chains.d.ts.map +0 -1
- package/dist/core/retry-fallback/chains.js +0 -109
- package/dist/core/retry-fallback/chains.js.map +0 -1
- package/dist/core/retry-fallback/controller.d.ts +0 -77
- package/dist/core/retry-fallback/controller.d.ts.map +0 -1
- package/dist/core/retry-fallback/controller.js +0 -157
- package/dist/core/retry-fallback/controller.js.map +0 -1
- package/dist/core/retry-fallback/cooldown.d.ts +0 -21
- package/dist/core/retry-fallback/cooldown.d.ts.map +0 -1
- package/dist/core/retry-fallback/cooldown.js +0 -49
- package/dist/core/retry-fallback/cooldown.js.map +0 -1
- package/dist/core/retry-fallback/log.d.ts +0 -10
- package/dist/core/retry-fallback/log.d.ts.map +0 -1
- package/dist/core/retry-fallback/log.js +0 -128
- package/dist/core/retry-fallback/log.js.map +0 -1
- package/dist/core/retry-fallback/validate.d.ts +0 -8
- package/dist/core/retry-fallback/validate.d.ts.map +0 -1
- package/dist/core/retry-fallback/validate.js +0 -63
- package/dist/core/retry-fallback/validate.js.map +0 -1
- package/node_modules/@earendil-works/pi-ai/dist/utils/stop-details.d.ts +0 -4
- package/node_modules/@earendil-works/pi-ai/dist/utils/stop-details.d.ts.map +0 -1
- package/node_modules/@earendil-works/pi-ai/dist/utils/stop-details.js +0 -6
- package/node_modules/@earendil-works/pi-ai/dist/utils/stop-details.js.map +0 -1
|
@@ -16,8 +16,7 @@ import { randomUUID } from "node:crypto";
|
|
|
16
16
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
17
17
|
import { basename, dirname } from "node:path";
|
|
18
18
|
import { prepareAgentToolCall } from "@earendil-works/pi-agent-core";
|
|
19
|
-
import { cleanupSessionResources,
|
|
20
|
-
import { getAgentDir } from "../config.js";
|
|
19
|
+
import { cleanupSessionResources, isContextOverflow, isRetryableAssistantError, modelsAreEqual, resetApiProviders, streamSimple, } from "@earendil-works/pi-ai/compat";
|
|
21
20
|
import { getThemeByName, theme } from "../modes/interactive/theme/theme.js";
|
|
22
21
|
import { stripFrontmatter } from "../utils/frontmatter.js";
|
|
23
22
|
import { resolvePath } from "../utils/paths.js";
|
|
@@ -35,10 +34,6 @@ import { filterContextExcludedMessages } from "./messages.js";
|
|
|
35
34
|
import { ModelRegistry } from "./model-registry.js";
|
|
36
35
|
import { getModelNarrowingPatterns, resolveModelScope } from "./model-resolver.js";
|
|
37
36
|
import { expandPromptTemplate } from "./prompt-templates.js";
|
|
38
|
-
import { RetryFallbackController } from "./retry-fallback/controller.js";
|
|
39
|
-
import { SelectorCooldowns } from "./retry-fallback/cooldown.js";
|
|
40
|
-
import { createFallbackLogger } from "./retry-fallback/log.js";
|
|
41
|
-
import { validateFallbackChains } from "./retry-fallback/validate.js";
|
|
42
37
|
import { buildSessionContext, CURRENT_SESSION_VERSION, getLatestCompactionEntry, } from "./session-manager.js";
|
|
43
38
|
import { generateSessionTitle, shouldSkipSessionTitle } from "./session-title-generator.js";
|
|
44
39
|
import { SessionWorkBarrier } from "./session-work-barrier.js";
|
|
@@ -70,39 +65,13 @@ function withoutDeletedHeaders(headers) {
|
|
|
70
65
|
? Object.fromEntries(Object.entries(headers).filter((entry) => entry[1] !== null))
|
|
71
66
|
: undefined;
|
|
72
67
|
}
|
|
73
|
-
/**
|
|
74
|
-
* Human-readable rejection message paired with a `CompactionRejectionCause`.
|
|
75
|
-
*
|
|
76
|
-
* Kept exhaustive over the union so the compiler flags any new cause that would
|
|
77
|
-
* otherwise reintroduce silent failures at the `compaction_end` UI seam.
|
|
78
|
-
*/
|
|
79
|
-
function describeCompactionRejection(cause) {
|
|
80
|
-
switch (cause) {
|
|
81
|
-
case "would-overflow":
|
|
82
|
-
return "Compaction rejected: the produced summary would still overflow the model context window. Reduce context (e.g. /new, drop attachments) or switch to a larger-context model.";
|
|
83
|
-
case "cancelled-by-extension":
|
|
84
|
-
return "Compaction rejected: cancelled by an extension.";
|
|
85
|
-
case "circuit-breaker":
|
|
86
|
-
return "Compaction rejected: the compaction circuit breaker is open after repeated failures. Wait for the cooldown and retry.";
|
|
87
|
-
case "per-turn-cap":
|
|
88
|
-
return "Compaction rejected: per-turn compaction cap reached for this turn.";
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
68
|
class CompactionRejectedError extends Error {
|
|
92
69
|
constructor(rejectionCause) {
|
|
93
|
-
super(rejectionCause === "cancelled-by-extension"
|
|
94
|
-
? "Compaction cancelled"
|
|
95
|
-
: describeCompactionRejection(rejectionCause));
|
|
70
|
+
super(rejectionCause === "cancelled-by-extension" ? "Compaction cancelled" : "Compaction rejected");
|
|
96
71
|
this.name = "CompactionRejectedError";
|
|
97
72
|
this.rejectionCause = rejectionCause;
|
|
98
73
|
}
|
|
99
74
|
}
|
|
100
|
-
class RequiredCompactionError extends Error {
|
|
101
|
-
constructor() {
|
|
102
|
-
super("Context remains above the compaction threshold because compaction did not complete");
|
|
103
|
-
this.name = "RequiredCompactionError";
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
75
|
class MissingModelAccessError extends Error {
|
|
107
76
|
constructor() {
|
|
108
77
|
super("AgentSession requires modelRuntime or modelRegistry");
|
|
@@ -158,7 +127,6 @@ export class AgentSession {
|
|
|
158
127
|
this._retryPromise = undefined;
|
|
159
128
|
this._retryResolve = undefined;
|
|
160
129
|
this._userAbortPromise = undefined;
|
|
161
|
-
this._suppressQueuedContinuationAfterUserAbort = false;
|
|
162
130
|
// Bash execution state
|
|
163
131
|
this._bashAbortController = undefined;
|
|
164
132
|
this._pendingBashMessages = [];
|
|
@@ -190,11 +158,6 @@ export class AgentSession {
|
|
|
190
158
|
this.agent = config.agent;
|
|
191
159
|
this.sessionManager = config.sessionManager;
|
|
192
160
|
this.settingsManager = config.settingsManager;
|
|
193
|
-
const noModelFallback = config.resourceLoader.getExtensions().runtime.flagValues.get("no-model-fallback") === true ||
|
|
194
|
-
process.env.SENPI_NO_FALLBACK === "1";
|
|
195
|
-
if (noModelFallback) {
|
|
196
|
-
this.settingsManager.applyOverrides({ retry: { modelFallback: false } });
|
|
197
|
-
}
|
|
198
161
|
this._scopedModels = config.scopedModels ?? [];
|
|
199
162
|
this._favoriteModels = config.favoriteModels ?? [];
|
|
200
163
|
this._resourceLoader = config.resourceLoader;
|
|
@@ -206,30 +169,6 @@ export class AgentSession {
|
|
|
206
169
|
}
|
|
207
170
|
this._modelRuntime = modelRuntime;
|
|
208
171
|
this._modelRegistry = config.modelRegistry ?? new ModelRegistry(modelRuntime);
|
|
209
|
-
const fallbackLogger = createFallbackLogger(config.agentDir ?? getAgentDir());
|
|
210
|
-
this._fallbackValidationWarnings = validateFallbackChains(this.settingsManager.getRawFallbackChains(), this._modelRegistry);
|
|
211
|
-
for (const warning of this._fallbackValidationWarnings) {
|
|
212
|
-
fallbackLogger.warn("validation_warning", { warning });
|
|
213
|
-
}
|
|
214
|
-
this._retryFallback = new RetryFallbackController({
|
|
215
|
-
getSettings: () => this.settingsManager.getRetryFallbackSettings(),
|
|
216
|
-
registry: this._modelRegistry,
|
|
217
|
-
cooldowns: new SelectorCooldowns(config.fallbackNow ?? (() => Date.now())),
|
|
218
|
-
logger: fallbackLogger,
|
|
219
|
-
switchModel: async (model, thinking, reason) => {
|
|
220
|
-
await this._switchActiveModel(model, {
|
|
221
|
-
persistDefault: false,
|
|
222
|
-
appendSessionEntry: true,
|
|
223
|
-
entryReason: reason,
|
|
224
|
-
emitModelSelect: reason === "fallback-revert",
|
|
225
|
-
invalidateCompaction: false,
|
|
226
|
-
ephemeralThinkingLevel: thinking,
|
|
227
|
-
});
|
|
228
|
-
},
|
|
229
|
-
emit: (event) => this._emit(event),
|
|
230
|
-
getCurrentSelector: () => (this.model ? { model: this.model, thinkingLevel: this.thinkingLevel } : undefined),
|
|
231
|
-
isAuthAvailable: (provider) => this._modelRuntime.hasConfiguredAuth(provider),
|
|
232
|
-
});
|
|
233
172
|
this._extensionRunnerRef = config.extensionRunnerRef;
|
|
234
173
|
this._initialActiveToolNames = config.initialActiveToolNames;
|
|
235
174
|
this._allowedToolNames = config.allowedToolNames ? new Set(config.allowedToolNames) : undefined;
|
|
@@ -370,27 +309,8 @@ export class AgentSession {
|
|
|
370
309
|
? async (_turn, signal) => await this.agent.prepareNextTurn?.(signal)
|
|
371
310
|
: undefined);
|
|
372
311
|
this.agent.prepareNextTurnWithContext = async (turn, signal) => {
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
await this._agentEventQueue;
|
|
376
|
-
const compacted = await this._checkCompaction(turn.message, true, "threshold");
|
|
377
|
-
if (compacted) {
|
|
378
|
-
messages = this.agent.state.messages.slice();
|
|
379
|
-
}
|
|
380
|
-
else {
|
|
381
|
-
const settings = this.settingsManager.getCompactionSettings();
|
|
382
|
-
const contextTokens = estimateContextTokens(filterContextExcludedMessages(this.sessionManager.buildSessionContext().messages)).tokens;
|
|
383
|
-
if (settings.enabled && this.model && shouldCompact(contextTokens, this.model.contextWindow, settings)) {
|
|
384
|
-
throw new RequiredCompactionError();
|
|
385
|
-
}
|
|
386
|
-
}
|
|
387
|
-
}
|
|
388
|
-
const postCompactionTurn = {
|
|
389
|
-
...turn,
|
|
390
|
-
context: { ...turn.context, messages },
|
|
391
|
-
};
|
|
392
|
-
const previousSnapshot = await previousPrepareNextTurnWithContext?.(postCompactionTurn, signal);
|
|
393
|
-
const previousContext = previousSnapshot?.context ?? postCompactionTurn.context;
|
|
312
|
+
const previousSnapshot = await previousPrepareNextTurnWithContext?.(turn, signal);
|
|
313
|
+
const previousContext = previousSnapshot?.context ?? turn.context;
|
|
394
314
|
return {
|
|
395
315
|
...previousSnapshot,
|
|
396
316
|
context: {
|
|
@@ -498,8 +418,7 @@ export class AgentSession {
|
|
|
498
418
|
return;
|
|
499
419
|
}
|
|
500
420
|
const lastAssistant = this._findLastAssistantInMessages(event.messages);
|
|
501
|
-
if (!lastAssistant ||
|
|
502
|
-
(!this._isRetryableError(lastAssistant) && !this._isHardErrorFallbackEligible(lastAssistant))) {
|
|
421
|
+
if (!lastAssistant || !this._isRetryableError(lastAssistant)) {
|
|
503
422
|
return;
|
|
504
423
|
}
|
|
505
424
|
this._retryPromise = new Promise((resolve) => {
|
|
@@ -549,21 +468,11 @@ export class AgentSession {
|
|
|
549
468
|
return false;
|
|
550
469
|
}
|
|
551
470
|
const lastAssistant = this._lastAssistantMessage ?? this._findLastAssistantInMessages(messages);
|
|
552
|
-
if (!lastAssistant) {
|
|
553
|
-
return false;
|
|
554
|
-
}
|
|
555
|
-
const retryableError = this._isRetryableError(lastAssistant);
|
|
556
|
-
if (!retryableError && this._isHardErrorFallbackEligible(lastAssistant)) {
|
|
557
|
-
return true;
|
|
558
|
-
}
|
|
559
|
-
if (!retryableError) {
|
|
471
|
+
if (!lastAssistant || !this._isRetryableError(lastAssistant)) {
|
|
560
472
|
return false;
|
|
561
473
|
}
|
|
562
|
-
if (isClassifierRefusal(lastAssistant)) {
|
|
563
|
-
return this._retryAttempt + 1 <= settings.maxRetries && this._retryFallback.canTryFallback();
|
|
564
|
-
}
|
|
565
474
|
if (this._retryAttempt + 1 > settings.maxRetries) {
|
|
566
|
-
return
|
|
475
|
+
return false;
|
|
567
476
|
}
|
|
568
477
|
const errorMessage = lastAssistant.errorMessage || "Unknown error";
|
|
569
478
|
const providerDelayMs = this._getProviderRetryDelayMs(errorMessage);
|
|
@@ -577,7 +486,6 @@ export class AgentSession {
|
|
|
577
486
|
// This ensures the UI sees the updated queue state
|
|
578
487
|
if (event.type === "message_start" && event.message.role === "user") {
|
|
579
488
|
this._overflowRecoveryAttempted = false;
|
|
580
|
-
this._retryFallback.resetTurn();
|
|
581
489
|
const messageText = this._getUserMessageText(event.message);
|
|
582
490
|
if (messageText) {
|
|
583
491
|
// Check steering queue first
|
|
@@ -626,14 +534,6 @@ export class AgentSession {
|
|
|
626
534
|
// Reset retry counter immediately on successful assistant response
|
|
627
535
|
// This prevents accumulation across multiple LLM calls within a turn
|
|
628
536
|
if (assistantMsg.stopReason !== "error" && this._retryAttempt > 0) {
|
|
629
|
-
const fallback = this._retryFallback.activeState;
|
|
630
|
-
if (fallback) {
|
|
631
|
-
this._emit({
|
|
632
|
-
type: "retry_fallback_succeeded",
|
|
633
|
-
model: this.model ? `${this.model.provider}/${this.model.id}` : "",
|
|
634
|
-
chainKey: fallback.chainKey,
|
|
635
|
-
});
|
|
636
|
-
}
|
|
637
537
|
this._emit({
|
|
638
538
|
type: "auto_retry_end",
|
|
639
539
|
success: true,
|
|
@@ -645,27 +545,16 @@ export class AgentSession {
|
|
|
645
545
|
}
|
|
646
546
|
// Check auto-retry and auto-compaction after agent completes
|
|
647
547
|
let launchedContinuation = false;
|
|
648
|
-
const
|
|
649
|
-
if (userAbortSuppressedQueuedContinuation) {
|
|
650
|
-
this._suppressQueuedContinuationAfterUserAbort = false;
|
|
651
|
-
}
|
|
652
|
-
const allowsQueuedContinuation = event.type === "agent_end" && !userAbortSuppressedQueuedContinuation
|
|
653
|
-
? this._agentEndAllowsQueuedContinuation(event.messages)
|
|
654
|
-
: false;
|
|
548
|
+
const allowsQueuedContinuation = event.type === "agent_end" ? this._agentEndAllowsQueuedContinuation(event.messages) : false;
|
|
655
549
|
if (event.type === "agent_end" && this._lastAssistantMessage) {
|
|
656
550
|
const msg = this._lastAssistantMessage;
|
|
657
551
|
this._lastAssistantMessage = undefined;
|
|
658
|
-
//
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
}
|
|
664
|
-
else if (this._isHardErrorFallbackEligible(msg)) {
|
|
665
|
-
didRetry = await this._handleRetryableError(msg, { hardErrorFallback: true });
|
|
552
|
+
// Check for retryable errors first (overloaded, rate limit, server errors)
|
|
553
|
+
if (this._isRetryableError(msg)) {
|
|
554
|
+
const didRetry = await this._handleRetryableError(msg);
|
|
555
|
+
if (didRetry)
|
|
556
|
+
return; // Retry was initiated, don't proceed to compaction
|
|
666
557
|
}
|
|
667
|
-
if (didRetry)
|
|
668
|
-
return; // Retry was initiated, don't proceed to compaction
|
|
669
558
|
this._resolveRetry();
|
|
670
559
|
launchedContinuation = await this._checkCompaction(msg);
|
|
671
560
|
}
|
|
@@ -709,10 +598,6 @@ export class AgentSession {
|
|
|
709
598
|
}
|
|
710
599
|
return undefined;
|
|
711
600
|
}
|
|
712
|
-
_isAssistantFromBeforeLatestCompaction(assistantMessage) {
|
|
713
|
-
const compactionEntry = getLatestCompactionEntry(this.sessionManager.getBranch());
|
|
714
|
-
return compactionEntry !== null && assistantMessage.timestamp <= new Date(compactionEntry.timestamp).getTime();
|
|
715
|
-
}
|
|
716
601
|
_replaceMessageInPlace(target, replacement) {
|
|
717
602
|
// Agent-core stores the finalized message object in its state before emitting message_end.
|
|
718
603
|
// SessionManager persistence happens later in _processAgentEvent() with event.message.
|
|
@@ -1168,10 +1053,6 @@ export class AgentSession {
|
|
|
1168
1053
|
await this._waitForSettledSessionWork();
|
|
1169
1054
|
throwIfCancelled();
|
|
1170
1055
|
}
|
|
1171
|
-
// Turn boundary: restore the primary model if the fallback cooldown expired.
|
|
1172
|
-
if (!this.isStreaming) {
|
|
1173
|
-
await this._maybeRestoreFallbackPrimary();
|
|
1174
|
-
}
|
|
1175
1056
|
const expandPromptTemplates = options?.expandPromptTemplates ?? true;
|
|
1176
1057
|
const preflightResult = options?.preflightResult;
|
|
1177
1058
|
const promptDisposition = options?.promptDisposition;
|
|
@@ -1249,14 +1130,7 @@ export class AgentSession {
|
|
|
1249
1130
|
// The user's new prompt is sent below, so do not call agent.continue() here.
|
|
1250
1131
|
const lastAssistant = this._findLastAssistantMessage();
|
|
1251
1132
|
if (lastAssistant) {
|
|
1252
|
-
|
|
1253
|
-
if (!compacted && !this._isAssistantFromBeforeLatestCompaction(lastAssistant)) {
|
|
1254
|
-
const settings = this.settingsManager.getCompactionSettings();
|
|
1255
|
-
const contextTokens = estimateContextTokens(filterContextExcludedMessages(this.sessionManager.buildSessionContext().messages)).tokens;
|
|
1256
|
-
if (settings.enabled && this.model && shouldCompact(contextTokens, this.model.contextWindow, settings)) {
|
|
1257
|
-
throw new RequiredCompactionError();
|
|
1258
|
-
}
|
|
1259
|
-
}
|
|
1133
|
+
await this._checkCompaction(lastAssistant, false, "pre_prompt");
|
|
1260
1134
|
}
|
|
1261
1135
|
// Build messages array (custom message if any, then user message)
|
|
1262
1136
|
messages = [];
|
|
@@ -1651,9 +1525,6 @@ export class AgentSession {
|
|
|
1651
1525
|
await this._userAbortPromise;
|
|
1652
1526
|
return;
|
|
1653
1527
|
}
|
|
1654
|
-
if (this.isStreaming) {
|
|
1655
|
-
this._suppressQueuedContinuationAfterUserAbort = true;
|
|
1656
|
-
}
|
|
1657
1528
|
const abortPromise = (async () => {
|
|
1658
1529
|
this.agent.abort();
|
|
1659
1530
|
await this.waitForIdle();
|
|
@@ -1721,57 +1592,20 @@ export class AgentSession {
|
|
|
1721
1592
|
if (!(await this._modelRuntime.checkAuth(model.provider))) {
|
|
1722
1593
|
throw new Error(`No API key for ${model.provider}/${model.id}`);
|
|
1723
1594
|
}
|
|
1724
|
-
// A manual model change abandons any active fallback window; if a fallback
|
|
1725
|
-
// retry sleep is still pending, cancel it so no surprise continuation fires.
|
|
1726
|
-
const hadActiveFallback = this._retryFallback.activeState !== undefined;
|
|
1727
|
-
this._retryFallback.clearForManualModelChange(model);
|
|
1728
|
-
if (hadActiveFallback && this._retryAbortController) {
|
|
1729
|
-
this.abortRetry();
|
|
1730
|
-
}
|
|
1731
|
-
return await this._switchActiveModel(model, {
|
|
1732
|
-
persistDefault: true,
|
|
1733
|
-
appendSessionEntry: true,
|
|
1734
|
-
emitModelSelect: true,
|
|
1735
|
-
invalidateCompaction: true,
|
|
1736
|
-
});
|
|
1737
|
-
}
|
|
1738
|
-
async _maybeRestoreFallbackPrimary() {
|
|
1739
|
-
try {
|
|
1740
|
-
await this._retryFallback.maybeRestorePrimary(this.settingsManager.getRetryFallbackSettings().revertPolicy);
|
|
1741
|
-
}
|
|
1742
|
-
catch (error) {
|
|
1743
|
-
this._retryFallback.clear();
|
|
1744
|
-
console.error("fallback revert failed; cleared fallback state", error);
|
|
1745
|
-
}
|
|
1746
|
-
}
|
|
1747
|
-
async _switchActiveModel(model, opts) {
|
|
1748
1595
|
const previousModel = this.model;
|
|
1749
|
-
|
|
1596
|
+
const invalidatesCompaction = this._modelSelectionChangesContext(previousModel, model);
|
|
1597
|
+
if (invalidatesCompaction) {
|
|
1750
1598
|
this._invalidateCompactionForModelSelection();
|
|
1751
1599
|
}
|
|
1752
|
-
const thinkingLevel = this._getThinkingLevelForModelSwitch(
|
|
1600
|
+
const thinkingLevel = this._getThinkingLevelForModelSwitch();
|
|
1753
1601
|
this.agent.state.model = model;
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
}
|
|
1757
|
-
if (opts.persistDefault) {
|
|
1758
|
-
this.settingsManager.setDefaultModelAndProvider(model.provider, model.id);
|
|
1759
|
-
}
|
|
1602
|
+
this.sessionManager.appendModelChange(model.provider, model.id);
|
|
1603
|
+
this.settingsManager.setDefaultModelAndProvider(model.provider, model.id);
|
|
1760
1604
|
const scopedMatch = this._scopedModels.find((sm) => modelsAreEqual(sm.model, model));
|
|
1761
1605
|
this._currentServiceTier = this._resolveServiceTier(model, scopedMatch?.serviceTier);
|
|
1762
|
-
|
|
1763
|
-
this._applyEphemeralThinkingLevel(thinkingLevel);
|
|
1764
|
-
}
|
|
1765
|
-
else {
|
|
1766
|
-
this.setThinkingLevel(thinkingLevel);
|
|
1767
|
-
}
|
|
1768
|
-
if (!opts.emitModelSelect)
|
|
1769
|
-
return undefined;
|
|
1606
|
+
this.setThinkingLevel(thinkingLevel);
|
|
1770
1607
|
return await this._emitModelSelect(model, previousModel, "set");
|
|
1771
1608
|
}
|
|
1772
|
-
_applyEphemeralThinkingLevel(level) {
|
|
1773
|
-
this.agent.state.thinkingLevel = level;
|
|
1774
|
-
}
|
|
1775
1609
|
/**
|
|
1776
1610
|
* Cycle to next/previous model.
|
|
1777
1611
|
* Uses favorite models, constrained by any global model narrowing.
|
|
@@ -1803,7 +1637,6 @@ export class AgentSession {
|
|
|
1803
1637
|
if (invalidatesCompaction) {
|
|
1804
1638
|
this._invalidateCompactionForModelSelection();
|
|
1805
1639
|
}
|
|
1806
|
-
this._retryFallback.clearForManualModelChange(next.model);
|
|
1807
1640
|
const thinkingLevel = this._getThinkingLevelForModelSwitch(next.thinkingLevel);
|
|
1808
1641
|
this.agent.state.model = next.model;
|
|
1809
1642
|
this.sessionManager.appendModelChange(next.model.provider, next.model.id);
|
|
@@ -1835,9 +1668,6 @@ export class AgentSession {
|
|
|
1835
1668
|
// Only persist if actually changing
|
|
1836
1669
|
const previousLevel = this.agent.state.thinkingLevel;
|
|
1837
1670
|
const isChanging = effectiveLevel !== previousLevel;
|
|
1838
|
-
if (isChanging) {
|
|
1839
|
-
this._retryFallback.noteManualThinkingLevel();
|
|
1840
|
-
}
|
|
1841
1671
|
this.agent.state.thinkingLevel = effectiveLevel;
|
|
1842
1672
|
if (isChanging) {
|
|
1843
1673
|
this.sessionManager.appendThinkingLevelChange(effectiveLevel);
|
|
@@ -2089,7 +1919,7 @@ export class AgentSession {
|
|
|
2089
1919
|
signal,
|
|
2090
1920
|
}));
|
|
2091
1921
|
if (extensionResult?.cancel) {
|
|
2092
|
-
return await this._rejectCompaction(request, requestId,
|
|
1922
|
+
return await this._rejectCompaction(request, requestId, "cancelled-by-extension", true);
|
|
2093
1923
|
}
|
|
2094
1924
|
if (extensionResult?.compaction) {
|
|
2095
1925
|
compactionResult = extensionResult.compaction;
|
|
@@ -2098,7 +1928,7 @@ export class AgentSession {
|
|
|
2098
1928
|
}
|
|
2099
1929
|
if (!compactionResult) {
|
|
2100
1930
|
const { apiKey, headers, extraBody, env } = await this._getCompactionRequestAuth(this.model);
|
|
2101
|
-
compactionResult = await compact(preparation, this.model, apiKey, headers, request.customInstructions, signal, extraBody, this.thinkingLevel, this.agent.streamFn, env
|
|
1931
|
+
compactionResult = await compact(preparation, this.model, apiKey, headers, request.customInstructions, signal, extraBody, this.thinkingLevel, this.agent.streamFn, env);
|
|
2102
1932
|
}
|
|
2103
1933
|
}
|
|
2104
1934
|
if (signal.aborted) {
|
|
@@ -2161,22 +1991,11 @@ export class AgentSession {
|
|
|
2161
1991
|
fromHook: fromExtension,
|
|
2162
1992
|
};
|
|
2163
1993
|
const simulatedMessages = buildSessionContext([...pathEntries, simulatedCompactionEntry], simulatedCompactionEntry.id).messages;
|
|
2164
|
-
const contextTokens =
|
|
1994
|
+
const contextTokens = estimateContextTokens(filterContextExcludedMessages(simulatedMessages)).tokens;
|
|
2165
1995
|
const settings = this.settingsManager.getCompactionSettings();
|
|
2166
1996
|
return contextTokens > this.model.contextWindow - settings.reserveTokens;
|
|
2167
1997
|
}
|
|
2168
|
-
async _rejectCompaction(request, requestId, rejectionCause, aborted
|
|
2169
|
-
// Per plan Section 1: rejection must never be silent. The compaction_end event
|
|
2170
|
-
// carries a non-empty human-readable errorMessage (unless the user aborted, where
|
|
2171
|
-
// the aborted branch already renders "Compaction cancelled"). session_compact is
|
|
2172
|
-
// also emitted with accepted:false so the compaction extension's circuit-breaker
|
|
2173
|
-
// bookkeeping stops being dead code; other builtin session_compact handlers guard
|
|
2174
|
-
// on event.accepted.
|
|
2175
|
-
const trimmedExtensionReason = extensionReason?.trim();
|
|
2176
|
-
const detailedMessage = trimmedExtensionReason
|
|
2177
|
-
? `Compaction rejected: ${trimmedExtensionReason}`
|
|
2178
|
-
: describeCompactionRejection(rejectionCause);
|
|
2179
|
-
const errorMessage = aborted && !trimmedExtensionReason ? undefined : detailedMessage;
|
|
1998
|
+
async _rejectCompaction(request, requestId, rejectionCause, aborted) {
|
|
2180
1999
|
this._emit({
|
|
2181
2000
|
type: "compaction_end",
|
|
2182
2001
|
reason: request.reason,
|
|
@@ -2186,16 +2005,6 @@ export class AgentSession {
|
|
|
2186
2005
|
requestId,
|
|
2187
2006
|
accepted: false,
|
|
2188
2007
|
rejectionCause,
|
|
2189
|
-
errorMessage,
|
|
2190
|
-
});
|
|
2191
|
-
await this._extensionRunner.emit({
|
|
2192
|
-
type: "session_compact",
|
|
2193
|
-
reason: request.reason,
|
|
2194
|
-
requestId,
|
|
2195
|
-
accepted: false,
|
|
2196
|
-
rejectionCause,
|
|
2197
|
-
fromExtension: false,
|
|
2198
|
-
willRetry: false,
|
|
2199
2008
|
});
|
|
2200
2009
|
return { accepted: false, requestId, rejectionCause };
|
|
2201
2010
|
}
|
|
@@ -2223,7 +2032,7 @@ export class AgentSession {
|
|
|
2223
2032
|
* @param assistantMessage The assistant message to check
|
|
2224
2033
|
* @param skipAbortedCheck If false, include aborted messages (for pre-prompt check). Default: true
|
|
2225
2034
|
*/
|
|
2226
|
-
async _checkCompaction(assistantMessage, skipAbortedCheck = true,
|
|
2035
|
+
async _checkCompaction(assistantMessage, skipAbortedCheck = true, requestReason) {
|
|
2227
2036
|
const settings = this.settingsManager.getCompactionSettings();
|
|
2228
2037
|
if (!settings.enabled)
|
|
2229
2038
|
return false;
|
|
@@ -2241,7 +2050,8 @@ export class AgentSession {
|
|
|
2241
2050
|
// compaction boundary. This prevents a stale pre-compaction usage/error
|
|
2242
2051
|
// from retriggering compaction on the first prompt after compaction.
|
|
2243
2052
|
const compactionEntry = getLatestCompactionEntry(this.sessionManager.getBranch());
|
|
2244
|
-
|
|
2053
|
+
const assistantIsFromBeforeCompaction = compactionEntry !== null && assistantMessage.timestamp <= new Date(compactionEntry.timestamp).getTime();
|
|
2054
|
+
if (assistantIsFromBeforeCompaction) {
|
|
2245
2055
|
return false;
|
|
2246
2056
|
}
|
|
2247
2057
|
// Case 1: Overflow - LLM returned context overflow error.
|
|
@@ -2266,7 +2076,7 @@ export class AgentSession {
|
|
|
2266
2076
|
willRetry: false,
|
|
2267
2077
|
errorMessage,
|
|
2268
2078
|
});
|
|
2269
|
-
if (
|
|
2079
|
+
if (requestReason === "pre_prompt") {
|
|
2270
2080
|
throw new Error(errorMessage);
|
|
2271
2081
|
}
|
|
2272
2082
|
return false;
|
|
@@ -2279,47 +2089,42 @@ export class AgentSession {
|
|
|
2279
2089
|
this.agent.state.messages = messages.slice(0, -1);
|
|
2280
2090
|
this._incrementMessageRevision();
|
|
2281
2091
|
}
|
|
2282
|
-
if (
|
|
2283
|
-
|
|
2092
|
+
if (requestReason) {
|
|
2093
|
+
await this._runPrePromptCompaction(assistantMessage, skipAbortedCheck, "overflow", willRetry);
|
|
2284
2094
|
}
|
|
2285
2095
|
else {
|
|
2286
2096
|
return await this._runAutoCompaction("overflow", willRetry);
|
|
2287
2097
|
}
|
|
2098
|
+
return false;
|
|
2288
2099
|
}
|
|
2289
2100
|
// Case 2: Threshold - context is getting large
|
|
2290
2101
|
// For error messages or all-zero usage messages, estimate from the last valid response.
|
|
2291
2102
|
// This ensures sessions that hit persistent API errors (e.g. 529) or malformed zero-usage
|
|
2292
2103
|
// responses can still compact and do not reset context accounting.
|
|
2293
2104
|
let contextTokens;
|
|
2294
|
-
|
|
2295
|
-
|
|
2296
|
-
|
|
2105
|
+
const directContextTokens = assistantMessage.usage ? calculateContextTokens(assistantMessage.usage) : 0;
|
|
2106
|
+
if (assistantMessage.stopReason === "error" || directContextTokens === 0) {
|
|
2107
|
+
const messages = filterContextExcludedMessages(this.agent.state.messages);
|
|
2108
|
+
const estimate = estimateContextTokens(messages);
|
|
2109
|
+
if (estimate.lastUsageIndex === null)
|
|
2110
|
+
return false; // No usage data at all
|
|
2111
|
+
// Verify the usage source is post-compaction. Kept pre-compaction messages
|
|
2112
|
+
// have stale usage reflecting the old (larger) context and would falsely
|
|
2113
|
+
// trigger compaction right after one just finished.
|
|
2114
|
+
const usageMsg = messages[estimate.lastUsageIndex];
|
|
2115
|
+
if (compactionEntry &&
|
|
2116
|
+
usageMsg.role === "assistant" &&
|
|
2117
|
+
usageMsg.timestamp <= new Date(compactionEntry.timestamp).getTime()) {
|
|
2118
|
+
return false;
|
|
2119
|
+
}
|
|
2120
|
+
contextTokens = estimate.tokens;
|
|
2297
2121
|
}
|
|
2298
2122
|
else {
|
|
2299
|
-
|
|
2300
|
-
if (assistantMessage.stopReason !== "error" && directContextTokens !== 0) {
|
|
2301
|
-
contextTokens = directContextTokens;
|
|
2302
|
-
}
|
|
2303
|
-
else {
|
|
2304
|
-
const messages = filterContextExcludedMessages(this.agent.state.messages);
|
|
2305
|
-
const estimate = estimateContextTokens(messages);
|
|
2306
|
-
if (estimate.lastUsageIndex === null)
|
|
2307
|
-
return false; // No usage data at all
|
|
2308
|
-
// Verify the usage source is post-compaction. Kept pre-compaction messages
|
|
2309
|
-
// have stale usage reflecting the old (larger) context and would falsely
|
|
2310
|
-
// trigger compaction right after one just finished.
|
|
2311
|
-
const usageMsg = messages[estimate.lastUsageIndex];
|
|
2312
|
-
if (compactionEntry &&
|
|
2313
|
-
usageMsg.role === "assistant" &&
|
|
2314
|
-
usageMsg.timestamp <= new Date(compactionEntry.timestamp).getTime()) {
|
|
2315
|
-
return false;
|
|
2316
|
-
}
|
|
2317
|
-
contextTokens = estimate.tokens;
|
|
2318
|
-
}
|
|
2123
|
+
contextTokens = directContextTokens;
|
|
2319
2124
|
}
|
|
2320
2125
|
if (shouldCompact(contextTokens, contextWindow, settings)) {
|
|
2321
|
-
if (
|
|
2322
|
-
|
|
2126
|
+
if (requestReason) {
|
|
2127
|
+
await this._runPrePromptCompaction(assistantMessage, skipAbortedCheck);
|
|
2323
2128
|
}
|
|
2324
2129
|
else {
|
|
2325
2130
|
return await this._runAutoCompaction("threshold", false);
|
|
@@ -2340,7 +2145,6 @@ export class AgentSession {
|
|
|
2340
2145
|
if (!execution.accepted && isContextOverflow(lastAssistantMessage, this.model?.contextWindow ?? 0)) {
|
|
2341
2146
|
this._overflowRecoveryAttempted = false;
|
|
2342
2147
|
}
|
|
2343
|
-
return execution.accepted;
|
|
2344
2148
|
}
|
|
2345
2149
|
catch (error) {
|
|
2346
2150
|
if (isContextOverflow(lastAssistantMessage, this.model?.contextWindow ?? 0)) {
|
|
@@ -2356,7 +2160,6 @@ export class AgentSession {
|
|
|
2356
2160
|
willRetry,
|
|
2357
2161
|
errorMessage: aborted ? undefined : `Pre-prompt compaction failed: ${errorMessage}`,
|
|
2358
2162
|
});
|
|
2359
|
-
return false;
|
|
2360
2163
|
}
|
|
2361
2164
|
finally {
|
|
2362
2165
|
this._compactionAbortController = undefined;
|
|
@@ -2679,37 +2482,6 @@ export class AgentSession {
|
|
|
2679
2482
|
},
|
|
2680
2483
|
getContextUsage: () => this.getContextUsage(),
|
|
2681
2484
|
getCompactionSettings: () => this.settingsManager.getCompactionSettings(),
|
|
2682
|
-
sessionSettings: {
|
|
2683
|
-
getRetryFallbackSettings: () => this.settingsManager.getRetryFallbackSettings(),
|
|
2684
|
-
setFallbackChain: async (key, entries) => {
|
|
2685
|
-
this.settingsManager.setFallbackChain(key, [...entries]);
|
|
2686
|
-
await this.settingsManager.flush();
|
|
2687
|
-
},
|
|
2688
|
-
removeFallbackChain: async (key) => {
|
|
2689
|
-
this.settingsManager.removeFallbackChain(key);
|
|
2690
|
-
await this.settingsManager.flush();
|
|
2691
|
-
},
|
|
2692
|
-
setModelFallbackEnabled: async (enabled) => {
|
|
2693
|
-
this.settingsManager.setModelFallbackEnabled(enabled);
|
|
2694
|
-
await this.settingsManager.flush();
|
|
2695
|
-
},
|
|
2696
|
-
setFallbackRevertPolicy: async (policy) => {
|
|
2697
|
-
this.settingsManager.setFallbackRevertPolicy(policy);
|
|
2698
|
-
await this.settingsManager.flush();
|
|
2699
|
-
},
|
|
2700
|
-
reload: () => this.settingsManager.reload(),
|
|
2701
|
-
getFallbackStatus: () => {
|
|
2702
|
-
const active = this._retryFallback.activeState;
|
|
2703
|
-
if (!active)
|
|
2704
|
-
return undefined;
|
|
2705
|
-
return {
|
|
2706
|
-
active: true,
|
|
2707
|
-
currentModel: this.model ? `${this.model.provider}/${this.model.id}` : undefined,
|
|
2708
|
-
originalSelector: active.originalSelector,
|
|
2709
|
-
pinned: active.pinned,
|
|
2710
|
-
};
|
|
2711
|
-
},
|
|
2712
|
-
},
|
|
2713
2485
|
compact: (options) => {
|
|
2714
2486
|
void (async () => {
|
|
2715
2487
|
this._disconnectFromAgent();
|
|
@@ -2781,10 +2553,6 @@ export class AgentSession {
|
|
|
2781
2553
|
},
|
|
2782
2554
|
});
|
|
2783
2555
|
}
|
|
2784
|
-
/** Fallback-chain configuration warnings calculated when this session started. */
|
|
2785
|
-
get fallbackValidationWarnings() {
|
|
2786
|
-
return this._fallbackValidationWarnings;
|
|
2787
|
-
}
|
|
2788
2556
|
_refreshToolRegistry(options) {
|
|
2789
2557
|
const previousRegistryNames = new Set(this._toolRegistry.keys());
|
|
2790
2558
|
const previousActiveToolNames = this.getActiveToolNames();
|
|
@@ -2932,25 +2700,16 @@ export class AgentSession {
|
|
|
2932
2700
|
* Context overflow errors are NOT retryable (handled by compaction instead).
|
|
2933
2701
|
*/
|
|
2934
2702
|
_isRetryableError(message) {
|
|
2703
|
+
if (!message.errorMessage)
|
|
2704
|
+
return false;
|
|
2935
2705
|
// Context overflow is handled by compaction, not retry.
|
|
2936
2706
|
if (isContextOverflow(message, this.model?.contextWindow ?? 0))
|
|
2937
2707
|
return false;
|
|
2938
|
-
if (isClassifierRefusal(message))
|
|
2939
|
-
return true;
|
|
2940
|
-
if (!message.errorMessage)
|
|
2941
|
-
return false;
|
|
2942
2708
|
if (message.stopReason === "aborted") {
|
|
2943
2709
|
return /timed? out|timeout/i.test(message.errorMessage);
|
|
2944
2710
|
}
|
|
2945
2711
|
return isRetryableAssistantError(message);
|
|
2946
2712
|
}
|
|
2947
|
-
_isHardErrorFallbackEligible(message) {
|
|
2948
|
-
return (message.stopReason === "error" &&
|
|
2949
|
-
!isContextOverflow(message, this.model?.contextWindow ?? 0) &&
|
|
2950
|
-
!isClassifierRefusal(message) &&
|
|
2951
|
-
!message.content.some((content) => content.type === "toolCall") &&
|
|
2952
|
-
this._retryFallback.canTryFallback());
|
|
2953
|
-
}
|
|
2954
2713
|
_getProviderRetryDelayMs(errorMessage) {
|
|
2955
2714
|
const retryAfterMsMatch = errorMessage.match(/\bretry[-_ ]?after[-_ ]?ms\s*[:=]\s*(\d+(?:\.\d+)?)/i);
|
|
2956
2715
|
if (retryAfterMsMatch) {
|
|
@@ -2983,7 +2742,7 @@ export class AgentSession {
|
|
|
2983
2742
|
* Handle retryable errors with exponential backoff.
|
|
2984
2743
|
* @returns true if retry was initiated, false if max retries exceeded or disabled
|
|
2985
2744
|
*/
|
|
2986
|
-
async _handleRetryableError(message
|
|
2745
|
+
async _handleRetryableError(message) {
|
|
2987
2746
|
const settings = this.settingsManager.getRetrySettings();
|
|
2988
2747
|
if (!settings.enabled) {
|
|
2989
2748
|
this._resolveRetry();
|
|
@@ -2996,71 +2755,21 @@ export class AgentSession {
|
|
|
2996
2755
|
this._retryResolve = resolve;
|
|
2997
2756
|
});
|
|
2998
2757
|
}
|
|
2999
|
-
|
|
3000
|
-
|
|
3001
|
-
|
|
3002
|
-
|
|
3003
|
-
|
|
3004
|
-
|
|
3005
|
-
|
|
3006
|
-
|
|
3007
|
-
|
|
3008
|
-
|
|
3009
|
-
|
|
3010
|
-
|
|
3011
|
-
this._retryAttempt = 1;
|
|
3012
|
-
}
|
|
3013
|
-
else if (isRefusal) {
|
|
3014
|
-
// Refusals are only retried through a new chain candidate. They never use
|
|
3015
|
-
// same-model retries or the transient over-budget fallback escape hatch.
|
|
3016
|
-
if (this._retryAttempt + 1 > settings.maxRetries) {
|
|
3017
|
-
this._resolveRetry();
|
|
3018
|
-
return false;
|
|
3019
|
-
}
|
|
3020
|
-
switchedFallback = await this._retryFallback.tryFallback("refusal", {});
|
|
3021
|
-
if (!switchedFallback) {
|
|
3022
|
-
const exhaustedChainKey = this._retryFallback.exhaustedChainKey;
|
|
3023
|
-
if (exhaustedChainKey) {
|
|
3024
|
-
this._emit({ type: "retry_fallback_exhausted", chainKey: exhaustedChainKey, lastError: errorMessage });
|
|
3025
|
-
}
|
|
3026
|
-
this._resolveRetry();
|
|
3027
|
-
return false;
|
|
3028
|
-
}
|
|
3029
|
-
this._retryAttempt++;
|
|
3030
|
-
}
|
|
3031
|
-
else {
|
|
3032
|
-
this._retryAttempt++;
|
|
3033
|
-
if (this._retryAttempt > settings.maxRetries) {
|
|
3034
|
-
switchedFallback = await this._retryFallback.tryFallback("transient", {
|
|
3035
|
-
errorMessage,
|
|
3036
|
-
retryAfterMs: this._getProviderRetryDelayMs(errorMessage),
|
|
3037
|
-
});
|
|
3038
|
-
if (switchedFallback) {
|
|
3039
|
-
// The new model receives a fresh retry budget; the failed model does not.
|
|
3040
|
-
this._retryAttempt = 1;
|
|
3041
|
-
}
|
|
3042
|
-
else {
|
|
3043
|
-
const exhaustedChainKey = this._retryFallback.exhaustedChainKey;
|
|
3044
|
-
if (exhaustedChainKey) {
|
|
3045
|
-
this._emit({
|
|
3046
|
-
type: "retry_fallback_exhausted",
|
|
3047
|
-
chainKey: exhaustedChainKey,
|
|
3048
|
-
lastError: errorMessage,
|
|
3049
|
-
});
|
|
3050
|
-
}
|
|
3051
|
-
this._emit({
|
|
3052
|
-
type: "auto_retry_end",
|
|
3053
|
-
success: false,
|
|
3054
|
-
attempt: this._retryAttempt - 1,
|
|
3055
|
-
finalError: message.errorMessage,
|
|
3056
|
-
});
|
|
3057
|
-
this._retryAttempt = 0;
|
|
3058
|
-
this._resolveRetry();
|
|
3059
|
-
return false;
|
|
3060
|
-
}
|
|
3061
|
-
}
|
|
2758
|
+
this._retryAttempt++;
|
|
2759
|
+
if (this._retryAttempt > settings.maxRetries) {
|
|
2760
|
+
// Max retries exceeded, emit final failure and reset
|
|
2761
|
+
this._emit({
|
|
2762
|
+
type: "auto_retry_end",
|
|
2763
|
+
success: false,
|
|
2764
|
+
attempt: this._retryAttempt - 1,
|
|
2765
|
+
finalError: message.errorMessage,
|
|
2766
|
+
});
|
|
2767
|
+
this._retryAttempt = 0;
|
|
2768
|
+
this._resolveRetry(); // Resolve so waitForRetry() completes
|
|
2769
|
+
return false;
|
|
3062
2770
|
}
|
|
3063
|
-
const
|
|
2771
|
+
const errorMessage = message.errorMessage || "Unknown error";
|
|
2772
|
+
const providerDelayMs = this._getProviderRetryDelayMs(errorMessage);
|
|
3064
2773
|
const maxRetryDelayMs = this.settingsManager.getProviderRetrySettings().maxRetryDelayMs;
|
|
3065
2774
|
if (providerDelayMs !== undefined && providerDelayMs > maxRetryDelayMs) {
|
|
3066
2775
|
this._emit({
|
|
@@ -3073,13 +2782,7 @@ export class AgentSession {
|
|
|
3073
2782
|
this._resolveRetry();
|
|
3074
2783
|
return false;
|
|
3075
2784
|
}
|
|
3076
|
-
|
|
3077
|
-
switchedFallback = await this._retryFallback.tryFallback("transient", {
|
|
3078
|
-
errorMessage,
|
|
3079
|
-
retryAfterMs: providerDelayMs,
|
|
3080
|
-
});
|
|
3081
|
-
}
|
|
3082
|
-
const delayMs = switchedFallback ? 0 : (providerDelayMs ?? settings.baseDelayMs * 2 ** (this._retryAttempt - 1));
|
|
2785
|
+
const delayMs = providerDelayMs ?? settings.baseDelayMs * 2 ** (this._retryAttempt - 1);
|
|
3083
2786
|
// Prepare before auto_retry_start so an immediate Esc can cancel the retry sleep.
|
|
3084
2787
|
this._retryAbortController = new AbortController();
|
|
3085
2788
|
this._emit({
|
|
@@ -3115,9 +2818,6 @@ export class AgentSession {
|
|
|
3115
2818
|
return false;
|
|
3116
2819
|
}
|
|
3117
2820
|
this._retryAbortController = undefined;
|
|
3118
|
-
// Turn boundary: a suppression that expired during the backoff sleep lets
|
|
3119
|
-
// the retry continue on the restored primary instead of the fallback model.
|
|
3120
|
-
await this._maybeRestoreFallbackPrimary();
|
|
3121
2821
|
// Retry via continue() - use setTimeout to break out of event handler chain
|
|
3122
2822
|
setTimeout(() => {
|
|
3123
2823
|
this.agent.continue().catch(() => {
|