@selesai/code 0.13.25 → 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 +85 -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 +14 -4
- 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 +339 -169
- 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/handoff.d.ts +1 -0
- package/dist/core/handoff.js +1 -0
- 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/defaults/models.json +2 -72
- package/dist/extensions/handoff-new.test.ts +13 -13
- package/dist/extensions/handoff-new.ts +17 -6
- 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 +7 -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,17 +23,19 @@ 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";
|
|
29
30
|
import { createToolHtmlRenderer } from "./export-html/tool-renderer.js";
|
|
31
|
+
import { AUTO_HANDOFF_GOAL } from "./handoff.js";
|
|
30
32
|
import { ExtensionRunner, wrapRegisteredTools, } from "./extensions/index.js";
|
|
31
33
|
import { emitSessionShutdownEvent } from "./extensions/runner.js";
|
|
32
34
|
import { ModelRegistry } from "./model-registry.js";
|
|
33
35
|
import { expandPromptTemplate } from "./prompt-templates.js";
|
|
34
36
|
import { CURRENT_SESSION_VERSION, getLatestCompactionEntry } from "./session-manager.js";
|
|
35
37
|
import { createSyntheticSourceInfo } from "./source-info.js";
|
|
36
|
-
import { buildSystemPrompt } from "./system-prompt.js";
|
|
38
|
+
import { buildSystemPrompt, buildSystemPromptSections, diffSystemPromptSections, normalizeBuildSystemPromptOptions, } from "./system-prompt.js";
|
|
37
39
|
import { createLocalBashOperations } from "./tools/bash.js";
|
|
38
40
|
import { createAllToolDefinitions } from "./tools/index.js";
|
|
39
41
|
import { createToolDefinitionFromAgentTool } from "./tools/tool-definition-wrapper.js";
|
|
@@ -98,6 +100,7 @@ export class AgentSession {
|
|
|
98
100
|
_unsubscribeAgent;
|
|
99
101
|
_eventListeners = [];
|
|
100
102
|
_isAgentRunActive = false;
|
|
103
|
+
_agentRunAbortRequested = false;
|
|
101
104
|
_idleWaitPromise;
|
|
102
105
|
_resolveIdleWait;
|
|
103
106
|
/** Tracks pending steering messages for UI display. Removed when delivered. */
|
|
@@ -143,15 +146,15 @@ export class AgentSession {
|
|
|
143
146
|
_extensionErrorListener;
|
|
144
147
|
_extensionErrorUnsubscriber;
|
|
145
148
|
_modelRuntime;
|
|
149
|
+
_cacheWarmer;
|
|
146
150
|
// Tool registry for extension getTools/setTools
|
|
147
151
|
_toolRegistry = new Map();
|
|
148
152
|
_toolDefinitions = new Map();
|
|
149
153
|
_toolPromptSnippets = new Map();
|
|
150
154
|
_toolPromptGuidelines = new Map();
|
|
151
|
-
// Base system prompt (without extension appends) - used to apply fresh appends each turn
|
|
152
|
-
_baseSystemPrompt = "";
|
|
153
155
|
_baseSystemPromptOptions;
|
|
154
|
-
|
|
156
|
+
/** Prompt options after before_agent_start mutations for the active run. */
|
|
157
|
+
_runSystemPromptOptions;
|
|
155
158
|
constructor(config) {
|
|
156
159
|
this.agent = config.agent;
|
|
157
160
|
this.sessionManager = config.sessionManager;
|
|
@@ -161,6 +164,10 @@ export class AgentSession {
|
|
|
161
164
|
this._customTools = config.customTools ?? [];
|
|
162
165
|
this._cwd = config.cwd;
|
|
163
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
|
+
}
|
|
164
171
|
this._extensionRunnerRef = config.extensionRunnerRef;
|
|
165
172
|
this._initialActiveToolNames = config.initialActiveToolNames;
|
|
166
173
|
this._allowedToolNames = config.allowedToolNames ? new Set(config.allowedToolNames) : undefined;
|
|
@@ -172,18 +179,21 @@ export class AgentSession {
|
|
|
172
179
|
this._unsubscribeAgent = this.agent.subscribe(this._handleAgentEvent);
|
|
173
180
|
this._installAgentToolHooks();
|
|
174
181
|
this._installAgentNextTurnRefresh();
|
|
182
|
+
this._installAgentForcedPromptProjection();
|
|
175
183
|
this._buildRuntime({
|
|
176
184
|
activeToolNames: this._initialActiveToolNames,
|
|
177
185
|
includeAllExtensionTools: true,
|
|
178
186
|
});
|
|
187
|
+
if (this._initialActiveToolNames === undefined)
|
|
188
|
+
this._restoreToolsFromTranscript();
|
|
179
189
|
}
|
|
180
190
|
get modelRuntime() {
|
|
181
191
|
return this._modelRuntime;
|
|
182
192
|
}
|
|
183
|
-
async _getRequiredRequestAuth(model) {
|
|
193
|
+
async _getRequiredRequestAuth(model, signal) {
|
|
184
194
|
let result;
|
|
185
195
|
try {
|
|
186
|
-
result = await this._modelRuntime.getAuth(model);
|
|
196
|
+
result = await this._modelRuntime.getAuth(model, { signal });
|
|
187
197
|
}
|
|
188
198
|
catch (error) {
|
|
189
199
|
const cause = error instanceof Error ? error.cause : undefined;
|
|
@@ -193,7 +203,9 @@ export class AgentSession {
|
|
|
193
203
|
throw error;
|
|
194
204
|
}
|
|
195
205
|
if (result && (result.auth.apiKey || result.auth.headers)) {
|
|
206
|
+
const requestModel = result.auth.baseUrl ? { ...model, baseUrl: result.auth.baseUrl } : model;
|
|
196
207
|
return {
|
|
208
|
+
model: requestModel,
|
|
197
209
|
apiKey: result.auth.apiKey,
|
|
198
210
|
headers: withoutDeletedHeaders(result.auth.headers),
|
|
199
211
|
env: result.env,
|
|
@@ -207,18 +219,26 @@ export class AgentSession {
|
|
|
207
219
|
}
|
|
208
220
|
throw new Error(formatNoApiKeyFoundMessage(model.provider));
|
|
209
221
|
}
|
|
210
|
-
async _getSummarizationRequestAuth(model) {
|
|
222
|
+
async _getSummarizationRequestAuth(model, signal) {
|
|
211
223
|
if (this.agent.streamFunction === streamSimple) {
|
|
212
|
-
return this._getRequiredRequestAuth(model);
|
|
224
|
+
return this._getRequiredRequestAuth(model, signal);
|
|
213
225
|
}
|
|
214
226
|
try {
|
|
215
|
-
const result = await this._modelRuntime.getAuth(model);
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
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
|
+
};
|
|
219
237
|
}
|
|
220
|
-
catch {
|
|
221
|
-
|
|
238
|
+
catch (error) {
|
|
239
|
+
if (signal?.aborted)
|
|
240
|
+
throw error;
|
|
241
|
+
return { model };
|
|
222
242
|
}
|
|
223
243
|
}
|
|
224
244
|
/**
|
|
@@ -279,7 +299,7 @@ export class AgentSession {
|
|
|
279
299
|
}
|
|
280
300
|
async _compactBeforeNextAssistantResponse(context) {
|
|
281
301
|
const model = this.model;
|
|
282
|
-
const settings = this.settingsManager.getCompactionSettings();
|
|
302
|
+
const settings = this.settingsManager.getCompactionSettings(model);
|
|
283
303
|
if (!model ||
|
|
284
304
|
model.contextWindow <= 0 ||
|
|
285
305
|
!shouldCompact(estimateContextTokens(context.messages).tokens, model.contextWindow, settings)) {
|
|
@@ -300,13 +320,25 @@ export class AgentSession {
|
|
|
300
320
|
const context = await this._compactBeforeNextAssistantResponse(turn.context);
|
|
301
321
|
const previousSnapshot = await previousPrepareNextTurnWithContext?.({ ...turn, context }, signal);
|
|
302
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;
|
|
303
333
|
return {
|
|
304
334
|
...previousSnapshot,
|
|
305
335
|
context: {
|
|
306
336
|
...nextContext,
|
|
307
|
-
systemPrompt: this._systemPromptOverride ?? this._baseSystemPrompt,
|
|
308
337
|
tools: this.agent.state.tools.slice(),
|
|
309
338
|
},
|
|
339
|
+
messages: updateMessage
|
|
340
|
+
? [...(previousSnapshot?.messages ?? []), updateMessage]
|
|
341
|
+
: previousSnapshot?.messages,
|
|
310
342
|
model: this.agent.state.model,
|
|
311
343
|
thinkingLevel: this.agent.state.thinkingLevel,
|
|
312
344
|
};
|
|
@@ -351,6 +383,7 @@ export class AgentSession {
|
|
|
351
383
|
resolve();
|
|
352
384
|
}
|
|
353
385
|
async _emitAgentSettled() {
|
|
386
|
+
this._cacheWarmer?.onAgentSettled();
|
|
354
387
|
this._isAgentRunActive = false;
|
|
355
388
|
try {
|
|
356
389
|
await this._extensionRunner.emit({ type: "agent_settled" });
|
|
@@ -375,7 +408,6 @@ export class AgentSession {
|
|
|
375
408
|
}
|
|
376
409
|
if (this._autoHandoffTriggered)
|
|
377
410
|
return;
|
|
378
|
-
this._autoHandoffTriggered = true;
|
|
379
411
|
// Auto-handoff is TUI-only: it runs at agent_settled, and headless/print
|
|
380
412
|
// modes should not churn sessions on their own. The handoff-new command
|
|
381
413
|
// itself now also works in non-TUI modes (direct generation, no loader).
|
|
@@ -384,11 +416,14 @@ export class AgentSession {
|
|
|
384
416
|
const command = this._extensionRunner.getCommand("handoff-new");
|
|
385
417
|
if (!command)
|
|
386
418
|
return;
|
|
419
|
+
this._autoHandoffTriggered = true;
|
|
387
420
|
const ctx = this._extensionRunner.createCommandContext();
|
|
388
421
|
try {
|
|
389
|
-
await command.handler(
|
|
422
|
+
await command.handler(AUTO_HANDOFF_GOAL, ctx);
|
|
390
423
|
}
|
|
391
424
|
catch (err) {
|
|
425
|
+
// Retry on the next settled turn while the session remains above the threshold.
|
|
426
|
+
this._autoHandoffTriggered = false;
|
|
392
427
|
this._extensionRunner.emitError({
|
|
393
428
|
extensionPath: "command:handoff-new",
|
|
394
429
|
event: "auto-handoff",
|
|
@@ -433,7 +468,8 @@ export class AgentSession {
|
|
|
433
468
|
// Persist as CustomMessageEntry
|
|
434
469
|
this.sessionManager.appendCustomMessageEntry(event.message.customType, event.message.content, event.message.display, event.message.details);
|
|
435
470
|
}
|
|
436
|
-
else if (event.message.role === "
|
|
471
|
+
else if (event.message.role === "system" ||
|
|
472
|
+
event.message.role === "user" ||
|
|
437
473
|
event.message.role === "assistant" ||
|
|
438
474
|
event.message.role === "toolResult") {
|
|
439
475
|
// Regular LLM message - persist as SessionMessageEntry
|
|
@@ -444,7 +480,9 @@ export class AgentSession {
|
|
|
444
480
|
if (event.message.role === "assistant") {
|
|
445
481
|
this._lastAssistantMessage = event.message;
|
|
446
482
|
const assistantMsg = event.message;
|
|
447
|
-
|
|
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") {
|
|
448
486
|
this._overflowRecoveryAttempted = false;
|
|
449
487
|
}
|
|
450
488
|
// Reset the length-continuation counter on any non-length stop so the
|
|
@@ -474,6 +512,8 @@ export class AgentSession {
|
|
|
474
512
|
}
|
|
475
513
|
};
|
|
476
514
|
_willRetryAfterAgentEnd(event) {
|
|
515
|
+
if (this._agentRunAbortRequested)
|
|
516
|
+
return false;
|
|
477
517
|
const settings = this.settingsManager.getRetrySettings();
|
|
478
518
|
if (!settings.enabled || this._retryAttempt >= settings.maxRetries) {
|
|
479
519
|
return false;
|
|
@@ -662,6 +702,10 @@ export class AgentSession {
|
|
|
662
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().");
|
|
663
703
|
this._disconnectFromAgent();
|
|
664
704
|
this._eventListeners = [];
|
|
705
|
+
if (this._cacheWarmer) {
|
|
706
|
+
this._cacheWarmer.onWarmed = undefined;
|
|
707
|
+
this._cacheWarmer.cancel();
|
|
708
|
+
}
|
|
665
709
|
cleanupSessionResources(this.sessionId);
|
|
666
710
|
}
|
|
667
711
|
// =========================================================================
|
|
@@ -671,6 +715,15 @@ export class AgentSession {
|
|
|
671
715
|
get state() {
|
|
672
716
|
return this.agent.state;
|
|
673
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
|
+
}
|
|
674
727
|
/** Current model (may be undefined if not yet selected) */
|
|
675
728
|
get model() {
|
|
676
729
|
return this.agent.state.model;
|
|
@@ -687,9 +740,9 @@ export class AgentSession {
|
|
|
687
740
|
get isIdle() {
|
|
688
741
|
return !this._isAgentRunActive && !this.isCompacting;
|
|
689
742
|
}
|
|
690
|
-
/** Current effective system prompt
|
|
743
|
+
/** Current effective system prompt, including changes not yet sent to the model. */
|
|
691
744
|
get systemPrompt() {
|
|
692
|
-
return this.
|
|
745
|
+
return buildSystemPrompt(this._runSystemPromptOptions ?? this._baseSystemPromptOptions);
|
|
693
746
|
}
|
|
694
747
|
/** Current retry attempt (0 if not retrying) */
|
|
695
748
|
get retryAttempt() {
|
|
@@ -735,9 +788,7 @@ export class AgentSession {
|
|
|
735
788
|
}
|
|
736
789
|
}
|
|
737
790
|
this.agent.state.tools = tools;
|
|
738
|
-
|
|
739
|
-
this._baseSystemPrompt = this._rebuildSystemPrompt(validToolNames);
|
|
740
|
-
this.agent.state.systemPrompt = this._systemPromptOverride ?? this._baseSystemPrompt;
|
|
791
|
+
this._rebuildSystemPrompt(validToolNames);
|
|
741
792
|
}
|
|
742
793
|
/** Whether compaction or branch summarization is currently running */
|
|
743
794
|
get isCompacting() {
|
|
@@ -806,16 +857,10 @@ export class AgentSession {
|
|
|
806
857
|
_rebuildSystemPrompt(toolNames) {
|
|
807
858
|
const validToolNames = toolNames.filter((name) => this._toolRegistry.has(name));
|
|
808
859
|
const toolSnippets = {};
|
|
809
|
-
const
|
|
810
|
-
for (const name of validToolNames) {
|
|
860
|
+
for (const name of this._toolRegistry.keys()) {
|
|
811
861
|
const snippet = this._toolPromptSnippets.get(name);
|
|
812
|
-
if (snippet)
|
|
862
|
+
if (snippet)
|
|
813
863
|
toolSnippets[name] = snippet;
|
|
814
|
-
}
|
|
815
|
-
const toolGuidelines = this._toolPromptGuidelines.get(name);
|
|
816
|
-
if (toolGuidelines) {
|
|
817
|
-
promptGuidelines.push(...toolGuidelines);
|
|
818
|
-
}
|
|
819
864
|
}
|
|
820
865
|
const loaderSystemPrompt = this._resourceLoader.getSystemPrompt();
|
|
821
866
|
const loaderAppendSystemPrompt = this._resourceLoader.getAppendSystemPrompt();
|
|
@@ -823,7 +868,7 @@ export class AgentSession {
|
|
|
823
868
|
const loadedSkills = this._resourceLoader.getPromptSkills();
|
|
824
869
|
const loadedAgents = this._resourceLoader.getAgents().agents;
|
|
825
870
|
const loadedContextFiles = this._resourceLoader.getAgentsFiles().agentsFiles;
|
|
826
|
-
this._baseSystemPromptOptions = {
|
|
871
|
+
this._baseSystemPromptOptions = normalizeBuildSystemPromptOptions({
|
|
827
872
|
cwd: this._cwd,
|
|
828
873
|
skills: loadedSkills,
|
|
829
874
|
agents: loadedAgents,
|
|
@@ -832,23 +877,87 @@ export class AgentSession {
|
|
|
832
877
|
appendSystemPrompt,
|
|
833
878
|
selectedTools: validToolNames,
|
|
834
879
|
toolSnippets,
|
|
835
|
-
|
|
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")];
|
|
836
927
|
};
|
|
837
|
-
|
|
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);
|
|
838
942
|
}
|
|
839
943
|
// =========================================================================
|
|
840
944
|
// Prompting
|
|
841
945
|
// =========================================================================
|
|
842
946
|
async _runAgentPrompt(messages) {
|
|
947
|
+
this._agentRunAbortRequested = false;
|
|
843
948
|
this._isAgentRunActive = true;
|
|
844
949
|
try {
|
|
845
950
|
await this.agent.prompt(messages);
|
|
846
951
|
while (await this._handlePostAgentRun()) {
|
|
952
|
+
if (this._agentRunAbortRequested)
|
|
953
|
+
break;
|
|
847
954
|
await this.agent.continue();
|
|
848
955
|
}
|
|
849
956
|
}
|
|
850
957
|
finally {
|
|
851
|
-
this.
|
|
958
|
+
if (this._agentRunAbortRequested)
|
|
959
|
+
this._finishCancelledRetry();
|
|
960
|
+
this._runSystemPromptOptions = undefined;
|
|
852
961
|
this._flushPendingBashMessages();
|
|
853
962
|
this._flushPendingCustomMessages();
|
|
854
963
|
await this._emitAgentSettled();
|
|
@@ -857,11 +966,21 @@ export class AgentSession {
|
|
|
857
966
|
async _handlePostAgentRun() {
|
|
858
967
|
const msg = this._lastAssistantMessage;
|
|
859
968
|
this._lastAssistantMessage = undefined;
|
|
969
|
+
if (this._agentRunAbortRequested) {
|
|
970
|
+
this._finishCancelledRetry();
|
|
971
|
+
return false;
|
|
972
|
+
}
|
|
860
973
|
if (!msg) {
|
|
861
974
|
return false;
|
|
862
975
|
}
|
|
863
976
|
if (this._isRetryableError(msg) && (await this._prepareRetry(msg))) {
|
|
864
|
-
|
|
977
|
+
if (this._agentRunAbortRequested)
|
|
978
|
+
this._finishCancelledRetry();
|
|
979
|
+
return !this._agentRunAbortRequested;
|
|
980
|
+
}
|
|
981
|
+
if (this._agentRunAbortRequested) {
|
|
982
|
+
this._finishCancelledRetry();
|
|
983
|
+
return false;
|
|
865
984
|
}
|
|
866
985
|
if (msg.stopReason === "error" && this._retryAttempt > 0) {
|
|
867
986
|
this._emit({
|
|
@@ -873,7 +992,7 @@ export class AgentSession {
|
|
|
873
992
|
this._retryAttempt = 0;
|
|
874
993
|
}
|
|
875
994
|
if (await this._checkCompaction(msg)) {
|
|
876
|
-
return
|
|
995
|
+
return !this._agentRunAbortRequested;
|
|
877
996
|
}
|
|
878
997
|
// A length stop means the model hit the output token limit mid-response.
|
|
879
998
|
// The agent loop already re-prompts when truncated tool calls are present;
|
|
@@ -889,7 +1008,20 @@ export class AgentSession {
|
|
|
889
1008
|
}
|
|
890
1009
|
// The agent loop drains both queues before emitting agent_end. Any messages
|
|
891
1010
|
// here were queued by agent_end extension handlers and need a continuation.
|
|
892
|
-
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 };
|
|
893
1025
|
}
|
|
894
1026
|
/**
|
|
895
1027
|
* Send a prompt to the agent.
|
|
@@ -915,20 +1047,16 @@ export class AgentSession {
|
|
|
915
1047
|
return;
|
|
916
1048
|
}
|
|
917
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
|
+
}
|
|
918
1053
|
// Emit input event for extension interception (before skill/template expansion)
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
if (inputResult.action === "handled") {
|
|
924
|
-
preflightResult?.(true);
|
|
925
|
-
return;
|
|
926
|
-
}
|
|
927
|
-
if (inputResult.action === "transform") {
|
|
928
|
-
currentText = inputResult.text;
|
|
929
|
-
currentImages = inputResult.images ?? currentImages;
|
|
930
|
-
}
|
|
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;
|
|
931
1058
|
}
|
|
1059
|
+
const { text: currentText, images: currentImages } = processedInput;
|
|
932
1060
|
// Expand skill commands (/skill:name args) and prompt templates (/template args)
|
|
933
1061
|
let expandedText = currentText;
|
|
934
1062
|
if (expandPromptTemplates) {
|
|
@@ -1000,30 +1128,25 @@ export class AgentSession {
|
|
|
1000
1128
|
}
|
|
1001
1129
|
this._pendingNextTurnMessages = [];
|
|
1002
1130
|
// Emit before_agent_start extension event
|
|
1003
|
-
const
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
}
|
|
1023
|
-
else {
|
|
1024
|
-
// Ensure we're using the base prompt (in case previous turn had modifications)
|
|
1025
|
-
this._systemPromptOverride = undefined;
|
|
1026
|
-
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
|
+
});
|
|
1027
1150
|
}
|
|
1028
1151
|
// Another prompt may have started while this one was preflighting (auth
|
|
1029
1152
|
// checks, compaction, and extension events can take seconds). The agent is
|
|
@@ -1040,6 +1163,10 @@ export class AgentSession {
|
|
|
1040
1163
|
preflightResult?.(true);
|
|
1041
1164
|
return;
|
|
1042
1165
|
}
|
|
1166
|
+
const updateMessage = this._preparePromptAndToolLoadout(result.systemPromptOptions);
|
|
1167
|
+
this._runSystemPromptOptions = result.systemPromptOptions;
|
|
1168
|
+
if (updateMessage)
|
|
1169
|
+
messages.unshift(updateMessage);
|
|
1043
1170
|
}
|
|
1044
1171
|
catch (error) {
|
|
1045
1172
|
preflightResult?.(false);
|
|
@@ -1108,40 +1235,44 @@ export class AgentSession {
|
|
|
1108
1235
|
return text; // Return original on error
|
|
1109
1236
|
}
|
|
1110
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
|
+
}
|
|
1111
1254
|
/**
|
|
1112
1255
|
* Queue a steering message while the agent is running.
|
|
1113
1256
|
* Delivered after the current assistant turn finishes executing its tool calls,
|
|
1114
1257
|
* before the next LLM call.
|
|
1115
1258
|
* Expands skill commands and prompt templates. Errors on extension commands.
|
|
1116
1259
|
* @param images Optional image attachments to include with the message
|
|
1260
|
+
* @param options Input source; defaults to interactive
|
|
1117
1261
|
* @throws Error if text is an extension command
|
|
1118
1262
|
*/
|
|
1119
|
-
async steer(text, images) {
|
|
1120
|
-
|
|
1121
|
-
if (text.startsWith("/")) {
|
|
1122
|
-
this._throwIfExtensionCommand(text);
|
|
1123
|
-
}
|
|
1124
|
-
// Expand skill commands and prompt templates
|
|
1125
|
-
let expandedText = this._expandSkillCommand(text);
|
|
1126
|
-
expandedText = expandPromptTemplate(expandedText, [...this.promptTemplates]);
|
|
1127
|
-
await this._queueSteer(expandedText, images);
|
|
1263
|
+
async steer(text, images, options) {
|
|
1264
|
+
await this._queueUserInput(text, images, "steer", options?.source ?? "interactive");
|
|
1128
1265
|
}
|
|
1129
1266
|
/**
|
|
1130
1267
|
* Queue a follow-up message to be processed after the agent finishes.
|
|
1131
1268
|
* Delivered only when agent has no more tool calls or steering messages.
|
|
1132
1269
|
* Expands skill commands and prompt templates. Errors on extension commands.
|
|
1133
1270
|
* @param images Optional image attachments to include with the message
|
|
1271
|
+
* @param options Input source; defaults to interactive
|
|
1134
1272
|
* @throws Error if text is an extension command
|
|
1135
1273
|
*/
|
|
1136
|
-
async followUp(text, images) {
|
|
1137
|
-
|
|
1138
|
-
if (text.startsWith("/")) {
|
|
1139
|
-
this._throwIfExtensionCommand(text);
|
|
1140
|
-
}
|
|
1141
|
-
// Expand skill commands and prompt templates
|
|
1142
|
-
let expandedText = this._expandSkillCommand(text);
|
|
1143
|
-
expandedText = expandPromptTemplate(expandedText, [...this.promptTemplates]);
|
|
1144
|
-
await this._queueFollowUp(expandedText, images);
|
|
1274
|
+
async followUp(text, images, options) {
|
|
1275
|
+
await this._queueUserInput(text, images, "followUp", options?.source ?? "interactive");
|
|
1145
1276
|
}
|
|
1146
1277
|
/**
|
|
1147
1278
|
* Internal: Queue a steering message (already expanded, no extension command check).
|
|
@@ -1485,6 +1616,9 @@ export class AgentSession {
|
|
|
1485
1616
|
* Abort current operation and wait for agent to become idle.
|
|
1486
1617
|
*/
|
|
1487
1618
|
async abort() {
|
|
1619
|
+
if (this._isAgentRunActive) {
|
|
1620
|
+
this._agentRunAbortRequested = true;
|
|
1621
|
+
}
|
|
1488
1622
|
this.abortRetry();
|
|
1489
1623
|
this.abortCompaction();
|
|
1490
1624
|
this.abortBranchSummary();
|
|
@@ -1707,6 +1841,10 @@ export class AgentSession {
|
|
|
1707
1841
|
// =========================================================================
|
|
1708
1842
|
// Compaction
|
|
1709
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
|
+
}
|
|
1710
1848
|
_clearManualCompactionState() {
|
|
1711
1849
|
this._compactionAbortController = undefined;
|
|
1712
1850
|
this._resolveIdleWaitIfIdle();
|
|
@@ -1721,13 +1859,16 @@ export class AgentSession {
|
|
|
1721
1859
|
await this.abort();
|
|
1722
1860
|
this._compactionAbortController = new AbortController();
|
|
1723
1861
|
this._emit({ type: "compaction_start", reason: "manual" });
|
|
1862
|
+
let fromExtension = false;
|
|
1863
|
+
let cancelledByExtension = false;
|
|
1724
1864
|
try {
|
|
1725
|
-
|
|
1865
|
+
const model = this.model;
|
|
1866
|
+
if (!model) {
|
|
1726
1867
|
throw new Error(formatNoModelSelectedMessage());
|
|
1727
1868
|
}
|
|
1728
|
-
const
|
|
1869
|
+
const settings = this.settingsManager.getCompactionSettings(model);
|
|
1870
|
+
const { model: requestModel, apiKey, headers, env, } = await this._getSummarizationRequestAuth(model, this._compactionAbortController.signal);
|
|
1729
1871
|
const pathEntries = this.sessionManager.getBranch();
|
|
1730
|
-
const settings = this.settingsManager.getCompactionSettings();
|
|
1731
1872
|
const preparation = prepareCompaction(pathEntries, settings);
|
|
1732
1873
|
if (!preparation) {
|
|
1733
1874
|
// Check why we can't compact
|
|
@@ -1750,6 +1891,7 @@ export class AgentSession {
|
|
|
1750
1891
|
signal: this._compactionAbortController.signal,
|
|
1751
1892
|
}));
|
|
1752
1893
|
if (result?.cancel) {
|
|
1894
|
+
cancelledByExtension = true;
|
|
1753
1895
|
throw new Error("Compaction cancelled");
|
|
1754
1896
|
}
|
|
1755
1897
|
if (result?.compaction) {
|
|
@@ -1771,8 +1913,8 @@ export class AgentSession {
|
|
|
1771
1913
|
details = extensionCompaction.details;
|
|
1772
1914
|
}
|
|
1773
1915
|
else {
|
|
1774
|
-
//
|
|
1775
|
-
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");
|
|
1776
1918
|
summary = result.summary;
|
|
1777
1919
|
firstKeptEntryId = result.firstKeptEntryId;
|
|
1778
1920
|
tokensBefore = result.tokensBefore;
|
|
@@ -1819,7 +1961,7 @@ export class AgentSession {
|
|
|
1819
1961
|
}
|
|
1820
1962
|
catch (error) {
|
|
1821
1963
|
const message = error instanceof Error ? error.message : String(error);
|
|
1822
|
-
const aborted =
|
|
1964
|
+
const aborted = this._compactionAbortController.signal.aborted || cancelledByExtension;
|
|
1823
1965
|
const errorMessage = aborted ? undefined : `Compaction failed: ${message}`;
|
|
1824
1966
|
this._clearManualCompactionState();
|
|
1825
1967
|
this._emit({
|
|
@@ -1869,7 +2011,7 @@ export class AgentSession {
|
|
|
1869
2011
|
* @param skipAbortedCheck If false, include aborted messages (for pre-prompt check). Default: true
|
|
1870
2012
|
*/
|
|
1871
2013
|
async _checkCompaction(assistantMessage, skipAbortedCheck = true) {
|
|
1872
|
-
const settings = this.settingsManager.getCompactionSettings();
|
|
2014
|
+
const settings = this.settingsManager.getCompactionSettings(this.model);
|
|
1873
2015
|
if (!settings.enabled)
|
|
1874
2016
|
return false;
|
|
1875
2017
|
// Skip if message was aborted (user cancelled) - unless skipAbortedCheck is false
|
|
@@ -1889,11 +2031,12 @@ export class AgentSession {
|
|
|
1889
2031
|
if (assistantIsFromBeforeCompaction) {
|
|
1890
2032
|
return false;
|
|
1891
2033
|
}
|
|
1892
|
-
//
|
|
1893
|
-
//
|
|
1894
|
-
//
|
|
1895
|
-
|
|
1896
|
-
|
|
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) {
|
|
1897
2040
|
const willRetry = assistantMessage.stopReason !== "stop";
|
|
1898
2041
|
if (!willRetry) {
|
|
1899
2042
|
return await this._runAutoCompaction("overflow", false);
|
|
@@ -1905,7 +2048,9 @@ export class AgentSession {
|
|
|
1905
2048
|
result: undefined,
|
|
1906
2049
|
aborted: false,
|
|
1907
2050
|
willRetry: false,
|
|
1908
|
-
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.",
|
|
1909
2054
|
});
|
|
1910
2055
|
return false;
|
|
1911
2056
|
}
|
|
@@ -1952,30 +2097,28 @@ export class AgentSession {
|
|
|
1952
2097
|
* Internal: Run auto-compaction with events.
|
|
1953
2098
|
*/
|
|
1954
2099
|
async _runAutoCompaction(reason, willRetry) {
|
|
1955
|
-
const
|
|
2100
|
+
const model = this.model;
|
|
2101
|
+
const settings = this.settingsManager.getCompactionSettings(model);
|
|
2102
|
+
let abortController;
|
|
1956
2103
|
let started = false;
|
|
1957
2104
|
let fromExtension = false;
|
|
2105
|
+
let cancelledByExtension = false;
|
|
1958
2106
|
try {
|
|
1959
|
-
if (!
|
|
2107
|
+
if (!model) {
|
|
1960
2108
|
return false;
|
|
1961
2109
|
}
|
|
1962
|
-
let apiKey;
|
|
1963
|
-
let headers;
|
|
1964
|
-
let env;
|
|
1965
|
-
if (this.agent.streamFunction === streamSimple) {
|
|
1966
|
-
({ apiKey, headers, env } = await this._getRequiredRequestAuth(this.model));
|
|
1967
|
-
}
|
|
1968
|
-
else {
|
|
1969
|
-
({ apiKey, headers, env } = await this._getSummarizationRequestAuth(this.model));
|
|
1970
|
-
}
|
|
1971
2110
|
const pathEntries = this.sessionManager.getBranch();
|
|
1972
2111
|
const preparation = prepareCompaction(pathEntries, settings);
|
|
1973
2112
|
if (!preparation) {
|
|
1974
2113
|
return false;
|
|
1975
2114
|
}
|
|
1976
|
-
|
|
1977
|
-
this._autoCompactionAbortController =
|
|
2115
|
+
abortController = new AbortController();
|
|
2116
|
+
this._autoCompactionAbortController = abortController;
|
|
1978
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();
|
|
1979
2122
|
let extensionCompaction;
|
|
1980
2123
|
if (this._extensionRunner.hasHandlers("session_before_compact")) {
|
|
1981
2124
|
const extensionResult = (await this._extensionRunner.emit({
|
|
@@ -1985,24 +2128,18 @@ export class AgentSession {
|
|
|
1985
2128
|
customInstructions: undefined,
|
|
1986
2129
|
reason,
|
|
1987
2130
|
willRetry,
|
|
1988
|
-
signal:
|
|
2131
|
+
signal: abortController.signal,
|
|
1989
2132
|
}));
|
|
1990
2133
|
if (extensionResult?.cancel) {
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
reason,
|
|
1994
|
-
result: undefined,
|
|
1995
|
-
aborted: true,
|
|
1996
|
-
willRetry: false,
|
|
1997
|
-
});
|
|
1998
|
-
await this._emitSessionCompactFailed({ reason, aborted: true, willRetry: false, fromExtension: false });
|
|
1999
|
-
return false;
|
|
2134
|
+
cancelledByExtension = true;
|
|
2135
|
+
throw new Error("Compaction cancelled");
|
|
2000
2136
|
}
|
|
2001
2137
|
if (extensionResult?.compaction) {
|
|
2002
2138
|
extensionCompaction = extensionResult.compaction;
|
|
2003
2139
|
fromExtension = true;
|
|
2004
2140
|
}
|
|
2005
2141
|
}
|
|
2142
|
+
abortController.signal.throwIfAborted();
|
|
2006
2143
|
let summary;
|
|
2007
2144
|
let firstKeptEntryId;
|
|
2008
2145
|
let tokensBefore;
|
|
@@ -2017,25 +2154,15 @@ export class AgentSession {
|
|
|
2017
2154
|
details = extensionCompaction.details;
|
|
2018
2155
|
}
|
|
2019
2156
|
else {
|
|
2020
|
-
//
|
|
2021
|
-
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);
|
|
2022
2159
|
summary = compactResult.summary;
|
|
2023
2160
|
firstKeptEntryId = compactResult.firstKeptEntryId;
|
|
2024
2161
|
tokensBefore = compactResult.tokensBefore;
|
|
2025
2162
|
usage = compactResult.usage;
|
|
2026
2163
|
details = compactResult.details;
|
|
2027
2164
|
}
|
|
2028
|
-
|
|
2029
|
-
this._emit({
|
|
2030
|
-
type: "compaction_end",
|
|
2031
|
-
reason,
|
|
2032
|
-
result: undefined,
|
|
2033
|
-
aborted: true,
|
|
2034
|
-
willRetry: false,
|
|
2035
|
-
});
|
|
2036
|
-
await this._emitSessionCompactFailed({ reason, aborted: true, willRetry: false, fromExtension });
|
|
2037
|
-
return false;
|
|
2038
|
-
}
|
|
2165
|
+
abortController.signal.throwIfAborted();
|
|
2039
2166
|
this.sessionManager.appendCompaction(summary, firstKeptEntryId, tokensBefore, details, fromExtension, usage);
|
|
2040
2167
|
const newEntries = this.sessionManager.getEntries();
|
|
2041
2168
|
const sessionContext = this.sessionManager.buildSessionContext();
|
|
@@ -2064,7 +2191,11 @@ export class AgentSession {
|
|
|
2064
2191
|
if (willRetry) {
|
|
2065
2192
|
const messages = this.agent.state.messages;
|
|
2066
2193
|
const lastMsg = messages[messages.length - 1];
|
|
2067
|
-
|
|
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")) {
|
|
2068
2199
|
this.agent.state.messages = messages.slice(0, -1);
|
|
2069
2200
|
}
|
|
2070
2201
|
return true;
|
|
@@ -2074,23 +2205,26 @@ export class AgentSession {
|
|
|
2074
2205
|
return this.agent.hasQueuedMessages();
|
|
2075
2206
|
}
|
|
2076
2207
|
catch (error) {
|
|
2077
|
-
const
|
|
2208
|
+
const message = error instanceof Error ? error.message : "compaction failed";
|
|
2209
|
+
const aborted = abortController?.signal.aborted === true || cancelledByExtension;
|
|
2078
2210
|
if (started) {
|
|
2079
|
-
const
|
|
2080
|
-
?
|
|
2081
|
-
:
|
|
2211
|
+
const errorMessage = aborted
|
|
2212
|
+
? undefined
|
|
2213
|
+
: reason === "overflow"
|
|
2214
|
+
? `Context overflow recovery failed: ${message}`
|
|
2215
|
+
: `Auto-compaction failed: ${message}`;
|
|
2082
2216
|
this._emit({
|
|
2083
2217
|
type: "compaction_end",
|
|
2084
2218
|
reason,
|
|
2085
2219
|
result: undefined,
|
|
2086
|
-
aborted
|
|
2220
|
+
aborted,
|
|
2087
2221
|
willRetry: false,
|
|
2088
|
-
errorMessage
|
|
2222
|
+
errorMessage,
|
|
2089
2223
|
});
|
|
2090
2224
|
await this._emitSessionCompactFailed({
|
|
2091
2225
|
reason,
|
|
2092
|
-
errorMessage
|
|
2093
|
-
aborted
|
|
2226
|
+
errorMessage,
|
|
2227
|
+
aborted,
|
|
2094
2228
|
willRetry: false,
|
|
2095
2229
|
fromExtension,
|
|
2096
2230
|
});
|
|
@@ -2098,7 +2232,9 @@ export class AgentSession {
|
|
|
2098
2232
|
return false;
|
|
2099
2233
|
}
|
|
2100
2234
|
finally {
|
|
2101
|
-
this._autoCompactionAbortController
|
|
2235
|
+
if (this._autoCompactionAbortController === abortController) {
|
|
2236
|
+
this._autoCompactionAbortController = undefined;
|
|
2237
|
+
}
|
|
2102
2238
|
this._resolveIdleWaitIfIdle();
|
|
2103
2239
|
}
|
|
2104
2240
|
}
|
|
@@ -2155,8 +2291,7 @@ export class AgentSession {
|
|
|
2155
2291
|
themePaths: this.buildExtensionResourcePaths(themePaths),
|
|
2156
2292
|
};
|
|
2157
2293
|
this._resourceLoader.extendResources(extensionPaths);
|
|
2158
|
-
this.
|
|
2159
|
-
this.agent.state.systemPrompt = this._baseSystemPrompt;
|
|
2294
|
+
this._rebuildSystemPrompt(this.getActiveToolNames());
|
|
2160
2295
|
}
|
|
2161
2296
|
buildExtensionResourcePaths(entries) {
|
|
2162
2297
|
return entries.map((entry) => {
|
|
@@ -2273,8 +2408,7 @@ export class AgentSession {
|
|
|
2273
2408
|
}),
|
|
2274
2409
|
setSkillsIndexFilter: (filter) => {
|
|
2275
2410
|
this._resourceLoader.setSkillsIndexFilter(filter);
|
|
2276
|
-
this.
|
|
2277
|
-
this.agent.state.systemPrompt = this._systemPromptOverride ?? this._baseSystemPrompt;
|
|
2411
|
+
this._rebuildSystemPrompt(this.getActiveToolNames());
|
|
2278
2412
|
},
|
|
2279
2413
|
refreshTools: () => this._refreshToolRegistry(),
|
|
2280
2414
|
getCommands,
|
|
@@ -2508,6 +2642,18 @@ export class AgentSession {
|
|
|
2508
2642
|
},
|
|
2509
2643
|
};
|
|
2510
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
|
+
}
|
|
2511
2657
|
/**
|
|
2512
2658
|
* Prepare a retryable error for continuation with exponential backoff.
|
|
2513
2659
|
* @returns true if the caller should continue the agent, false otherwise
|
|
@@ -2523,7 +2669,7 @@ export class AgentSession {
|
|
|
2523
2669
|
this._retryAttempt--;
|
|
2524
2670
|
return false;
|
|
2525
2671
|
}
|
|
2526
|
-
const delayMs = settings
|
|
2672
|
+
const delayMs = retryDelayMs(settings, this._retryAttempt);
|
|
2527
2673
|
this._emit({
|
|
2528
2674
|
type: "auto_retry_start",
|
|
2529
2675
|
attempt: this._retryAttempt,
|
|
@@ -2543,14 +2689,7 @@ export class AgentSession {
|
|
|
2543
2689
|
}
|
|
2544
2690
|
catch {
|
|
2545
2691
|
// Aborted during sleep - emit end event so UI can clean up
|
|
2546
|
-
|
|
2547
|
-
this._retryAttempt = 0;
|
|
2548
|
-
this._emit({
|
|
2549
|
-
type: "auto_retry_end",
|
|
2550
|
-
success: false,
|
|
2551
|
-
attempt,
|
|
2552
|
-
finalError: "Retry cancelled",
|
|
2553
|
-
});
|
|
2692
|
+
this._finishCancelledRetry();
|
|
2554
2693
|
return false;
|
|
2555
2694
|
}
|
|
2556
2695
|
finally {
|
|
@@ -2701,6 +2840,9 @@ export class AgentSession {
|
|
|
2701
2840
|
if (this.isStreaming) {
|
|
2702
2841
|
throw new Error("Wait for the current response to finish before navigating the session tree.");
|
|
2703
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
|
+
}
|
|
2704
2846
|
const oldLeafId = this.sessionManager.getLeafId();
|
|
2705
2847
|
// No-op if already at target
|
|
2706
2848
|
if (targetId === oldLeafId) {
|
|
@@ -2843,6 +2985,7 @@ export class AgentSession {
|
|
|
2843
2985
|
// Update agent state
|
|
2844
2986
|
const sessionContext = this.sessionManager.buildSessionContext();
|
|
2845
2987
|
this.agent.state.messages = sessionContext.messages;
|
|
2988
|
+
this._restoreToolsFromTranscript();
|
|
2846
2989
|
// Emit session_tree event (extension runners + RPC stdout subscribers)
|
|
2847
2990
|
const treeEvent = {
|
|
2848
2991
|
type: "session_tree",
|
|
@@ -2892,7 +3035,10 @@ export class AgentSession {
|
|
|
2892
3035
|
let toolCalls = 0;
|
|
2893
3036
|
const usageTotals = createUsageTotals();
|
|
2894
3037
|
for (const entry of this.sessionManager.getEntries()) {
|
|
2895
|
-
if (
|
|
3038
|
+
if (entry.type === "usage") {
|
|
3039
|
+
addUsageToTotals(usageTotals, entry.usage);
|
|
3040
|
+
}
|
|
3041
|
+
else if ((entry.type === "branch_summary" || entry.type === "compaction") && entry.usage) {
|
|
2896
3042
|
addUsageToTotals(usageTotals, entry.usage);
|
|
2897
3043
|
}
|
|
2898
3044
|
if (entry.type !== "message")
|
|
@@ -3027,6 +3173,30 @@ export class AgentSession {
|
|
|
3027
3173
|
writeFileSync(filePath, `${lines.join("\n")}\n`);
|
|
3028
3174
|
return filePath;
|
|
3029
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
|
+
}
|
|
3030
3200
|
// =========================================================================
|
|
3031
3201
|
// Utilities
|
|
3032
3202
|
// =========================================================================
|