@selesai/code 0.13.26 → 0.13.27
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 +74 -0
- package/dist/cli/args.js +1 -0
- package/dist/cli/session-picker.d.ts +1 -1
- package/dist/config.js +1 -1
- package/dist/core/agent-session-auto-handoff.test.js +2 -1
- package/dist/core/agent-session-length-continuation.test.js +2 -1
- package/dist/core/agent-session.d.ts +58 -7
- package/dist/core/agent-session.js +334 -167
- package/dist/core/bug-report-upload.d.ts +11 -0
- package/dist/core/bug-report-upload.js +21 -0
- package/dist/core/bug-report.d.ts +175 -0
- package/dist/core/bug-report.js +291 -0
- package/dist/core/cache-stats.js +12 -1
- package/dist/core/cache-warmer.d.ts +101 -0
- package/dist/core/cache-warmer.js +341 -0
- package/dist/core/compaction/branch-summarization.js +2 -2
- package/dist/core/compaction/compaction.d.ts +6 -5
- package/dist/core/compaction/compaction.js +49 -42
- package/dist/core/crash-log.d.ts +21 -0
- package/dist/core/crash-log.js +70 -0
- package/dist/core/experimental.d.ts +0 -4
- package/dist/core/experimental.js +0 -4
- package/dist/core/extensions/index.d.ts +1 -1
- package/dist/core/extensions/jiti-loader.d.ts +1 -0
- package/dist/core/extensions/jiti-loader.js +3 -0
- package/dist/core/extensions/jiti-static-loader.d.ts +1 -0
- package/dist/core/extensions/jiti-static-loader.js +3 -0
- package/dist/core/extensions/loader.js +39 -49
- package/dist/core/extensions/runner.d.ts +10 -7
- package/dist/core/extensions/runner.js +85 -67
- package/dist/core/extensions/types.d.ts +66 -52
- package/dist/core/extensions/virtual-modules.d.ts +2 -0
- package/dist/core/extensions/virtual-modules.js +39 -0
- package/dist/core/extensions/wrapper.js +1 -20
- package/dist/core/index.d.ts +2 -1
- package/dist/core/keybindings.d.ts +1 -1
- package/dist/core/keybindings.js +1 -1
- package/dist/core/messages.js +1 -0
- package/dist/core/model-config.d.ts +101 -20
- package/dist/core/model-config.js +25 -18
- package/dist/core/model-resolver.js +2 -1
- package/dist/core/model-runtime.js +5 -3
- package/dist/core/provider-composer.d.ts +7 -2
- package/dist/core/provider-composer.js +10 -5
- package/dist/core/radius.d.ts +3 -0
- package/dist/core/radius.js +6 -0
- package/dist/core/sdk.js +60 -38
- package/dist/core/session-export.d.ts +6 -2
- package/dist/core/session-export.js +14 -13
- package/dist/core/session-format.test.d.ts +1 -0
- package/dist/core/session-format.test.js +96 -0
- package/dist/core/session-manager.d.ts +29 -6
- package/dist/core/session-manager.js +159 -96
- package/dist/core/settings-manager.d.ts +22 -5
- package/dist/core/settings-manager.js +56 -23
- package/dist/core/slash-commands.js +1 -0
- package/dist/core/system-prompt.d.ts +49 -4
- package/dist/core/system-prompt.js +155 -102
- package/dist/core/system-prompt.test.js +61 -23
- package/dist/core/tools/bash.d.ts +2 -1
- package/dist/core/tools/bash.js +10 -4
- package/dist/core/tools/edit.js +1 -2
- package/dist/core/tools/read.js +1 -2
- package/dist/core/tools/renderers/bash.js +9 -1
- package/dist/core/tools/write.js +1 -2
- package/dist/core/usage-totals.d.ts +1 -1
- package/dist/core/usage-totals.js +5 -1
- package/dist/extensions/pi-web-agent/src/backends/factory.ts +14 -6
- package/dist/extensions/pi-web-agent/src/extension.ts +3 -1
- package/dist/extensions/pi-web-agent/src/orchestration/answer-synthesizer.ts +7 -3
- package/dist/extensions/pi-web-agent/src/orchestration/research-orchestrator.ts +3 -0
- package/dist/extensions/pi-web-agent/src/orchestration/research-types.ts +2 -0
- package/dist/extensions/pi-web-agent/src/presentation/explore-presentation.ts +4 -2
- package/dist/extensions/pi-web-agent/src/search/tokenin.ts +4 -1
- package/dist/extensions/pi-web-agent/src/tools/web-explore.ts +2 -1
- package/dist/extensions/pi-web-agent/src/types.ts +4 -0
- package/dist/index.d.ts +3 -2
- package/dist/main.js +13 -9
- package/dist/migrations.js +2 -2
- package/dist/modes/interactive/bug-report.d.ts +15 -0
- package/dist/modes/interactive/bug-report.js +205 -0
- package/dist/modes/interactive/chat-viewport.js +1 -1
- package/dist/modes/interactive/components/branch-summary-message.js +12 -5
- package/dist/modes/interactive/components/compaction-summary-message.js +12 -5
- package/dist/modes/interactive/components/custom-editor.d.ts +3 -3
- package/dist/modes/interactive/components/extension-editor.d.ts +4 -1
- package/dist/modes/interactive/components/extension-editor.js +7 -2
- package/dist/modes/interactive/components/extension-input.d.ts +2 -0
- package/dist/modes/interactive/components/extension-input.js +6 -0
- package/dist/modes/interactive/components/extension-selector.d.ts +1 -0
- package/dist/modes/interactive/components/extension-selector.js +4 -0
- package/dist/modes/interactive/components/footer.js +4 -1
- package/dist/modes/interactive/components/session-selector.d.ts +5 -5
- package/dist/modes/interactive/components/session-selector.js +74 -48
- package/dist/modes/interactive/components/settings-selector.d.ts +3 -1
- package/dist/modes/interactive/components/settings-selector.js +11 -0
- package/dist/modes/interactive/components/skill-invocation-message.js +11 -4
- package/dist/modes/interactive/components/status-indicator.d.ts +2 -2
- package/dist/modes/interactive/components/status-indicator.js +7 -7
- package/dist/modes/interactive/components/tool-execution.js +18 -9
- package/dist/modes/interactive/components/tree-selector.js +2 -0
- package/dist/modes/interactive/interactive-mode.d.ts +15 -2
- package/dist/modes/interactive/interactive-mode.js +257 -81
- package/dist/modes/interactive/session-share.d.ts +2 -0
- package/dist/modes/interactive/session-share.js +8 -4
- package/dist/modes/interactive/tui-renderer.js +2 -2
- package/dist/modes/rpc/rpc-mode.js +2 -2
- package/dist/utils/clipboard-command.d.ts +6 -0
- package/dist/utils/clipboard-command.js +44 -0
- package/dist/utils/clipboard-image.js +55 -89
- package/dist/utils/clipboard.d.ts +1 -1
- package/dist/utils/clipboard.js +118 -108
- package/dist/utils/wsl.d.ts +2 -0
- package/dist/utils/wsl.js +14 -0
- package/dist/utils/zip.d.ts +6 -0
- package/dist/utils/zip.js +59 -0
- package/package.json +9 -9
- package/dist/utils/clipboard-native.d.ts +0 -10
- package/dist/utils/clipboard-native.js +0 -19
|
@@ -14,8 +14,8 @@
|
|
|
14
14
|
*/
|
|
15
15
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
16
16
|
import { basename, dirname } from "node:path";
|
|
17
|
-
import { contentText } from "@earendil-works/pi-ai";
|
|
18
|
-
import { clampThinkingLevel, cleanupSessionResources, getSupportedThinkingLevels, isContextOverflow, isRetryableAssistantError, modelsAreEqual, resetApiProviders, streamSimple, } from "@earendil-works/pi-ai/compat";
|
|
17
|
+
import { contentText, getCurrentSystemMessage, retryDelayMs } from "@earendil-works/pi-ai";
|
|
18
|
+
import { clampThinkingLevel, cleanupSessionResources, getSupportedThinkingLevels, isContextOverflow, isRecoverableLength, 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,6 +23,7 @@ import { sleep } from "../utils/sleep.js";
|
|
|
23
23
|
import { normalizeAssistantThinkingTags } from "../utils/thinking-tags.js";
|
|
24
24
|
import { formatNoApiKeyFoundMessage, formatNoModelSelectedMessage } from "./auth-guidance.js";
|
|
25
25
|
import { executeBashWithOperations } from "./bash-executor.js";
|
|
26
|
+
import { generateBugReportSummary } from "./bug-report.js";
|
|
26
27
|
import { calculateContextTokens, collectEntriesForBranchSummary, compact, estimateContextTokens, estimateTokens, generateBranchSummary, prepareCompaction, shouldCompact, } from "./compaction/index.js";
|
|
27
28
|
import { DEFAULT_THINKING_LEVEL, THINKING_LEVEL_OPTIONS } from "./defaults.js";
|
|
28
29
|
import { exportSessionToHtml } from "./export-html/index.js";
|
|
@@ -34,7 +35,7 @@ import { ModelRegistry } from "./model-registry.js";
|
|
|
34
35
|
import { expandPromptTemplate } from "./prompt-templates.js";
|
|
35
36
|
import { CURRENT_SESSION_VERSION, getLatestCompactionEntry } from "./session-manager.js";
|
|
36
37
|
import { createSyntheticSourceInfo } from "./source-info.js";
|
|
37
|
-
import { buildSystemPrompt } from "./system-prompt.js";
|
|
38
|
+
import { buildSystemPrompt, buildSystemPromptSections, diffSystemPromptSections, normalizeBuildSystemPromptOptions, } from "./system-prompt.js";
|
|
38
39
|
import { createLocalBashOperations } from "./tools/bash.js";
|
|
39
40
|
import { createAllToolDefinitions } from "./tools/index.js";
|
|
40
41
|
import { createToolDefinitionFromAgentTool } from "./tools/tool-definition-wrapper.js";
|
|
@@ -99,6 +100,7 @@ export class AgentSession {
|
|
|
99
100
|
_unsubscribeAgent;
|
|
100
101
|
_eventListeners = [];
|
|
101
102
|
_isAgentRunActive = false;
|
|
103
|
+
_agentRunAbortRequested = false;
|
|
102
104
|
_idleWaitPromise;
|
|
103
105
|
_resolveIdleWait;
|
|
104
106
|
/** Tracks pending steering messages for UI display. Removed when delivered. */
|
|
@@ -144,15 +146,15 @@ export class AgentSession {
|
|
|
144
146
|
_extensionErrorListener;
|
|
145
147
|
_extensionErrorUnsubscriber;
|
|
146
148
|
_modelRuntime;
|
|
149
|
+
_cacheWarmer;
|
|
147
150
|
// Tool registry for extension getTools/setTools
|
|
148
151
|
_toolRegistry = new Map();
|
|
149
152
|
_toolDefinitions = new Map();
|
|
150
153
|
_toolPromptSnippets = new Map();
|
|
151
154
|
_toolPromptGuidelines = new Map();
|
|
152
|
-
// Base system prompt (without extension appends) - used to apply fresh appends each turn
|
|
153
|
-
_baseSystemPrompt = "";
|
|
154
155
|
_baseSystemPromptOptions;
|
|
155
|
-
|
|
156
|
+
/** Prompt options after before_agent_start mutations for the active run. */
|
|
157
|
+
_runSystemPromptOptions;
|
|
156
158
|
constructor(config) {
|
|
157
159
|
this.agent = config.agent;
|
|
158
160
|
this.sessionManager = config.sessionManager;
|
|
@@ -162,6 +164,10 @@ export class AgentSession {
|
|
|
162
164
|
this._customTools = config.customTools ?? [];
|
|
163
165
|
this._cwd = config.cwd;
|
|
164
166
|
this._modelRuntime = config.modelRuntime;
|
|
167
|
+
this._cacheWarmer = config.cacheWarmer;
|
|
168
|
+
if (this._cacheWarmer) {
|
|
169
|
+
this._cacheWarmer.onWarmed = (entry) => this._emit({ type: "entry_appended", entry });
|
|
170
|
+
}
|
|
165
171
|
this._extensionRunnerRef = config.extensionRunnerRef;
|
|
166
172
|
this._initialActiveToolNames = config.initialActiveToolNames;
|
|
167
173
|
this._allowedToolNames = config.allowedToolNames ? new Set(config.allowedToolNames) : undefined;
|
|
@@ -173,18 +179,21 @@ export class AgentSession {
|
|
|
173
179
|
this._unsubscribeAgent = this.agent.subscribe(this._handleAgentEvent);
|
|
174
180
|
this._installAgentToolHooks();
|
|
175
181
|
this._installAgentNextTurnRefresh();
|
|
182
|
+
this._installAgentForcedPromptProjection();
|
|
176
183
|
this._buildRuntime({
|
|
177
184
|
activeToolNames: this._initialActiveToolNames,
|
|
178
185
|
includeAllExtensionTools: true,
|
|
179
186
|
});
|
|
187
|
+
if (this._initialActiveToolNames === undefined)
|
|
188
|
+
this._restoreToolsFromTranscript();
|
|
180
189
|
}
|
|
181
190
|
get modelRuntime() {
|
|
182
191
|
return this._modelRuntime;
|
|
183
192
|
}
|
|
184
|
-
async _getRequiredRequestAuth(model) {
|
|
193
|
+
async _getRequiredRequestAuth(model, signal) {
|
|
185
194
|
let result;
|
|
186
195
|
try {
|
|
187
|
-
result = await this._modelRuntime.getAuth(model);
|
|
196
|
+
result = await this._modelRuntime.getAuth(model, { signal });
|
|
188
197
|
}
|
|
189
198
|
catch (error) {
|
|
190
199
|
const cause = error instanceof Error ? error.cause : undefined;
|
|
@@ -194,7 +203,9 @@ export class AgentSession {
|
|
|
194
203
|
throw error;
|
|
195
204
|
}
|
|
196
205
|
if (result && (result.auth.apiKey || result.auth.headers)) {
|
|
206
|
+
const requestModel = result.auth.baseUrl ? { ...model, baseUrl: result.auth.baseUrl } : model;
|
|
197
207
|
return {
|
|
208
|
+
model: requestModel,
|
|
198
209
|
apiKey: result.auth.apiKey,
|
|
199
210
|
headers: withoutDeletedHeaders(result.auth.headers),
|
|
200
211
|
env: result.env,
|
|
@@ -208,18 +219,26 @@ export class AgentSession {
|
|
|
208
219
|
}
|
|
209
220
|
throw new Error(formatNoApiKeyFoundMessage(model.provider));
|
|
210
221
|
}
|
|
211
|
-
async _getSummarizationRequestAuth(model) {
|
|
222
|
+
async _getSummarizationRequestAuth(model, signal) {
|
|
212
223
|
if (this.agent.streamFunction === streamSimple) {
|
|
213
|
-
return this._getRequiredRequestAuth(model);
|
|
224
|
+
return this._getRequiredRequestAuth(model, signal);
|
|
214
225
|
}
|
|
215
226
|
try {
|
|
216
|
-
const result = await this._modelRuntime.getAuth(model);
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
227
|
+
const result = await this._modelRuntime.getAuth(model, { signal });
|
|
228
|
+
if (!result)
|
|
229
|
+
return { model };
|
|
230
|
+
const requestModel = result.auth.baseUrl ? { ...model, baseUrl: result.auth.baseUrl } : model;
|
|
231
|
+
return {
|
|
232
|
+
model: requestModel,
|
|
233
|
+
apiKey: result.auth.apiKey,
|
|
234
|
+
headers: withoutDeletedHeaders(result.auth.headers),
|
|
235
|
+
env: result.env,
|
|
236
|
+
};
|
|
220
237
|
}
|
|
221
|
-
catch {
|
|
222
|
-
|
|
238
|
+
catch (error) {
|
|
239
|
+
if (signal?.aborted)
|
|
240
|
+
throw error;
|
|
241
|
+
return { model };
|
|
223
242
|
}
|
|
224
243
|
}
|
|
225
244
|
/**
|
|
@@ -280,7 +299,7 @@ export class AgentSession {
|
|
|
280
299
|
}
|
|
281
300
|
async _compactBeforeNextAssistantResponse(context) {
|
|
282
301
|
const model = this.model;
|
|
283
|
-
const settings = this.settingsManager.getCompactionSettings();
|
|
302
|
+
const settings = this.settingsManager.getCompactionSettings(model);
|
|
284
303
|
if (!model ||
|
|
285
304
|
model.contextWindow <= 0 ||
|
|
286
305
|
!shouldCompact(estimateContextTokens(context.messages).tokens, model.contextWindow, settings)) {
|
|
@@ -301,13 +320,25 @@ export class AgentSession {
|
|
|
301
320
|
const context = await this._compactBeforeNextAssistantResponse(turn.context);
|
|
302
321
|
const previousSnapshot = await previousPrepareNextTurnWithContext?.({ ...turn, context }, signal);
|
|
303
322
|
const nextContext = previousSnapshot?.context ?? context;
|
|
323
|
+
const runOptions = this._runSystemPromptOptions ?? this._baseSystemPromptOptions;
|
|
324
|
+
const options = normalizeBuildSystemPromptOptions({
|
|
325
|
+
...runOptions,
|
|
326
|
+
selectedTools: this.getActiveToolNames(),
|
|
327
|
+
toolSnippets: { ...this._baseSystemPromptOptions.toolSnippets, ...runOptions.toolSnippets },
|
|
328
|
+
toolGuidelines: { ...this._baseSystemPromptOptions.toolGuidelines, ...runOptions.toolGuidelines },
|
|
329
|
+
});
|
|
330
|
+
const updateMessage = this._preparePromptAndToolLoadout(options, nextContext.messages);
|
|
331
|
+
// Keep session.systemPrompt and ctx.getSystemPrompt() in step with what the provider sees.
|
|
332
|
+
this._runSystemPromptOptions = options;
|
|
304
333
|
return {
|
|
305
334
|
...previousSnapshot,
|
|
306
335
|
context: {
|
|
307
336
|
...nextContext,
|
|
308
|
-
systemPrompt: this._systemPromptOverride ?? this._baseSystemPrompt,
|
|
309
337
|
tools: this.agent.state.tools.slice(),
|
|
310
338
|
},
|
|
339
|
+
messages: updateMessage
|
|
340
|
+
? [...(previousSnapshot?.messages ?? []), updateMessage]
|
|
341
|
+
: previousSnapshot?.messages,
|
|
311
342
|
model: this.agent.state.model,
|
|
312
343
|
thinkingLevel: this.agent.state.thinkingLevel,
|
|
313
344
|
};
|
|
@@ -352,6 +383,7 @@ export class AgentSession {
|
|
|
352
383
|
resolve();
|
|
353
384
|
}
|
|
354
385
|
async _emitAgentSettled() {
|
|
386
|
+
this._cacheWarmer?.onAgentSettled();
|
|
355
387
|
this._isAgentRunActive = false;
|
|
356
388
|
try {
|
|
357
389
|
await this._extensionRunner.emit({ type: "agent_settled" });
|
|
@@ -436,7 +468,8 @@ export class AgentSession {
|
|
|
436
468
|
// Persist as CustomMessageEntry
|
|
437
469
|
this.sessionManager.appendCustomMessageEntry(event.message.customType, event.message.content, event.message.display, event.message.details);
|
|
438
470
|
}
|
|
439
|
-
else if (event.message.role === "
|
|
471
|
+
else if (event.message.role === "system" ||
|
|
472
|
+
event.message.role === "user" ||
|
|
440
473
|
event.message.role === "assistant" ||
|
|
441
474
|
event.message.role === "toolResult") {
|
|
442
475
|
// Regular LLM message - persist as SessionMessageEntry
|
|
@@ -447,7 +480,9 @@ export class AgentSession {
|
|
|
447
480
|
if (event.message.role === "assistant") {
|
|
448
481
|
this._lastAssistantMessage = event.message;
|
|
449
482
|
const assistantMsg = event.message;
|
|
450
|
-
|
|
483
|
+
// A length stop is itself a recovery trigger, so it must not clear the
|
|
484
|
+
// one-shot overflow-recovery guard before the retry has run.
|
|
485
|
+
if (assistantMsg.stopReason !== "error" && assistantMsg.stopReason !== "length") {
|
|
451
486
|
this._overflowRecoveryAttempted = false;
|
|
452
487
|
}
|
|
453
488
|
// Reset the length-continuation counter on any non-length stop so the
|
|
@@ -477,6 +512,8 @@ export class AgentSession {
|
|
|
477
512
|
}
|
|
478
513
|
};
|
|
479
514
|
_willRetryAfterAgentEnd(event) {
|
|
515
|
+
if (this._agentRunAbortRequested)
|
|
516
|
+
return false;
|
|
480
517
|
const settings = this.settingsManager.getRetrySettings();
|
|
481
518
|
if (!settings.enabled || this._retryAttempt >= settings.maxRetries) {
|
|
482
519
|
return false;
|
|
@@ -665,6 +702,10 @@ export class AgentSession {
|
|
|
665
702
|
this._extensionRunner.invalidate("This extension ctx is stale after session replacement or reload. Do not use a captured pi or command ctx after ctx.newSession(), ctx.fork(), ctx.switchSession(), or ctx.reload(). For newSession, fork, and switchSession, move post-replacement work into withSession and use the ctx passed to withSession. For reload, do not use the old ctx after await ctx.reload().");
|
|
666
703
|
this._disconnectFromAgent();
|
|
667
704
|
this._eventListeners = [];
|
|
705
|
+
if (this._cacheWarmer) {
|
|
706
|
+
this._cacheWarmer.onWarmed = undefined;
|
|
707
|
+
this._cacheWarmer.cancel();
|
|
708
|
+
}
|
|
668
709
|
cleanupSessionResources(this.sessionId);
|
|
669
710
|
}
|
|
670
711
|
// =========================================================================
|
|
@@ -674,6 +715,15 @@ export class AgentSession {
|
|
|
674
715
|
get state() {
|
|
675
716
|
return this.agent.state;
|
|
676
717
|
}
|
|
718
|
+
/** Current cache-warming state and the policy inputs that produced it. */
|
|
719
|
+
get cacheWarmingStatus() {
|
|
720
|
+
return this._cacheWarmer?.status;
|
|
721
|
+
}
|
|
722
|
+
/** Persist the cache-warming mode and immediately reconcile active warming. */
|
|
723
|
+
setCacheWarmingMode(mode) {
|
|
724
|
+
this.settingsManager.setCacheWarmingMode(mode);
|
|
725
|
+
this._cacheWarmer?.onModeChanged();
|
|
726
|
+
}
|
|
677
727
|
/** Current model (may be undefined if not yet selected) */
|
|
678
728
|
get model() {
|
|
679
729
|
return this.agent.state.model;
|
|
@@ -690,9 +740,9 @@ export class AgentSession {
|
|
|
690
740
|
get isIdle() {
|
|
691
741
|
return !this._isAgentRunActive && !this.isCompacting;
|
|
692
742
|
}
|
|
693
|
-
/** Current effective system prompt
|
|
743
|
+
/** Current effective system prompt, including changes not yet sent to the model. */
|
|
694
744
|
get systemPrompt() {
|
|
695
|
-
return this.
|
|
745
|
+
return buildSystemPrompt(this._runSystemPromptOptions ?? this._baseSystemPromptOptions);
|
|
696
746
|
}
|
|
697
747
|
/** Current retry attempt (0 if not retrying) */
|
|
698
748
|
get retryAttempt() {
|
|
@@ -738,9 +788,7 @@ export class AgentSession {
|
|
|
738
788
|
}
|
|
739
789
|
}
|
|
740
790
|
this.agent.state.tools = tools;
|
|
741
|
-
|
|
742
|
-
this._baseSystemPrompt = this._rebuildSystemPrompt(validToolNames);
|
|
743
|
-
this.agent.state.systemPrompt = this._systemPromptOverride ?? this._baseSystemPrompt;
|
|
791
|
+
this._rebuildSystemPrompt(validToolNames);
|
|
744
792
|
}
|
|
745
793
|
/** Whether compaction or branch summarization is currently running */
|
|
746
794
|
get isCompacting() {
|
|
@@ -809,16 +857,10 @@ export class AgentSession {
|
|
|
809
857
|
_rebuildSystemPrompt(toolNames) {
|
|
810
858
|
const validToolNames = toolNames.filter((name) => this._toolRegistry.has(name));
|
|
811
859
|
const toolSnippets = {};
|
|
812
|
-
const
|
|
813
|
-
for (const name of validToolNames) {
|
|
860
|
+
for (const name of this._toolRegistry.keys()) {
|
|
814
861
|
const snippet = this._toolPromptSnippets.get(name);
|
|
815
|
-
if (snippet)
|
|
862
|
+
if (snippet)
|
|
816
863
|
toolSnippets[name] = snippet;
|
|
817
|
-
}
|
|
818
|
-
const toolGuidelines = this._toolPromptGuidelines.get(name);
|
|
819
|
-
if (toolGuidelines) {
|
|
820
|
-
promptGuidelines.push(...toolGuidelines);
|
|
821
|
-
}
|
|
822
864
|
}
|
|
823
865
|
const loaderSystemPrompt = this._resourceLoader.getSystemPrompt();
|
|
824
866
|
const loaderAppendSystemPrompt = this._resourceLoader.getAppendSystemPrompt();
|
|
@@ -826,7 +868,7 @@ export class AgentSession {
|
|
|
826
868
|
const loadedSkills = this._resourceLoader.getPromptSkills();
|
|
827
869
|
const loadedAgents = this._resourceLoader.getAgents().agents;
|
|
828
870
|
const loadedContextFiles = this._resourceLoader.getAgentsFiles().agentsFiles;
|
|
829
|
-
this._baseSystemPromptOptions = {
|
|
871
|
+
this._baseSystemPromptOptions = normalizeBuildSystemPromptOptions({
|
|
830
872
|
cwd: this._cwd,
|
|
831
873
|
skills: loadedSkills,
|
|
832
874
|
agents: loadedAgents,
|
|
@@ -835,23 +877,87 @@ export class AgentSession {
|
|
|
835
877
|
appendSystemPrompt,
|
|
836
878
|
selectedTools: validToolNames,
|
|
837
879
|
toolSnippets,
|
|
838
|
-
|
|
880
|
+
toolGuidelines: Object.fromEntries(this._toolPromptGuidelines),
|
|
881
|
+
});
|
|
882
|
+
}
|
|
883
|
+
/**
|
|
884
|
+
* Apply a prompt and tool loadout for the next request. Sets the executable tools and
|
|
885
|
+
* returns a system message patching the prompt sections the model currently has (replayed
|
|
886
|
+
* from `messages`), or undefined when the prompt is unchanged. Tool changes are declared by
|
|
887
|
+
* the agent loop before the request.
|
|
888
|
+
*
|
|
889
|
+
* A forced prompt does not affect the transcript: the structured sections are still diffed
|
|
890
|
+
* and persisted, and the forced text is projected onto the request by
|
|
891
|
+
* {@link _installAgentForcedPromptProjection}.
|
|
892
|
+
*/
|
|
893
|
+
_preparePromptAndToolLoadout(options, messages = this.agent.state.messages) {
|
|
894
|
+
options.selectedTools = [...new Set(options.selectedTools)].filter((name) => this._toolRegistry.has(name));
|
|
895
|
+
this.agent.state.tools = options.selectedTools.flatMap((name) => {
|
|
896
|
+
const tool = this._toolRegistry.get(name);
|
|
897
|
+
return tool ? [tool] : [];
|
|
898
|
+
});
|
|
899
|
+
const sections = diffSystemPromptSections(getCurrentSystemMessage(messages)?.sections ?? {}, buildSystemPromptSections(options));
|
|
900
|
+
return sections ? { role: "system", content: "", sections, timestamp: Date.now() } : undefined;
|
|
901
|
+
}
|
|
902
|
+
/**
|
|
903
|
+
* Send a forced prompt as the provider's leading system prompt without recording it.
|
|
904
|
+
*
|
|
905
|
+
* A `before_agent_start` handler that returns `systemPrompt` needs that exact text at the
|
|
906
|
+
* head of the request; a mid-conversation system message would leave the original prompt
|
|
907
|
+
* in place. The forced text is a rendering of the current prompt, so the transcript keeps
|
|
908
|
+
* its structured sections and the request is projected instead: the system messages
|
|
909
|
+
* collapse into one head holding the forced text and the current tools. Runs after the
|
|
910
|
+
* `context` extension handlers.
|
|
911
|
+
*/
|
|
912
|
+
_installAgentForcedPromptProjection() {
|
|
913
|
+
const previousTransformContext = this.agent.transformContext;
|
|
914
|
+
this.agent.transformContext = async (messages, signal) => {
|
|
915
|
+
const transformed = previousTransformContext ? await previousTransformContext(messages, signal) : messages;
|
|
916
|
+
const forced = this._runSystemPromptOptions?.forceSystemPrompt;
|
|
917
|
+
if (forced === undefined)
|
|
918
|
+
return transformed;
|
|
919
|
+
const current = getCurrentSystemMessage(transformed);
|
|
920
|
+
const head = {
|
|
921
|
+
role: "system",
|
|
922
|
+
content: forced,
|
|
923
|
+
...(current?.toolsAdded ? { toolsAdded: current.toolsAdded } : {}),
|
|
924
|
+
timestamp: current?.timestamp ?? Date.now(),
|
|
925
|
+
};
|
|
926
|
+
return [head, ...transformed.filter((message) => message.role !== "system")];
|
|
839
927
|
};
|
|
840
|
-
|
|
928
|
+
}
|
|
929
|
+
/** Restore the active tool loadout declared by the session transcript, if it declares one. */
|
|
930
|
+
_restoreToolsFromTranscript() {
|
|
931
|
+
const current = getCurrentSystemMessage(this.sessionManager.buildSessionContext().messages);
|
|
932
|
+
if (!current)
|
|
933
|
+
return;
|
|
934
|
+
const toolNames = (current.toolsAdded ?? [])
|
|
935
|
+
.map((tool) => tool.name)
|
|
936
|
+
.filter((name) => this._toolRegistry.has(name));
|
|
937
|
+
this.agent.state.tools = toolNames.flatMap((name) => {
|
|
938
|
+
const registered = this._toolRegistry.get(name);
|
|
939
|
+
return registered ? [registered] : [];
|
|
940
|
+
});
|
|
941
|
+
this._rebuildSystemPrompt(toolNames);
|
|
841
942
|
}
|
|
842
943
|
// =========================================================================
|
|
843
944
|
// Prompting
|
|
844
945
|
// =========================================================================
|
|
845
946
|
async _runAgentPrompt(messages) {
|
|
947
|
+
this._agentRunAbortRequested = false;
|
|
846
948
|
this._isAgentRunActive = true;
|
|
847
949
|
try {
|
|
848
950
|
await this.agent.prompt(messages);
|
|
849
951
|
while (await this._handlePostAgentRun()) {
|
|
952
|
+
if (this._agentRunAbortRequested)
|
|
953
|
+
break;
|
|
850
954
|
await this.agent.continue();
|
|
851
955
|
}
|
|
852
956
|
}
|
|
853
957
|
finally {
|
|
854
|
-
this.
|
|
958
|
+
if (this._agentRunAbortRequested)
|
|
959
|
+
this._finishCancelledRetry();
|
|
960
|
+
this._runSystemPromptOptions = undefined;
|
|
855
961
|
this._flushPendingBashMessages();
|
|
856
962
|
this._flushPendingCustomMessages();
|
|
857
963
|
await this._emitAgentSettled();
|
|
@@ -860,11 +966,21 @@ export class AgentSession {
|
|
|
860
966
|
async _handlePostAgentRun() {
|
|
861
967
|
const msg = this._lastAssistantMessage;
|
|
862
968
|
this._lastAssistantMessage = undefined;
|
|
969
|
+
if (this._agentRunAbortRequested) {
|
|
970
|
+
this._finishCancelledRetry();
|
|
971
|
+
return false;
|
|
972
|
+
}
|
|
863
973
|
if (!msg) {
|
|
864
974
|
return false;
|
|
865
975
|
}
|
|
866
976
|
if (this._isRetryableError(msg) && (await this._prepareRetry(msg))) {
|
|
867
|
-
|
|
977
|
+
if (this._agentRunAbortRequested)
|
|
978
|
+
this._finishCancelledRetry();
|
|
979
|
+
return !this._agentRunAbortRequested;
|
|
980
|
+
}
|
|
981
|
+
if (this._agentRunAbortRequested) {
|
|
982
|
+
this._finishCancelledRetry();
|
|
983
|
+
return false;
|
|
868
984
|
}
|
|
869
985
|
if (msg.stopReason === "error" && this._retryAttempt > 0) {
|
|
870
986
|
this._emit({
|
|
@@ -876,7 +992,7 @@ export class AgentSession {
|
|
|
876
992
|
this._retryAttempt = 0;
|
|
877
993
|
}
|
|
878
994
|
if (await this._checkCompaction(msg)) {
|
|
879
|
-
return
|
|
995
|
+
return !this._agentRunAbortRequested;
|
|
880
996
|
}
|
|
881
997
|
// A length stop means the model hit the output token limit mid-response.
|
|
882
998
|
// The agent loop already re-prompts when truncated tool calls are present;
|
|
@@ -892,7 +1008,20 @@ export class AgentSession {
|
|
|
892
1008
|
}
|
|
893
1009
|
// The agent loop drains both queues before emitting agent_end. Any messages
|
|
894
1010
|
// here were queued by agent_end extension handlers and need a continuation.
|
|
895
|
-
return this.agent.hasQueuedMessages();
|
|
1011
|
+
return !this._agentRunAbortRequested && this.agent.hasQueuedMessages();
|
|
1012
|
+
}
|
|
1013
|
+
async _runInputHandlers(text, images, source, streamingBehavior) {
|
|
1014
|
+
if (!this._extensionRunner.hasHandlers("input")) {
|
|
1015
|
+
return { text, images };
|
|
1016
|
+
}
|
|
1017
|
+
const inputResult = await this._extensionRunner.emitInput(text, images, source, streamingBehavior);
|
|
1018
|
+
if (inputResult.action === "handled") {
|
|
1019
|
+
return undefined;
|
|
1020
|
+
}
|
|
1021
|
+
if (inputResult.action === "transform") {
|
|
1022
|
+
return { text: inputResult.text, images: inputResult.images ?? images };
|
|
1023
|
+
}
|
|
1024
|
+
return { text, images };
|
|
896
1025
|
}
|
|
897
1026
|
/**
|
|
898
1027
|
* Send a prompt to the agent.
|
|
@@ -918,20 +1047,16 @@ export class AgentSession {
|
|
|
918
1047
|
return;
|
|
919
1048
|
}
|
|
920
1049
|
}
|
|
1050
|
+
if (this._compactionAbortController !== undefined) {
|
|
1051
|
+
throw new Error("Cannot submit a prompt while compaction is in progress. Wait for compaction to finish and retry.");
|
|
1052
|
+
}
|
|
921
1053
|
// Emit input event for extension interception (before skill/template expansion)
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
if (inputResult.action === "handled") {
|
|
927
|
-
preflightResult?.(true);
|
|
928
|
-
return;
|
|
929
|
-
}
|
|
930
|
-
if (inputResult.action === "transform") {
|
|
931
|
-
currentText = inputResult.text;
|
|
932
|
-
currentImages = inputResult.images ?? currentImages;
|
|
933
|
-
}
|
|
1054
|
+
const processedInput = await this._runInputHandlers(text, options?.images, options?.source ?? "interactive", this.isStreaming ? options?.streamingBehavior : undefined);
|
|
1055
|
+
if (!processedInput) {
|
|
1056
|
+
preflightResult?.(true);
|
|
1057
|
+
return;
|
|
934
1058
|
}
|
|
1059
|
+
const { text: currentText, images: currentImages } = processedInput;
|
|
935
1060
|
// Expand skill commands (/skill:name args) and prompt templates (/template args)
|
|
936
1061
|
let expandedText = currentText;
|
|
937
1062
|
if (expandPromptTemplates) {
|
|
@@ -1003,30 +1128,25 @@ export class AgentSession {
|
|
|
1003
1128
|
}
|
|
1004
1129
|
this._pendingNextTurnMessages = [];
|
|
1005
1130
|
// Emit before_agent_start extension event
|
|
1006
|
-
const
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
}
|
|
1026
|
-
else {
|
|
1027
|
-
// Ensure we're using the base prompt (in case previous turn had modifications)
|
|
1028
|
-
this._systemPromptOverride = undefined;
|
|
1029
|
-
this.agent.state.systemPrompt = this._baseSystemPrompt;
|
|
1131
|
+
const selectedToolsBefore = this._baseSystemPromptOptions.selectedTools;
|
|
1132
|
+
const result = await this._extensionRunner.emitBeforeAgentStart(expandedText, currentImages, this._baseSystemPromptOptions);
|
|
1133
|
+
// Handlers may edit event.systemPromptOptions.selectedTools or call setActiveTools(),
|
|
1134
|
+
// which updates the live loadout instead. An explicit edit wins; otherwise the live
|
|
1135
|
+
// loadout is authoritative, so a setActiveTools() call is not undone here.
|
|
1136
|
+
const handlerEditedTools = result.systemPromptOptions.selectedTools.length !== selectedToolsBefore.length ||
|
|
1137
|
+
result.systemPromptOptions.selectedTools.some((name, index) => name !== selectedToolsBefore[index]);
|
|
1138
|
+
if (!handlerEditedTools)
|
|
1139
|
+
result.systemPromptOptions.selectedTools = this.getActiveToolNames();
|
|
1140
|
+
for (const msg of result.messages) {
|
|
1141
|
+
messages.push({
|
|
1142
|
+
role: "custom",
|
|
1143
|
+
customType: msg.customType,
|
|
1144
|
+
// Untyped extensions can pass null/missing content; normalize at ingestion.
|
|
1145
|
+
content: msg.content ?? [],
|
|
1146
|
+
display: msg.display,
|
|
1147
|
+
details: msg.details,
|
|
1148
|
+
timestamp: Date.now(),
|
|
1149
|
+
});
|
|
1030
1150
|
}
|
|
1031
1151
|
// Another prompt may have started while this one was preflighting (auth
|
|
1032
1152
|
// checks, compaction, and extension events can take seconds). The agent is
|
|
@@ -1043,6 +1163,10 @@ export class AgentSession {
|
|
|
1043
1163
|
preflightResult?.(true);
|
|
1044
1164
|
return;
|
|
1045
1165
|
}
|
|
1166
|
+
const updateMessage = this._preparePromptAndToolLoadout(result.systemPromptOptions);
|
|
1167
|
+
this._runSystemPromptOptions = result.systemPromptOptions;
|
|
1168
|
+
if (updateMessage)
|
|
1169
|
+
messages.unshift(updateMessage);
|
|
1046
1170
|
}
|
|
1047
1171
|
catch (error) {
|
|
1048
1172
|
preflightResult?.(false);
|
|
@@ -1111,40 +1235,44 @@ export class AgentSession {
|
|
|
1111
1235
|
return text; // Return original on error
|
|
1112
1236
|
}
|
|
1113
1237
|
}
|
|
1238
|
+
async _queueUserInput(text, images, behavior, source) {
|
|
1239
|
+
if (text.startsWith("/")) {
|
|
1240
|
+
this._throwIfExtensionCommand(text);
|
|
1241
|
+
}
|
|
1242
|
+
const processedInput = await this._runInputHandlers(text, images, source, this.isStreaming ? behavior : undefined);
|
|
1243
|
+
if (!processedInput)
|
|
1244
|
+
return;
|
|
1245
|
+
let expandedText = this._expandSkillCommand(processedInput.text);
|
|
1246
|
+
expandedText = expandPromptTemplate(expandedText, [...this.promptTemplates]);
|
|
1247
|
+
if (behavior === "steer") {
|
|
1248
|
+
await this._queueSteer(expandedText, processedInput.images);
|
|
1249
|
+
}
|
|
1250
|
+
else {
|
|
1251
|
+
await this._queueFollowUp(expandedText, processedInput.images);
|
|
1252
|
+
}
|
|
1253
|
+
}
|
|
1114
1254
|
/**
|
|
1115
1255
|
* Queue a steering message while the agent is running.
|
|
1116
1256
|
* Delivered after the current assistant turn finishes executing its tool calls,
|
|
1117
1257
|
* before the next LLM call.
|
|
1118
1258
|
* Expands skill commands and prompt templates. Errors on extension commands.
|
|
1119
1259
|
* @param images Optional image attachments to include with the message
|
|
1260
|
+
* @param options Input source; defaults to interactive
|
|
1120
1261
|
* @throws Error if text is an extension command
|
|
1121
1262
|
*/
|
|
1122
|
-
async steer(text, images) {
|
|
1123
|
-
|
|
1124
|
-
if (text.startsWith("/")) {
|
|
1125
|
-
this._throwIfExtensionCommand(text);
|
|
1126
|
-
}
|
|
1127
|
-
// Expand skill commands and prompt templates
|
|
1128
|
-
let expandedText = this._expandSkillCommand(text);
|
|
1129
|
-
expandedText = expandPromptTemplate(expandedText, [...this.promptTemplates]);
|
|
1130
|
-
await this._queueSteer(expandedText, images);
|
|
1263
|
+
async steer(text, images, options) {
|
|
1264
|
+
await this._queueUserInput(text, images, "steer", options?.source ?? "interactive");
|
|
1131
1265
|
}
|
|
1132
1266
|
/**
|
|
1133
1267
|
* Queue a follow-up message to be processed after the agent finishes.
|
|
1134
1268
|
* Delivered only when agent has no more tool calls or steering messages.
|
|
1135
1269
|
* Expands skill commands and prompt templates. Errors on extension commands.
|
|
1136
1270
|
* @param images Optional image attachments to include with the message
|
|
1271
|
+
* @param options Input source; defaults to interactive
|
|
1137
1272
|
* @throws Error if text is an extension command
|
|
1138
1273
|
*/
|
|
1139
|
-
async followUp(text, images) {
|
|
1140
|
-
|
|
1141
|
-
if (text.startsWith("/")) {
|
|
1142
|
-
this._throwIfExtensionCommand(text);
|
|
1143
|
-
}
|
|
1144
|
-
// Expand skill commands and prompt templates
|
|
1145
|
-
let expandedText = this._expandSkillCommand(text);
|
|
1146
|
-
expandedText = expandPromptTemplate(expandedText, [...this.promptTemplates]);
|
|
1147
|
-
await this._queueFollowUp(expandedText, images);
|
|
1274
|
+
async followUp(text, images, options) {
|
|
1275
|
+
await this._queueUserInput(text, images, "followUp", options?.source ?? "interactive");
|
|
1148
1276
|
}
|
|
1149
1277
|
/**
|
|
1150
1278
|
* Internal: Queue a steering message (already expanded, no extension command check).
|
|
@@ -1488,6 +1616,9 @@ export class AgentSession {
|
|
|
1488
1616
|
* Abort current operation and wait for agent to become idle.
|
|
1489
1617
|
*/
|
|
1490
1618
|
async abort() {
|
|
1619
|
+
if (this._isAgentRunActive) {
|
|
1620
|
+
this._agentRunAbortRequested = true;
|
|
1621
|
+
}
|
|
1491
1622
|
this.abortRetry();
|
|
1492
1623
|
this.abortCompaction();
|
|
1493
1624
|
this.abortBranchSummary();
|
|
@@ -1710,6 +1841,10 @@ export class AgentSession {
|
|
|
1710
1841
|
// =========================================================================
|
|
1711
1842
|
// Compaction
|
|
1712
1843
|
// =========================================================================
|
|
1844
|
+
/** Generate the built-in compaction summary for manual and automatic compaction. */
|
|
1845
|
+
async _runDefaultCompaction(preparation, requestModel, apiKey, headers, customInstructions, signal, env, reason) {
|
|
1846
|
+
return compact(preparation, requestModel, apiKey, headers, customInstructions, signal, this.thinkingLevel, this.agent.streamFunction, env, this.settingsManager.getRetrySettings(), this._summarizationRetryCallbacks({ source: "compaction", reason }), undefined);
|
|
1847
|
+
}
|
|
1713
1848
|
_clearManualCompactionState() {
|
|
1714
1849
|
this._compactionAbortController = undefined;
|
|
1715
1850
|
this._resolveIdleWaitIfIdle();
|
|
@@ -1724,13 +1859,16 @@ export class AgentSession {
|
|
|
1724
1859
|
await this.abort();
|
|
1725
1860
|
this._compactionAbortController = new AbortController();
|
|
1726
1861
|
this._emit({ type: "compaction_start", reason: "manual" });
|
|
1862
|
+
let fromExtension = false;
|
|
1863
|
+
let cancelledByExtension = false;
|
|
1727
1864
|
try {
|
|
1728
|
-
|
|
1865
|
+
const model = this.model;
|
|
1866
|
+
if (!model) {
|
|
1729
1867
|
throw new Error(formatNoModelSelectedMessage());
|
|
1730
1868
|
}
|
|
1731
|
-
const
|
|
1869
|
+
const settings = this.settingsManager.getCompactionSettings(model);
|
|
1870
|
+
const { model: requestModel, apiKey, headers, env, } = await this._getSummarizationRequestAuth(model, this._compactionAbortController.signal);
|
|
1732
1871
|
const pathEntries = this.sessionManager.getBranch();
|
|
1733
|
-
const settings = this.settingsManager.getCompactionSettings();
|
|
1734
1872
|
const preparation = prepareCompaction(pathEntries, settings);
|
|
1735
1873
|
if (!preparation) {
|
|
1736
1874
|
// Check why we can't compact
|
|
@@ -1753,6 +1891,7 @@ export class AgentSession {
|
|
|
1753
1891
|
signal: this._compactionAbortController.signal,
|
|
1754
1892
|
}));
|
|
1755
1893
|
if (result?.cancel) {
|
|
1894
|
+
cancelledByExtension = true;
|
|
1756
1895
|
throw new Error("Compaction cancelled");
|
|
1757
1896
|
}
|
|
1758
1897
|
if (result?.compaction) {
|
|
@@ -1774,8 +1913,8 @@ export class AgentSession {
|
|
|
1774
1913
|
details = extensionCompaction.details;
|
|
1775
1914
|
}
|
|
1776
1915
|
else {
|
|
1777
|
-
//
|
|
1778
|
-
const result = await
|
|
1916
|
+
// Shared default summary generator, also used by automatic compaction.
|
|
1917
|
+
const result = await this._runDefaultCompaction(preparation, requestModel, apiKey, headers, customInstructions, this._compactionAbortController.signal, env, "manual");
|
|
1779
1918
|
summary = result.summary;
|
|
1780
1919
|
firstKeptEntryId = result.firstKeptEntryId;
|
|
1781
1920
|
tokensBefore = result.tokensBefore;
|
|
@@ -1822,7 +1961,7 @@ export class AgentSession {
|
|
|
1822
1961
|
}
|
|
1823
1962
|
catch (error) {
|
|
1824
1963
|
const message = error instanceof Error ? error.message : String(error);
|
|
1825
|
-
const aborted =
|
|
1964
|
+
const aborted = this._compactionAbortController.signal.aborted || cancelledByExtension;
|
|
1826
1965
|
const errorMessage = aborted ? undefined : `Compaction failed: ${message}`;
|
|
1827
1966
|
this._clearManualCompactionState();
|
|
1828
1967
|
this._emit({
|
|
@@ -1872,7 +2011,7 @@ export class AgentSession {
|
|
|
1872
2011
|
* @param skipAbortedCheck If false, include aborted messages (for pre-prompt check). Default: true
|
|
1873
2012
|
*/
|
|
1874
2013
|
async _checkCompaction(assistantMessage, skipAbortedCheck = true) {
|
|
1875
|
-
const settings = this.settingsManager.getCompactionSettings();
|
|
2014
|
+
const settings = this.settingsManager.getCompactionSettings(this.model);
|
|
1876
2015
|
if (!settings.enabled)
|
|
1877
2016
|
return false;
|
|
1878
2017
|
// Skip if message was aborted (user cancelled) - unless skipAbortedCheck is false
|
|
@@ -1892,11 +2031,12 @@ export class AgentSession {
|
|
|
1892
2031
|
if (assistantIsFromBeforeCompaction) {
|
|
1893
2032
|
return false;
|
|
1894
2033
|
}
|
|
1895
|
-
//
|
|
1896
|
-
//
|
|
1897
|
-
//
|
|
1898
|
-
|
|
1899
|
-
|
|
2034
|
+
// Automatic cases 1 and 2: context overflow.
|
|
2035
|
+
// A length stop is recoverable when output ended below the model's original desired limit,
|
|
2036
|
+
// independent of the configured context size or any context-clamped provider request limit.
|
|
2037
|
+
const contextOverflow = sameModel && isContextOverflow(assistantMessage, contextWindow);
|
|
2038
|
+
const recoverableLength = sameModel && isRecoverableLength(assistantMessage, this.model?.maxTokens ?? 0);
|
|
2039
|
+
if (contextOverflow || recoverableLength) {
|
|
1900
2040
|
const willRetry = assistantMessage.stopReason !== "stop";
|
|
1901
2041
|
if (!willRetry) {
|
|
1902
2042
|
return await this._runAutoCompaction("overflow", false);
|
|
@@ -1908,7 +2048,9 @@ export class AgentSession {
|
|
|
1908
2048
|
result: undefined,
|
|
1909
2049
|
aborted: false,
|
|
1910
2050
|
willRetry: false,
|
|
1911
|
-
errorMessage:
|
|
2051
|
+
errorMessage: contextOverflow
|
|
2052
|
+
? "Context overflow recovery failed after one compact-and-retry attempt. Try reducing context or switching to a larger-context model."
|
|
2053
|
+
: "Truncated response recovery failed after one compact-and-retry attempt.",
|
|
1912
2054
|
});
|
|
1913
2055
|
return false;
|
|
1914
2056
|
}
|
|
@@ -1955,30 +2097,28 @@ export class AgentSession {
|
|
|
1955
2097
|
* Internal: Run auto-compaction with events.
|
|
1956
2098
|
*/
|
|
1957
2099
|
async _runAutoCompaction(reason, willRetry) {
|
|
1958
|
-
const
|
|
2100
|
+
const model = this.model;
|
|
2101
|
+
const settings = this.settingsManager.getCompactionSettings(model);
|
|
2102
|
+
let abortController;
|
|
1959
2103
|
let started = false;
|
|
1960
2104
|
let fromExtension = false;
|
|
2105
|
+
let cancelledByExtension = false;
|
|
1961
2106
|
try {
|
|
1962
|
-
if (!
|
|
2107
|
+
if (!model) {
|
|
1963
2108
|
return false;
|
|
1964
2109
|
}
|
|
1965
|
-
let apiKey;
|
|
1966
|
-
let headers;
|
|
1967
|
-
let env;
|
|
1968
|
-
if (this.agent.streamFunction === streamSimple) {
|
|
1969
|
-
({ apiKey, headers, env } = await this._getRequiredRequestAuth(this.model));
|
|
1970
|
-
}
|
|
1971
|
-
else {
|
|
1972
|
-
({ apiKey, headers, env } = await this._getSummarizationRequestAuth(this.model));
|
|
1973
|
-
}
|
|
1974
2110
|
const pathEntries = this.sessionManager.getBranch();
|
|
1975
2111
|
const preparation = prepareCompaction(pathEntries, settings);
|
|
1976
2112
|
if (!preparation) {
|
|
1977
2113
|
return false;
|
|
1978
2114
|
}
|
|
1979
|
-
|
|
1980
|
-
this._autoCompactionAbortController =
|
|
2115
|
+
abortController = new AbortController();
|
|
2116
|
+
this._autoCompactionAbortController = abortController;
|
|
1981
2117
|
started = true;
|
|
2118
|
+
this._emit({ type: "compaction_start", reason });
|
|
2119
|
+
abortController.signal.throwIfAborted();
|
|
2120
|
+
const { model: requestModel, apiKey, headers, env, } = await this._getSummarizationRequestAuth(model, abortController.signal);
|
|
2121
|
+
abortController.signal.throwIfAborted();
|
|
1982
2122
|
let extensionCompaction;
|
|
1983
2123
|
if (this._extensionRunner.hasHandlers("session_before_compact")) {
|
|
1984
2124
|
const extensionResult = (await this._extensionRunner.emit({
|
|
@@ -1988,24 +2128,18 @@ export class AgentSession {
|
|
|
1988
2128
|
customInstructions: undefined,
|
|
1989
2129
|
reason,
|
|
1990
2130
|
willRetry,
|
|
1991
|
-
signal:
|
|
2131
|
+
signal: abortController.signal,
|
|
1992
2132
|
}));
|
|
1993
2133
|
if (extensionResult?.cancel) {
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
reason,
|
|
1997
|
-
result: undefined,
|
|
1998
|
-
aborted: true,
|
|
1999
|
-
willRetry: false,
|
|
2000
|
-
});
|
|
2001
|
-
await this._emitSessionCompactFailed({ reason, aborted: true, willRetry: false, fromExtension: false });
|
|
2002
|
-
return false;
|
|
2134
|
+
cancelledByExtension = true;
|
|
2135
|
+
throw new Error("Compaction cancelled");
|
|
2003
2136
|
}
|
|
2004
2137
|
if (extensionResult?.compaction) {
|
|
2005
2138
|
extensionCompaction = extensionResult.compaction;
|
|
2006
2139
|
fromExtension = true;
|
|
2007
2140
|
}
|
|
2008
2141
|
}
|
|
2142
|
+
abortController.signal.throwIfAborted();
|
|
2009
2143
|
let summary;
|
|
2010
2144
|
let firstKeptEntryId;
|
|
2011
2145
|
let tokensBefore;
|
|
@@ -2020,25 +2154,15 @@ export class AgentSession {
|
|
|
2020
2154
|
details = extensionCompaction.details;
|
|
2021
2155
|
}
|
|
2022
2156
|
else {
|
|
2023
|
-
//
|
|
2024
|
-
const compactResult = await
|
|
2157
|
+
// Shared default summary generator, also used by manual compaction.
|
|
2158
|
+
const compactResult = await this._runDefaultCompaction(preparation, requestModel, apiKey, headers, undefined, abortController.signal, env, reason);
|
|
2025
2159
|
summary = compactResult.summary;
|
|
2026
2160
|
firstKeptEntryId = compactResult.firstKeptEntryId;
|
|
2027
2161
|
tokensBefore = compactResult.tokensBefore;
|
|
2028
2162
|
usage = compactResult.usage;
|
|
2029
2163
|
details = compactResult.details;
|
|
2030
2164
|
}
|
|
2031
|
-
|
|
2032
|
-
this._emit({
|
|
2033
|
-
type: "compaction_end",
|
|
2034
|
-
reason,
|
|
2035
|
-
result: undefined,
|
|
2036
|
-
aborted: true,
|
|
2037
|
-
willRetry: false,
|
|
2038
|
-
});
|
|
2039
|
-
await this._emitSessionCompactFailed({ reason, aborted: true, willRetry: false, fromExtension });
|
|
2040
|
-
return false;
|
|
2041
|
-
}
|
|
2165
|
+
abortController.signal.throwIfAborted();
|
|
2042
2166
|
this.sessionManager.appendCompaction(summary, firstKeptEntryId, tokensBefore, details, fromExtension, usage);
|
|
2043
2167
|
const newEntries = this.sessionManager.getEntries();
|
|
2044
2168
|
const sessionContext = this.sessionManager.buildSessionContext();
|
|
@@ -2067,7 +2191,11 @@ export class AgentSession {
|
|
|
2067
2191
|
if (willRetry) {
|
|
2068
2192
|
const messages = this.agent.state.messages;
|
|
2069
2193
|
const lastMsg = messages[messages.length - 1];
|
|
2070
|
-
|
|
2194
|
+
// The overflow response was persisted on message_end before _checkCompaction() removed it
|
|
2195
|
+
// from agent state. Rebuilding state from the new compaction can restore that kept entry,
|
|
2196
|
+
// leaving an assistant as the final message. agent.continue() rejects that state, so remove
|
|
2197
|
+
// the retriable error or truncated-length response again before continuing the interrupted turn.
|
|
2198
|
+
if (lastMsg?.role === "assistant" && (lastMsg.stopReason === "error" || lastMsg.stopReason === "length")) {
|
|
2071
2199
|
this.agent.state.messages = messages.slice(0, -1);
|
|
2072
2200
|
}
|
|
2073
2201
|
return true;
|
|
@@ -2077,23 +2205,26 @@ export class AgentSession {
|
|
|
2077
2205
|
return this.agent.hasQueuedMessages();
|
|
2078
2206
|
}
|
|
2079
2207
|
catch (error) {
|
|
2080
|
-
const
|
|
2208
|
+
const message = error instanceof Error ? error.message : "compaction failed";
|
|
2209
|
+
const aborted = abortController?.signal.aborted === true || cancelledByExtension;
|
|
2081
2210
|
if (started) {
|
|
2082
|
-
const
|
|
2083
|
-
?
|
|
2084
|
-
:
|
|
2211
|
+
const errorMessage = aborted
|
|
2212
|
+
? undefined
|
|
2213
|
+
: reason === "overflow"
|
|
2214
|
+
? `Context overflow recovery failed: ${message}`
|
|
2215
|
+
: `Auto-compaction failed: ${message}`;
|
|
2085
2216
|
this._emit({
|
|
2086
2217
|
type: "compaction_end",
|
|
2087
2218
|
reason,
|
|
2088
2219
|
result: undefined,
|
|
2089
|
-
aborted
|
|
2220
|
+
aborted,
|
|
2090
2221
|
willRetry: false,
|
|
2091
|
-
errorMessage
|
|
2222
|
+
errorMessage,
|
|
2092
2223
|
});
|
|
2093
2224
|
await this._emitSessionCompactFailed({
|
|
2094
2225
|
reason,
|
|
2095
|
-
errorMessage
|
|
2096
|
-
aborted
|
|
2226
|
+
errorMessage,
|
|
2227
|
+
aborted,
|
|
2097
2228
|
willRetry: false,
|
|
2098
2229
|
fromExtension,
|
|
2099
2230
|
});
|
|
@@ -2101,7 +2232,9 @@ export class AgentSession {
|
|
|
2101
2232
|
return false;
|
|
2102
2233
|
}
|
|
2103
2234
|
finally {
|
|
2104
|
-
this._autoCompactionAbortController
|
|
2235
|
+
if (this._autoCompactionAbortController === abortController) {
|
|
2236
|
+
this._autoCompactionAbortController = undefined;
|
|
2237
|
+
}
|
|
2105
2238
|
this._resolveIdleWaitIfIdle();
|
|
2106
2239
|
}
|
|
2107
2240
|
}
|
|
@@ -2158,8 +2291,7 @@ export class AgentSession {
|
|
|
2158
2291
|
themePaths: this.buildExtensionResourcePaths(themePaths),
|
|
2159
2292
|
};
|
|
2160
2293
|
this._resourceLoader.extendResources(extensionPaths);
|
|
2161
|
-
this.
|
|
2162
|
-
this.agent.state.systemPrompt = this._baseSystemPrompt;
|
|
2294
|
+
this._rebuildSystemPrompt(this.getActiveToolNames());
|
|
2163
2295
|
}
|
|
2164
2296
|
buildExtensionResourcePaths(entries) {
|
|
2165
2297
|
return entries.map((entry) => {
|
|
@@ -2276,8 +2408,7 @@ export class AgentSession {
|
|
|
2276
2408
|
}),
|
|
2277
2409
|
setSkillsIndexFilter: (filter) => {
|
|
2278
2410
|
this._resourceLoader.setSkillsIndexFilter(filter);
|
|
2279
|
-
this.
|
|
2280
|
-
this.agent.state.systemPrompt = this._systemPromptOverride ?? this._baseSystemPrompt;
|
|
2411
|
+
this._rebuildSystemPrompt(this.getActiveToolNames());
|
|
2281
2412
|
},
|
|
2282
2413
|
refreshTools: () => this._refreshToolRegistry(),
|
|
2283
2414
|
getCommands,
|
|
@@ -2511,6 +2642,18 @@ export class AgentSession {
|
|
|
2511
2642
|
},
|
|
2512
2643
|
};
|
|
2513
2644
|
}
|
|
2645
|
+
_finishCancelledRetry() {
|
|
2646
|
+
if (this._retryAttempt === 0)
|
|
2647
|
+
return;
|
|
2648
|
+
const attempt = this._retryAttempt;
|
|
2649
|
+
this._retryAttempt = 0;
|
|
2650
|
+
this._emit({
|
|
2651
|
+
type: "auto_retry_end",
|
|
2652
|
+
success: false,
|
|
2653
|
+
attempt,
|
|
2654
|
+
finalError: "Retry cancelled",
|
|
2655
|
+
});
|
|
2656
|
+
}
|
|
2514
2657
|
/**
|
|
2515
2658
|
* Prepare a retryable error for continuation with exponential backoff.
|
|
2516
2659
|
* @returns true if the caller should continue the agent, false otherwise
|
|
@@ -2526,7 +2669,7 @@ export class AgentSession {
|
|
|
2526
2669
|
this._retryAttempt--;
|
|
2527
2670
|
return false;
|
|
2528
2671
|
}
|
|
2529
|
-
const delayMs = settings
|
|
2672
|
+
const delayMs = retryDelayMs(settings, this._retryAttempt);
|
|
2530
2673
|
this._emit({
|
|
2531
2674
|
type: "auto_retry_start",
|
|
2532
2675
|
attempt: this._retryAttempt,
|
|
@@ -2546,14 +2689,7 @@ export class AgentSession {
|
|
|
2546
2689
|
}
|
|
2547
2690
|
catch {
|
|
2548
2691
|
// Aborted during sleep - emit end event so UI can clean up
|
|
2549
|
-
|
|
2550
|
-
this._retryAttempt = 0;
|
|
2551
|
-
this._emit({
|
|
2552
|
-
type: "auto_retry_end",
|
|
2553
|
-
success: false,
|
|
2554
|
-
attempt,
|
|
2555
|
-
finalError: "Retry cancelled",
|
|
2556
|
-
});
|
|
2692
|
+
this._finishCancelledRetry();
|
|
2557
2693
|
return false;
|
|
2558
2694
|
}
|
|
2559
2695
|
finally {
|
|
@@ -2704,6 +2840,9 @@ export class AgentSession {
|
|
|
2704
2840
|
if (this.isStreaming) {
|
|
2705
2841
|
throw new Error("Wait for the current response to finish before navigating the session tree.");
|
|
2706
2842
|
}
|
|
2843
|
+
if (this.isCompacting) {
|
|
2844
|
+
throw new Error("Wait for the current compaction or tree navigation to finish before navigating the session tree.");
|
|
2845
|
+
}
|
|
2707
2846
|
const oldLeafId = this.sessionManager.getLeafId();
|
|
2708
2847
|
// No-op if already at target
|
|
2709
2848
|
if (targetId === oldLeafId) {
|
|
@@ -2846,6 +2985,7 @@ export class AgentSession {
|
|
|
2846
2985
|
// Update agent state
|
|
2847
2986
|
const sessionContext = this.sessionManager.buildSessionContext();
|
|
2848
2987
|
this.agent.state.messages = sessionContext.messages;
|
|
2988
|
+
this._restoreToolsFromTranscript();
|
|
2849
2989
|
// Emit session_tree event (extension runners + RPC stdout subscribers)
|
|
2850
2990
|
const treeEvent = {
|
|
2851
2991
|
type: "session_tree",
|
|
@@ -2895,7 +3035,10 @@ export class AgentSession {
|
|
|
2895
3035
|
let toolCalls = 0;
|
|
2896
3036
|
const usageTotals = createUsageTotals();
|
|
2897
3037
|
for (const entry of this.sessionManager.getEntries()) {
|
|
2898
|
-
if (
|
|
3038
|
+
if (entry.type === "usage") {
|
|
3039
|
+
addUsageToTotals(usageTotals, entry.usage);
|
|
3040
|
+
}
|
|
3041
|
+
else if ((entry.type === "branch_summary" || entry.type === "compaction") && entry.usage) {
|
|
2899
3042
|
addUsageToTotals(usageTotals, entry.usage);
|
|
2900
3043
|
}
|
|
2901
3044
|
if (entry.type !== "message")
|
|
@@ -3030,6 +3173,30 @@ export class AgentSession {
|
|
|
3030
3173
|
writeFileSync(filePath, `${lines.join("\n")}\n`);
|
|
3031
3174
|
return filePath;
|
|
3032
3175
|
}
|
|
3176
|
+
/**
|
|
3177
|
+
* Ask the current model to describe what went wrong in this session for a bug report.
|
|
3178
|
+
* Used when the user declines to share the transcript itself.
|
|
3179
|
+
*/
|
|
3180
|
+
async summarizeForBugReport(options) {
|
|
3181
|
+
const model = this.model;
|
|
3182
|
+
if (!model) {
|
|
3183
|
+
throw new Error("No model selected");
|
|
3184
|
+
}
|
|
3185
|
+
const { model: requestModel, apiKey, headers, env } = await this._getSummarizationRequestAuth(model);
|
|
3186
|
+
return generateBugReportSummary({
|
|
3187
|
+
messages: this.messages,
|
|
3188
|
+
hint: options.hint,
|
|
3189
|
+
model: requestModel,
|
|
3190
|
+
apiKey,
|
|
3191
|
+
headers,
|
|
3192
|
+
env,
|
|
3193
|
+
signal: options.signal,
|
|
3194
|
+
thinkingLevel: this.thinkingLevel,
|
|
3195
|
+
streamFn: this.agent.streamFunction,
|
|
3196
|
+
retry: this.settingsManager.getRetrySettings(),
|
|
3197
|
+
sessionId: this.sessionId,
|
|
3198
|
+
});
|
|
3199
|
+
}
|
|
3033
3200
|
// =========================================================================
|
|
3034
3201
|
// Utilities
|
|
3035
3202
|
// =========================================================================
|