@astralyn/pi 0.84.2 → 0.84.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/CHANGELOG.md +41 -0
- package/dist/cli/args.js +1 -1
- package/dist/cli/args.js.map +1 -1
- package/dist/cli/config-selector.d.ts.map +1 -1
- package/dist/cli/config-selector.js +2 -3
- package/dist/cli/config-selector.js.map +1 -1
- package/dist/cli/project-trust.d.ts.map +1 -1
- package/dist/cli/project-trust.js +1 -4
- package/dist/cli/project-trust.js.map +1 -1
- package/dist/cli/startup-ui.d.ts.map +1 -1
- package/dist/cli/startup-ui.js +3 -5
- package/dist/cli/startup-ui.js.map +1 -1
- package/dist/config.d.ts +5 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +8 -1
- package/dist/config.js.map +1 -1
- package/dist/core/agent-session.d.ts +1 -25
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +29 -408
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/compaction-controller.d.ts +89 -0
- package/dist/core/compaction-controller.d.ts.map +1 -0
- package/dist/core/compaction-controller.js +428 -0
- package/dist/core/compaction-controller.js.map +1 -0
- package/dist/core/extensions/index.d.ts +1 -1
- package/dist/core/extensions/index.d.ts.map +1 -1
- package/dist/core/extensions/index.js.map +1 -1
- package/dist/core/extensions/loader.js +3 -3
- package/dist/core/extensions/loader.js.map +1 -1
- package/dist/core/extensions/types.d.ts +2 -27
- package/dist/core/extensions/types.d.ts.map +1 -1
- package/dist/core/extensions/types.js.map +1 -1
- package/dist/core/provider-attribution.js +2 -2
- package/dist/core/provider-attribution.js.map +1 -1
- package/dist/core/settings-manager.js +1 -1
- package/dist/core/settings-manager.js.map +1 -1
- package/dist/core/telemetry.d.ts +1 -1
- package/dist/core/telemetry.d.ts.map +1 -1
- package/dist/core/telemetry.js +1 -1
- package/dist/core/telemetry.js.map +1 -1
- package/dist/extensions/deepwiki/index.d.ts.map +1 -1
- package/dist/extensions/deepwiki/index.js +23 -1
- package/dist/extensions/deepwiki/index.js.map +1 -1
- package/dist/extensions/deepwiki/render.d.ts +1 -1
- package/dist/extensions/deepwiki/render.d.ts.map +1 -1
- package/dist/extensions/deepwiki/render.js +4 -2
- package/dist/extensions/deepwiki/render.js.map +1 -1
- package/dist/extensions/question/index.d.ts.map +1 -1
- package/dist/extensions/question/index.js +0 -1
- package/dist/extensions/question/index.js.map +1 -1
- package/dist/extensions/subagent/agents-command.d.ts +4 -0
- package/dist/extensions/subagent/agents-command.d.ts.map +1 -0
- package/dist/extensions/subagent/agents-command.js +230 -0
- package/dist/extensions/subagent/agents-command.js.map +1 -0
- package/dist/extensions/subagent/agents.d.ts.map +1 -1
- package/dist/extensions/subagent/agents.js +2 -0
- package/dist/extensions/subagent/agents.js.map +1 -1
- package/dist/extensions/subagent/display-name.d.ts +6 -0
- package/dist/extensions/subagent/display-name.d.ts.map +1 -0
- package/dist/extensions/subagent/display-name.js +13 -0
- package/dist/extensions/subagent/display-name.js.map +1 -0
- package/dist/extensions/subagent/index.d.ts.map +1 -1
- package/dist/extensions/subagent/index.js +19 -126
- package/dist/extensions/subagent/index.js.map +1 -1
- package/dist/extensions/subagent/render.d.ts.map +1 -1
- package/dist/extensions/subagent/render.js +1 -10
- package/dist/extensions/subagent/render.js.map +1 -1
- package/dist/extensions/subagent/settings.d.ts +0 -1
- package/dist/extensions/subagent/settings.d.ts.map +1 -1
- package/dist/extensions/subagent/settings.js +3 -7
- package/dist/extensions/subagent/settings.js.map +1 -1
- package/dist/extensions/subagent/types.d.ts +2 -0
- package/dist/extensions/subagent/types.d.ts.map +1 -1
- package/dist/extensions/subagent/types.js.map +1 -1
- package/dist/extensions/subagent/ui/model-list.d.ts +67 -0
- package/dist/extensions/subagent/ui/model-list.d.ts.map +1 -0
- package/dist/extensions/subagent/ui/model-list.js +242 -0
- package/dist/extensions/subagent/ui/model-list.js.map +1 -0
- package/dist/extensions/subagent/ui/profile-editor.d.ts +54 -0
- package/dist/extensions/subagent/ui/profile-editor.d.ts.map +1 -0
- package/dist/extensions/subagent/ui/profile-editor.js +154 -0
- package/dist/extensions/subagent/ui/profile-editor.js.map +1 -0
- package/dist/extensions/subagent/ui/profile-picker.d.ts +32 -0
- package/dist/extensions/subagent/ui/profile-picker.d.ts.map +1 -0
- package/dist/extensions/subagent/ui/profile-picker.js +149 -0
- package/dist/extensions/subagent/ui/profile-picker.js.map +1 -0
- package/dist/extensions/todo/constants.d.ts +6 -17
- package/dist/extensions/todo/constants.d.ts.map +1 -1
- package/dist/extensions/todo/constants.js +20 -31
- package/dist/extensions/todo/constants.js.map +1 -1
- package/dist/extensions/todo/index.d.ts.map +1 -1
- package/dist/extensions/todo/index.js +36 -57
- package/dist/extensions/todo/index.js.map +1 -1
- package/dist/extensions/todo/schema.d.ts +20 -59
- package/dist/extensions/todo/schema.d.ts.map +1 -1
- package/dist/extensions/todo/schema.js +23 -69
- package/dist/extensions/todo/schema.js.map +1 -1
- package/dist/extensions/todo/state.d.ts +23 -65
- package/dist/extensions/todo/state.d.ts.map +1 -1
- package/dist/extensions/todo/state.js +238 -1262
- package/dist/extensions/todo/state.js.map +1 -1
- package/dist/extensions/todo/view.d.ts +19 -10
- package/dist/extensions/todo/view.d.ts.map +1 -1
- package/dist/extensions/todo/view.js +411 -463
- package/dist/extensions/todo/view.js.map +1 -1
- package/dist/extensions/todo/widget.d.ts +15 -0
- package/dist/extensions/todo/widget.d.ts.map +1 -0
- package/dist/extensions/todo/widget.js +50 -0
- package/dist/extensions/todo/widget.js.map +1 -0
- package/dist/modes/interactive/components/extension-selector.d.ts +1 -4
- package/dist/modes/interactive/components/extension-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/extension-selector.js +6 -12
- package/dist/modes/interactive/components/extension-selector.js.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/interactive/components/tool-chrome.d.ts +28 -0
- package/dist/modes/interactive/components/tool-chrome.d.ts.map +1 -0
- package/dist/modes/interactive/components/tool-chrome.js +81 -0
- package/dist/modes/interactive/components/tool-chrome.js.map +1 -0
- package/dist/modes/interactive/components/tool-execution.d.ts +0 -3
- package/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
- package/dist/modes/interactive/components/tool-execution.js +11 -108
- package/dist/modes/interactive/components/tool-execution.js.map +1 -1
- package/dist/modes/interactive/interactive-mode.d.ts +1 -1
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +35 -52
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
- package/dist/modes/rpc/rpc-mode.js +1 -8
- package/dist/modes/rpc/rpc-mode.js.map +1 -1
- package/docs/bundled/README.md +1 -1
- package/docs/bundled/extensions/subagent.md +20 -5
- package/docs/bundled/extensions/todo.md +84 -24
- package/docs/bundled/tool-presentation.md +3 -3
- package/docs/extensions.md +6 -23
- package/docs/settings.md +1 -1
- package/docs/usage.md +1 -1
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
- package/dist/extensions/subagent/picker.d.ts +0 -31
- package/dist/extensions/subagent/picker.d.ts.map +0 -1
- package/dist/extensions/subagent/picker.js +0 -109
- package/dist/extensions/subagent/picker.js.map +0 -1
- package/dist/extensions/todo/overlay.d.ts +0 -24
- package/dist/extensions/todo/overlay.d.ts.map +0 -1
- package/dist/extensions/todo/overlay.js +0 -135
- package/dist/extensions/todo/overlay.js.map +0 -1
- package/dist/utils/coalescing-terminal.d.ts +0 -132
- package/dist/utils/coalescing-terminal.d.ts.map +0 -1
- package/dist/utils/coalescing-terminal.js +0 -292
- package/dist/utils/coalescing-terminal.js.map +0 -1
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
16
16
|
import { basename, dirname } from "node:path";
|
|
17
17
|
import { contentText } from "@earendil-works/pi-ai";
|
|
18
|
-
import { clampThinkingLevel, cleanupSessionResources, getSupportedThinkingLevels, isContextOverflow,
|
|
18
|
+
import { clampThinkingLevel, cleanupSessionResources, getSupportedThinkingLevels, isContextOverflow, isRetryableAssistantError, modelsAreEqual, resetApiProviders, streamSimple, } from "@earendil-works/pi-ai/compat";
|
|
19
19
|
import { getThemeByName, theme } from "../modes/interactive/theme/theme.js";
|
|
20
20
|
import { stripFrontmatter } from "../utils/frontmatter.js";
|
|
21
21
|
import { resolvePath } from "../utils/paths.js";
|
|
@@ -23,7 +23,8 @@ import { sleep } from "../utils/sleep.js";
|
|
|
23
23
|
import { normalizeToolResultImages } from "../utils/tool-result-images.js";
|
|
24
24
|
import { formatNoApiKeyFoundMessage, formatNoModelSelectedMessage } from "./auth-guidance.js";
|
|
25
25
|
import { executeBashWithOperations } from "./bash-executor.js";
|
|
26
|
-
import { calculateContextTokens, collectEntriesForBranchSummary, compact, estimateContextTokens,
|
|
26
|
+
import { calculateContextTokens, collectEntriesForBranchSummary, compact, estimateContextTokens, generateBranchSummary, prepareCompaction, shouldCompact, } from "./compaction/index.js";
|
|
27
|
+
import { CONTEXT_REMAINS_OVER_COMPACTION_THRESHOLD, CompactionController, estimateMessagesTokens, } from "./compaction-controller.js";
|
|
27
28
|
import { DEFAULT_THINKING_LEVEL } from "./defaults.js";
|
|
28
29
|
import { exportSessionToHtml } from "./export-html/index.js";
|
|
29
30
|
import { createToolHtmlRenderer } from "./export-html/tool-renderer.js";
|
|
@@ -61,19 +62,9 @@ function withoutDeletedHeaders(headers) {
|
|
|
61
62
|
? Object.fromEntries(Object.entries(headers).filter((entry) => entry[1] !== null))
|
|
62
63
|
: undefined;
|
|
63
64
|
}
|
|
64
|
-
function estimateMessagesTokens(messages) {
|
|
65
|
-
let tokens = 0;
|
|
66
|
-
for (const message of messages) {
|
|
67
|
-
tokens += estimateTokens(message);
|
|
68
|
-
}
|
|
69
|
-
return tokens;
|
|
70
|
-
}
|
|
71
65
|
// ============================================================================
|
|
72
66
|
// Constants
|
|
73
67
|
// ============================================================================
|
|
74
|
-
const CONTEXT_REMAINS_OVER_COMPACTION_THRESHOLD = "Estimated retained context remains above the configured auto-compaction threshold. Reduce compaction.keepRecentTokens, remove large retained content, or switch to a model with a larger context window before continuing.";
|
|
75
|
-
const NOTHING_TO_COMPACT_WITHIN_KEEP_WINDOW = "Nothing to compact while preserving the configured recent context. Reduce compaction.keepRecentTokens, remove large retained content, or switch to a model with a larger context window before continuing.";
|
|
76
|
-
const AUTO_COMPACTION_STOPPED_BEFORE_NEXT_PROVIDER = "Stopped before the next provider request because auto-compaction did not produce a safe context. Review the compaction warning before continuing.";
|
|
77
68
|
/** Standard thinking levels */
|
|
78
69
|
const THINKING_LEVELS = ["off", "minimal", "low", "medium", "high"];
|
|
79
70
|
// ============================================================================
|
|
@@ -91,11 +82,6 @@ export class AgentSession {
|
|
|
91
82
|
this._pendingNextTurnMessages = [];
|
|
92
83
|
// Compaction state
|
|
93
84
|
this._compactionAbortController = undefined;
|
|
94
|
-
this._autoCompactionAbortController = undefined;
|
|
95
|
-
this._stopAfterTurnRequested = false;
|
|
96
|
-
/** An extension cancelled a mid-turn compaction; skip further mid-turn checks this run. */
|
|
97
|
-
this._midTurnCompactionDeclined = false;
|
|
98
|
-
this._overflowRecoveryAttempted = false;
|
|
99
85
|
// Branch summarization state
|
|
100
86
|
this._branchSummaryAbortController = undefined;
|
|
101
87
|
// Retry state
|
|
@@ -121,7 +107,7 @@ export class AgentSession {
|
|
|
121
107
|
// When a user message starts, check if it's from either queue and remove it BEFORE emitting
|
|
122
108
|
// This ensures the UI sees the updated queue state
|
|
123
109
|
if (event.type === "message_start" && event.message.role === "user") {
|
|
124
|
-
this.
|
|
110
|
+
this._compaction.resetOverflowRecovery();
|
|
125
111
|
const messageText = contentText(event.message.content, "");
|
|
126
112
|
if (messageText) {
|
|
127
113
|
// Check steering queue first
|
|
@@ -163,7 +149,7 @@ export class AgentSession {
|
|
|
163
149
|
this._lastAssistantMessage = event.message;
|
|
164
150
|
const assistantMsg = event.message;
|
|
165
151
|
if (assistantMsg.stopReason !== "error" && assistantMsg.stopReason !== "length") {
|
|
166
|
-
this.
|
|
152
|
+
this._compaction.resetOverflowRecovery();
|
|
167
153
|
}
|
|
168
154
|
// Reset retry counter immediately on successful assistant response
|
|
169
155
|
// This prevents accumulation across multiple LLM calls within a turn
|
|
@@ -192,6 +178,19 @@ export class AgentSession {
|
|
|
192
178
|
this._excludedToolNames = config.excludedToolNames ? new Set(config.excludedToolNames) : undefined;
|
|
193
179
|
this._baseToolsOverride = config.baseToolsOverride;
|
|
194
180
|
this._sessionStartEvent = config.sessionStartEvent ?? { type: "session_start", reason: "startup" };
|
|
181
|
+
this._compaction = new CompactionController({
|
|
182
|
+
agent: this.agent,
|
|
183
|
+
sessionManager: this.sessionManager,
|
|
184
|
+
settingsManager: this.settingsManager,
|
|
185
|
+
getModel: () => this.model,
|
|
186
|
+
getThinkingLevel: () => this.thinkingLevel,
|
|
187
|
+
getExtensionRunner: () => this._extensionRunner,
|
|
188
|
+
isManualCompactionActive: () => this._compactionAbortController !== undefined,
|
|
189
|
+
emit: (event) => this._emit(event),
|
|
190
|
+
getRequiredRequestAuth: (model) => this._getRequiredRequestAuth(model),
|
|
191
|
+
getSummarizationRequestAuth: (model, streamFunction) => this._getSummarizationRequestAuth(model, streamFunction),
|
|
192
|
+
summarizationRetryCallbacks: (source) => this._summarizationRetryCallbacks(source),
|
|
193
|
+
});
|
|
195
194
|
// Always subscribe to agent events for internal handling
|
|
196
195
|
// (session persistence, extensions, auto-compaction, retry logic)
|
|
197
196
|
this._unsubscribeAgent = this.agent.subscribe(this._handleAgentEvent);
|
|
@@ -319,12 +318,12 @@ export class AgentSession {
|
|
|
319
318
|
? async (_turn, signal) => await this.agent.prepareNextTurn?.(signal)
|
|
320
319
|
: undefined);
|
|
321
320
|
this.agent.prepareNextTurnWithContext = async (turn, signal) => {
|
|
322
|
-
const compaction = await this.
|
|
321
|
+
const compaction = await this._compaction.maybeCompactBeforeNextToolTurn(turn, signal);
|
|
323
322
|
if (compaction.stopErrorMessage) {
|
|
324
323
|
// Agent does not expose the low-level graceful turn-stop hook. Throwing here
|
|
325
324
|
// exits before queue polling or another provider request; the run records a
|
|
326
325
|
// normal error/aborted lifecycle instead of risking an oversized context.
|
|
327
|
-
this.
|
|
326
|
+
this._compaction.requestStopAfterTurn();
|
|
328
327
|
throw new Error(compaction.stopErrorMessage);
|
|
329
328
|
}
|
|
330
329
|
const effectiveTurn = compaction.messages === undefined
|
|
@@ -640,7 +639,7 @@ export class AgentSession {
|
|
|
640
639
|
}
|
|
641
640
|
/** Whether compaction or branch summarization is currently running */
|
|
642
641
|
get isCompacting() {
|
|
643
|
-
return (this.
|
|
642
|
+
return (this._compaction.isAutoCompacting ||
|
|
644
643
|
this._compactionAbortController !== undefined ||
|
|
645
644
|
this._branchSummaryAbortController !== undefined);
|
|
646
645
|
}
|
|
@@ -738,8 +737,7 @@ export class AgentSession {
|
|
|
738
737
|
// =========================================================================
|
|
739
738
|
async _runAgentPrompt(messages) {
|
|
740
739
|
this._isAgentRunActive = true;
|
|
741
|
-
this.
|
|
742
|
-
this._midTurnCompactionDeclined = false;
|
|
740
|
+
this._compaction.noteRunStart();
|
|
743
741
|
try {
|
|
744
742
|
await this.agent.prompt(messages);
|
|
745
743
|
while (await this._handlePostAgentRun()) {
|
|
@@ -756,12 +754,10 @@ export class AgentSession {
|
|
|
756
754
|
const msg = this._lastAssistantMessage;
|
|
757
755
|
this._lastAssistantMessage = undefined;
|
|
758
756
|
if (!msg) {
|
|
759
|
-
this.
|
|
757
|
+
this._compaction.consumeStopAfterTurn();
|
|
760
758
|
return false;
|
|
761
759
|
}
|
|
762
|
-
|
|
763
|
-
this._stopAfterTurnRequested = false;
|
|
764
|
-
if (stopAfterTurnRequested) {
|
|
760
|
+
if (this._compaction.consumeStopAfterTurn()) {
|
|
765
761
|
return false;
|
|
766
762
|
}
|
|
767
763
|
if (this._isRetryableError(msg) && (await this._prepareRetry(msg))) {
|
|
@@ -776,11 +772,10 @@ export class AgentSession {
|
|
|
776
772
|
});
|
|
777
773
|
this._retryAttempt = 0;
|
|
778
774
|
}
|
|
779
|
-
if (await this.
|
|
775
|
+
if (await this._compaction.checkCompaction(msg)) {
|
|
780
776
|
return true;
|
|
781
777
|
}
|
|
782
|
-
if (this.
|
|
783
|
-
this._stopAfterTurnRequested = false;
|
|
778
|
+
if (this._compaction.consumeStopAfterTurn()) {
|
|
784
779
|
return false;
|
|
785
780
|
}
|
|
786
781
|
// The agent loop drains both queues before emitting agent_end. Any messages
|
|
@@ -869,13 +864,13 @@ export class AgentSession {
|
|
|
869
864
|
// The user's new prompt is sent below, so do not call agent.continue() here.
|
|
870
865
|
const lastAssistant = this._findLastAssistantMessage();
|
|
871
866
|
if (lastAssistant) {
|
|
872
|
-
await this.
|
|
867
|
+
await this._compaction.checkCompaction(lastAssistant, false, "prePrompt");
|
|
873
868
|
}
|
|
874
869
|
const compactionSettings = this.settingsManager.getCompactionSettings();
|
|
875
870
|
// A compaction entry can be valid while its retained suffix is still too large.
|
|
876
871
|
// Refuse the existing context here rather than discovering that via provider overflow.
|
|
877
872
|
if (compactionSettings.enabled &&
|
|
878
|
-
shouldCompact(this.
|
|
873
|
+
shouldCompact(this._compaction.estimateContextTokens(this.agent.state.messages), this.model.contextWindow, compactionSettings)) {
|
|
879
874
|
throw new Error(CONTEXT_REMAINS_OVER_COMPACTION_THRESHOLD);
|
|
880
875
|
}
|
|
881
876
|
// Build messages array (custom message if any, then user message)
|
|
@@ -1503,7 +1498,7 @@ export class AgentSession {
|
|
|
1503
1498
|
*/
|
|
1504
1499
|
abortCompaction() {
|
|
1505
1500
|
this._compactionAbortController?.abort();
|
|
1506
|
-
this.
|
|
1501
|
+
this._compaction.abort();
|
|
1507
1502
|
}
|
|
1508
1503
|
/**
|
|
1509
1504
|
* Cancel in-progress branch summarization.
|
|
@@ -1511,380 +1506,6 @@ export class AgentSession {
|
|
|
1511
1506
|
abortBranchSummary() {
|
|
1512
1507
|
this._branchSummaryAbortController?.abort();
|
|
1513
1508
|
}
|
|
1514
|
-
_estimateContextTokensForCompaction(messages) {
|
|
1515
|
-
const estimate = estimateContextTokens(messages);
|
|
1516
|
-
const compactionEntry = getLatestCompactionEntry(this.sessionManager.getBranch());
|
|
1517
|
-
if (compactionEntry && estimate.lastUsageIndex !== null) {
|
|
1518
|
-
const usageMessage = messages[estimate.lastUsageIndex];
|
|
1519
|
-
if (usageMessage?.role === "assistant" &&
|
|
1520
|
-
usageMessage.timestamp <= new Date(compactionEntry.timestamp).getTime()) {
|
|
1521
|
-
// A retained pre-compaction usage describes the old, larger context. Fall back
|
|
1522
|
-
// to message estimates rather than immediately compacting again from stale data.
|
|
1523
|
-
return estimateMessagesTokens(messages);
|
|
1524
|
-
}
|
|
1525
|
-
}
|
|
1526
|
-
return estimate.tokens;
|
|
1527
|
-
}
|
|
1528
|
-
async _maybeCompactBeforeNextToolTurn(turn, signal) {
|
|
1529
|
-
if (signal?.aborted) {
|
|
1530
|
-
return { stopErrorMessage: AUTO_COMPACTION_STOPPED_BEFORE_NEXT_PROVIDER };
|
|
1531
|
-
}
|
|
1532
|
-
// Tool batches always continue the run; a turn without tool results only
|
|
1533
|
-
// continues when steering/follow-up messages are queued, otherwise the
|
|
1534
|
-
// post-run path covers it.
|
|
1535
|
-
if (turn.toolResults.length === 0 && !this.agent.hasQueuedMessages()) {
|
|
1536
|
-
return {};
|
|
1537
|
-
}
|
|
1538
|
-
if (this._midTurnCompactionDeclined) {
|
|
1539
|
-
return {};
|
|
1540
|
-
}
|
|
1541
|
-
const settings = this.settingsManager.getCompactionSettings();
|
|
1542
|
-
if (!settings.enabled) {
|
|
1543
|
-
return {};
|
|
1544
|
-
}
|
|
1545
|
-
const contextTokens = this._estimateContextTokensForCompaction(turn.context.messages);
|
|
1546
|
-
const contextWindow = this.model?.contextWindow ?? 0;
|
|
1547
|
-
if (!shouldCompact(contextTokens, contextWindow, settings)) {
|
|
1548
|
-
return {};
|
|
1549
|
-
}
|
|
1550
|
-
const outcome = await this._runAutoCompactionWithOutcome("threshold", false, "midTurn", signal);
|
|
1551
|
-
if (!outcome.compacted) {
|
|
1552
|
-
if (outcome.cancelled && signal?.aborted !== true) {
|
|
1553
|
-
// The extension took ownership of the risk; don't re-ask on every batch.
|
|
1554
|
-
this._midTurnCompactionDeclined = true;
|
|
1555
|
-
return {};
|
|
1556
|
-
}
|
|
1557
|
-
return { stopErrorMessage: AUTO_COMPACTION_STOPPED_BEFORE_NEXT_PROVIDER };
|
|
1558
|
-
}
|
|
1559
|
-
return {
|
|
1560
|
-
messages: this.agent.state.messages.slice(),
|
|
1561
|
-
...(signal?.aborted === true
|
|
1562
|
-
? { stopErrorMessage: AUTO_COMPACTION_STOPPED_BEFORE_NEXT_PROVIDER }
|
|
1563
|
-
: outcome.mustStop === true
|
|
1564
|
-
? { stopErrorMessage: CONTEXT_REMAINS_OVER_COMPACTION_THRESHOLD }
|
|
1565
|
-
: {}),
|
|
1566
|
-
};
|
|
1567
|
-
}
|
|
1568
|
-
/**
|
|
1569
|
-
* Check if compaction is needed and run it.
|
|
1570
|
-
* Called after agent_end and before prompt submission.
|
|
1571
|
-
*
|
|
1572
|
-
* Two cases:
|
|
1573
|
-
* 1. Recoverable failure: LLM returned context overflow or stopped below its desired output limit;
|
|
1574
|
-
* remove the assistant message, compact, and auto-retry once
|
|
1575
|
-
* 2. Threshold: Context over threshold, compact, NO auto-retry (user continues manually)
|
|
1576
|
-
*
|
|
1577
|
-
* @param assistantMessage The assistant message to check
|
|
1578
|
-
* @param skipAbortedCheck If false, include aborted messages (for pre-prompt check). Default: true
|
|
1579
|
-
*/
|
|
1580
|
-
async _checkCompaction(assistantMessage, skipAbortedCheck = true, timing = "postRun") {
|
|
1581
|
-
const settings = this.settingsManager.getCompactionSettings();
|
|
1582
|
-
if (!settings.enabled)
|
|
1583
|
-
return false;
|
|
1584
|
-
// Skip if message was aborted (user cancelled) - unless skipAbortedCheck is false
|
|
1585
|
-
if (skipAbortedCheck && assistantMessage.stopReason === "aborted")
|
|
1586
|
-
return false;
|
|
1587
|
-
const contextWindow = this.model?.contextWindow ?? 0;
|
|
1588
|
-
// Skip overflow check if the message came from a different model.
|
|
1589
|
-
// This handles the case where user switched from a smaller-context model (e.g. opus)
|
|
1590
|
-
// to a larger-context model (e.g. codex) - the overflow error from the old model
|
|
1591
|
-
// shouldn't trigger compaction for the new model.
|
|
1592
|
-
const sameModel = this.model && assistantMessage.provider === this.model.provider && assistantMessage.model === this.model.id;
|
|
1593
|
-
// Skip compaction checks if this assistant message is older than the latest
|
|
1594
|
-
// compaction boundary. This prevents a stale pre-compaction usage/error
|
|
1595
|
-
// from retriggering compaction on the first prompt after compaction.
|
|
1596
|
-
const compactionEntry = getLatestCompactionEntry(this.sessionManager.getBranch());
|
|
1597
|
-
const assistantIsFromBeforeCompaction = compactionEntry !== null && assistantMessage.timestamp <= new Date(compactionEntry.timestamp).getTime();
|
|
1598
|
-
if (assistantIsFromBeforeCompaction) {
|
|
1599
|
-
return false;
|
|
1600
|
-
}
|
|
1601
|
-
// Case 1: Recoverable failure. Explicit/silent context overflow still uses context metadata.
|
|
1602
|
-
// A length stop is recoverable when output ended below the model's original desired limit,
|
|
1603
|
-
// independent of the configured context size or any context-clamped provider request limit.
|
|
1604
|
-
// A successful response over the configured window should compact but must not retry: the
|
|
1605
|
-
// assistant answer already completed and agent.continue() cannot continue from an assistant.
|
|
1606
|
-
const recoverableLength = sameModel && isRecoverableLength(assistantMessage, this.model?.maxTokens ?? 0);
|
|
1607
|
-
if (sameModel && (isContextOverflow(assistantMessage, contextWindow) || recoverableLength)) {
|
|
1608
|
-
const willRetry = assistantMessage.stopReason !== "stop";
|
|
1609
|
-
if (!willRetry) {
|
|
1610
|
-
return await this._runAutoCompaction("overflow", false, timing);
|
|
1611
|
-
}
|
|
1612
|
-
if (this._overflowRecoveryAttempted) {
|
|
1613
|
-
this._stopAfterTurnRequested = true;
|
|
1614
|
-
this._emit({
|
|
1615
|
-
type: "compaction_end",
|
|
1616
|
-
reason: "overflow",
|
|
1617
|
-
result: undefined,
|
|
1618
|
-
aborted: false,
|
|
1619
|
-
willRetry: false,
|
|
1620
|
-
errorMessage: "Context overflow recovery failed after one compact-and-retry attempt. Try reducing context or switching to a larger-context model.",
|
|
1621
|
-
});
|
|
1622
|
-
return false;
|
|
1623
|
-
}
|
|
1624
|
-
this._overflowRecoveryAttempted = true;
|
|
1625
|
-
// Remove the failed or truncated message from agent state. It remains in session history,
|
|
1626
|
-
// but must not be included in the compact-and-retry context.
|
|
1627
|
-
const messages = this.agent.state.messages;
|
|
1628
|
-
if (messages.length > 0 && messages[messages.length - 1].role === "assistant") {
|
|
1629
|
-
this.agent.state.messages = messages.slice(0, -1);
|
|
1630
|
-
}
|
|
1631
|
-
return await this._runAutoCompaction("overflow", willRetry, timing);
|
|
1632
|
-
}
|
|
1633
|
-
// Case 2: Threshold - context is getting large
|
|
1634
|
-
// For error messages or all-zero usage messages, estimate from the last valid response.
|
|
1635
|
-
// This ensures sessions that hit persistent API errors (e.g. 529) or malformed zero-usage
|
|
1636
|
-
// responses can still compact and do not reset context accounting.
|
|
1637
|
-
let contextTokens;
|
|
1638
|
-
const directContextTokens = assistantMessage.usage ? calculateContextTokens(assistantMessage.usage) : 0;
|
|
1639
|
-
if (assistantMessage.stopReason === "error" || directContextTokens === 0) {
|
|
1640
|
-
const messages = this.agent.state.messages;
|
|
1641
|
-
const estimate = estimateContextTokens(messages);
|
|
1642
|
-
if (estimate.lastUsageIndex === null)
|
|
1643
|
-
return false; // No usage data at all
|
|
1644
|
-
// Verify the usage source is post-compaction. Kept pre-compaction messages
|
|
1645
|
-
// have stale usage reflecting the old (larger) context and would falsely
|
|
1646
|
-
// trigger compaction right after one just finished.
|
|
1647
|
-
const usageMsg = messages[estimate.lastUsageIndex];
|
|
1648
|
-
if (compactionEntry &&
|
|
1649
|
-
usageMsg.role === "assistant" &&
|
|
1650
|
-
usageMsg.timestamp <= new Date(compactionEntry.timestamp).getTime()) {
|
|
1651
|
-
return false;
|
|
1652
|
-
}
|
|
1653
|
-
contextTokens = estimate.tokens;
|
|
1654
|
-
}
|
|
1655
|
-
else {
|
|
1656
|
-
contextTokens = directContextTokens;
|
|
1657
|
-
}
|
|
1658
|
-
if (shouldCompact(contextTokens, contextWindow, settings)) {
|
|
1659
|
-
return await this._runAutoCompaction("threshold", false, timing);
|
|
1660
|
-
}
|
|
1661
|
-
return false;
|
|
1662
|
-
}
|
|
1663
|
-
/**
|
|
1664
|
-
* Internal: Run auto-compaction with events.
|
|
1665
|
-
*/
|
|
1666
|
-
async _runAutoCompaction(reason, willRetry, timing = "postRun") {
|
|
1667
|
-
const outcome = await this._runAutoCompactionWithOutcome(reason, willRetry, timing);
|
|
1668
|
-
if (!outcome.compacted || outcome.mustStop) {
|
|
1669
|
-
this._stopAfterTurnRequested = true;
|
|
1670
|
-
}
|
|
1671
|
-
return outcome.shouldContinue;
|
|
1672
|
-
}
|
|
1673
|
-
async _runAutoCompactionWithOutcome(reason, willRetry, timing, parentSignal) {
|
|
1674
|
-
const settings = this.settingsManager.getCompactionSettings();
|
|
1675
|
-
let started = false;
|
|
1676
|
-
let controller;
|
|
1677
|
-
let removeParentAbortListener;
|
|
1678
|
-
try {
|
|
1679
|
-
if (!this.model ||
|
|
1680
|
-
this._autoCompactionAbortController ||
|
|
1681
|
-
this._compactionAbortController ||
|
|
1682
|
-
parentSignal?.aborted) {
|
|
1683
|
-
return { compacted: false, shouldContinue: false };
|
|
1684
|
-
}
|
|
1685
|
-
const model = this.model;
|
|
1686
|
-
const streamFunction = this.agent.streamFunction;
|
|
1687
|
-
const thinkingLevel = this.thinkingLevel;
|
|
1688
|
-
const retrySettings = this.settingsManager.getRetrySettings();
|
|
1689
|
-
controller = new AbortController();
|
|
1690
|
-
if (parentSignal) {
|
|
1691
|
-
const abortFromParent = () => controller?.abort();
|
|
1692
|
-
parentSignal.addEventListener("abort", abortFromParent, { once: true });
|
|
1693
|
-
removeParentAbortListener = () => parentSignal.removeEventListener("abort", abortFromParent);
|
|
1694
|
-
}
|
|
1695
|
-
if (controller.signal.aborted) {
|
|
1696
|
-
return { compacted: false, shouldContinue: false };
|
|
1697
|
-
}
|
|
1698
|
-
this._autoCompactionAbortController = controller;
|
|
1699
|
-
started = true;
|
|
1700
|
-
this._emit({ type: "compaction_start", reason });
|
|
1701
|
-
if (controller.signal.aborted) {
|
|
1702
|
-
this._emit({
|
|
1703
|
-
type: "compaction_end",
|
|
1704
|
-
reason,
|
|
1705
|
-
result: undefined,
|
|
1706
|
-
aborted: true,
|
|
1707
|
-
willRetry: false,
|
|
1708
|
-
});
|
|
1709
|
-
return { compacted: false, shouldContinue: false };
|
|
1710
|
-
}
|
|
1711
|
-
let extensionCompaction;
|
|
1712
|
-
let fromExtension = false;
|
|
1713
|
-
const pathEntries = this.sessionManager.getBranch();
|
|
1714
|
-
const preparation = prepareCompaction(pathEntries, settings);
|
|
1715
|
-
if (!preparation) {
|
|
1716
|
-
throw new Error(NOTHING_TO_COMPACT_WITHIN_KEEP_WINDOW);
|
|
1717
|
-
}
|
|
1718
|
-
if (this._extensionRunner.hasHandlers("session_before_compact")) {
|
|
1719
|
-
const extensionResult = (await this._extensionRunner.emit({
|
|
1720
|
-
type: "session_before_compact",
|
|
1721
|
-
preparation,
|
|
1722
|
-
branchEntries: pathEntries,
|
|
1723
|
-
customInstructions: undefined,
|
|
1724
|
-
reason,
|
|
1725
|
-
timing,
|
|
1726
|
-
willRetry,
|
|
1727
|
-
signal: controller.signal,
|
|
1728
|
-
}));
|
|
1729
|
-
if (extensionResult?.cancel) {
|
|
1730
|
-
this._emit({
|
|
1731
|
-
type: "compaction_end",
|
|
1732
|
-
reason,
|
|
1733
|
-
result: undefined,
|
|
1734
|
-
aborted: true,
|
|
1735
|
-
willRetry: false,
|
|
1736
|
-
});
|
|
1737
|
-
// A cancel produced by an abort (abortCompaction/parent abort) is not an
|
|
1738
|
-
// extension decision; only a voluntary cancel hands the risk to the extension.
|
|
1739
|
-
return { compacted: false, shouldContinue: false, cancelled: !controller.signal.aborted };
|
|
1740
|
-
}
|
|
1741
|
-
if (extensionResult?.compaction) {
|
|
1742
|
-
extensionCompaction = extensionResult.compaction;
|
|
1743
|
-
fromExtension = true;
|
|
1744
|
-
}
|
|
1745
|
-
}
|
|
1746
|
-
let summary;
|
|
1747
|
-
let firstKeptEntryId;
|
|
1748
|
-
let tokensBefore;
|
|
1749
|
-
let usage;
|
|
1750
|
-
let details;
|
|
1751
|
-
if (extensionCompaction) {
|
|
1752
|
-
// Extension provided compaction content
|
|
1753
|
-
summary = extensionCompaction.summary;
|
|
1754
|
-
firstKeptEntryId = extensionCompaction.firstKeptEntryId;
|
|
1755
|
-
tokensBefore = extensionCompaction.tokensBefore;
|
|
1756
|
-
usage = extensionCompaction.usage;
|
|
1757
|
-
details = extensionCompaction.details;
|
|
1758
|
-
}
|
|
1759
|
-
else {
|
|
1760
|
-
let requestModel = model;
|
|
1761
|
-
let apiKey;
|
|
1762
|
-
let headers;
|
|
1763
|
-
let env;
|
|
1764
|
-
if (streamFunction === streamSimple) {
|
|
1765
|
-
({ model: requestModel, apiKey, headers, env } = await this._getRequiredRequestAuth(model));
|
|
1766
|
-
}
|
|
1767
|
-
else {
|
|
1768
|
-
({
|
|
1769
|
-
model: requestModel,
|
|
1770
|
-
apiKey,
|
|
1771
|
-
headers,
|
|
1772
|
-
env,
|
|
1773
|
-
} = await this._getSummarizationRequestAuth(model, streamFunction));
|
|
1774
|
-
}
|
|
1775
|
-
if (controller.signal.aborted) {
|
|
1776
|
-
this._emit({
|
|
1777
|
-
type: "compaction_end",
|
|
1778
|
-
reason,
|
|
1779
|
-
result: undefined,
|
|
1780
|
-
aborted: true,
|
|
1781
|
-
willRetry: false,
|
|
1782
|
-
});
|
|
1783
|
-
return { compacted: false, shouldContinue: false };
|
|
1784
|
-
}
|
|
1785
|
-
// Generate compaction result
|
|
1786
|
-
const compactResult = await compact(preparation, requestModel, apiKey, headers, undefined, controller.signal, thinkingLevel, streamFunction, env, retrySettings, this._summarizationRetryCallbacks({ source: "compaction", reason }));
|
|
1787
|
-
summary = compactResult.summary;
|
|
1788
|
-
firstKeptEntryId = compactResult.firstKeptEntryId;
|
|
1789
|
-
tokensBefore = compactResult.tokensBefore;
|
|
1790
|
-
usage = compactResult.usage;
|
|
1791
|
-
details = compactResult.details;
|
|
1792
|
-
}
|
|
1793
|
-
if (controller.signal.aborted) {
|
|
1794
|
-
this._emit({
|
|
1795
|
-
type: "compaction_end",
|
|
1796
|
-
reason,
|
|
1797
|
-
result: undefined,
|
|
1798
|
-
aborted: true,
|
|
1799
|
-
willRetry: false,
|
|
1800
|
-
});
|
|
1801
|
-
return { compacted: false, shouldContinue: false };
|
|
1802
|
-
}
|
|
1803
|
-
this.sessionManager.appendCompaction(summary, firstKeptEntryId, tokensBefore, details, fromExtension, usage);
|
|
1804
|
-
const newEntries = this.sessionManager.getEntries();
|
|
1805
|
-
const sessionContext = this.sessionManager.buildSessionContext();
|
|
1806
|
-
this.agent.state.messages = sessionContext.messages;
|
|
1807
|
-
if (willRetry) {
|
|
1808
|
-
const messages = this.agent.state.messages;
|
|
1809
|
-
const lastMsg = messages[messages.length - 1];
|
|
1810
|
-
// The overflow response was persisted on message_end before _checkCompaction() removed it
|
|
1811
|
-
// from agent state. Rebuilding state from the new compaction can restore that kept entry,
|
|
1812
|
-
// leaving an assistant as the final message. agent.continue() rejects that state, so remove
|
|
1813
|
-
// the retriable error or truncated-length response again before continuing the interrupted turn.
|
|
1814
|
-
if (lastMsg?.role === "assistant" &&
|
|
1815
|
-
(lastMsg.stopReason === "error" ||
|
|
1816
|
-
lastMsg.stopReason === "length")) {
|
|
1817
|
-
this.agent.state.messages = messages.slice(0, -1);
|
|
1818
|
-
}
|
|
1819
|
-
}
|
|
1820
|
-
const estimatedTokensAfter = this._estimateContextTokensForCompaction(this.agent.state.messages);
|
|
1821
|
-
const contextStillOverThreshold = shouldCompact(estimatedTokensAfter, model.contextWindow, settings);
|
|
1822
|
-
const effectiveWillRetry = willRetry && !contextStillOverThreshold;
|
|
1823
|
-
// Get the saved compaction entry for the extension event
|
|
1824
|
-
const savedCompactionEntry = newEntries.find((e) => e.type === "compaction" && e.summary === summary);
|
|
1825
|
-
if (this._extensionRunner && savedCompactionEntry) {
|
|
1826
|
-
await this._extensionRunner.emit({
|
|
1827
|
-
type: "session_compact",
|
|
1828
|
-
compactionEntry: savedCompactionEntry,
|
|
1829
|
-
fromExtension,
|
|
1830
|
-
reason,
|
|
1831
|
-
willRetry: effectiveWillRetry,
|
|
1832
|
-
});
|
|
1833
|
-
}
|
|
1834
|
-
const result = {
|
|
1835
|
-
summary,
|
|
1836
|
-
firstKeptEntryId,
|
|
1837
|
-
tokensBefore,
|
|
1838
|
-
estimatedTokensAfter,
|
|
1839
|
-
usage,
|
|
1840
|
-
details,
|
|
1841
|
-
};
|
|
1842
|
-
this._emit({
|
|
1843
|
-
type: "compaction_end",
|
|
1844
|
-
reason,
|
|
1845
|
-
result,
|
|
1846
|
-
aborted: false,
|
|
1847
|
-
willRetry: effectiveWillRetry,
|
|
1848
|
-
...(contextStillOverThreshold ? { errorMessage: CONTEXT_REMAINS_OVER_COMPACTION_THRESHOLD } : {}),
|
|
1849
|
-
});
|
|
1850
|
-
if (contextStillOverThreshold) {
|
|
1851
|
-
return { compacted: true, shouldContinue: false, mustStop: true };
|
|
1852
|
-
}
|
|
1853
|
-
if (effectiveWillRetry) {
|
|
1854
|
-
return { compacted: true, shouldContinue: true };
|
|
1855
|
-
}
|
|
1856
|
-
// Auto-compaction can complete while follow-up/steering/custom messages are waiting.
|
|
1857
|
-
// Continue once so queued messages are delivered.
|
|
1858
|
-
return { compacted: true, shouldContinue: this.agent.hasQueuedMessages() };
|
|
1859
|
-
}
|
|
1860
|
-
catch (error) {
|
|
1861
|
-
const aborted = controller?.signal.aborted || (error instanceof Error && error.name === "AbortError");
|
|
1862
|
-
const errorMessage = error instanceof Error ? error.message : "compaction failed";
|
|
1863
|
-
if (started) {
|
|
1864
|
-
this._emit({
|
|
1865
|
-
type: "compaction_end",
|
|
1866
|
-
reason,
|
|
1867
|
-
result: undefined,
|
|
1868
|
-
aborted,
|
|
1869
|
-
willRetry: false,
|
|
1870
|
-
...(aborted
|
|
1871
|
-
? {}
|
|
1872
|
-
: {
|
|
1873
|
-
errorMessage: reason === "overflow"
|
|
1874
|
-
? `Context overflow recovery failed: ${errorMessage}`
|
|
1875
|
-
: `Auto-compaction failed: ${errorMessage}`,
|
|
1876
|
-
}),
|
|
1877
|
-
});
|
|
1878
|
-
}
|
|
1879
|
-
return { compacted: false, shouldContinue: false };
|
|
1880
|
-
}
|
|
1881
|
-
finally {
|
|
1882
|
-
removeParentAbortListener?.();
|
|
1883
|
-
if (this._autoCompactionAbortController === controller) {
|
|
1884
|
-
this._autoCompactionAbortController = undefined;
|
|
1885
|
-
}
|
|
1886
|
-
}
|
|
1887
|
-
}
|
|
1888
1509
|
/**
|
|
1889
1510
|
* Toggle auto-compaction setting.
|
|
1890
1511
|
*/
|