@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.
Files changed (124) hide show
  1. package/CHANGELOG.md +85 -0
  2. package/dist/cli/args.js +1 -0
  3. package/dist/cli/session-picker.d.ts +1 -1
  4. package/dist/config.js +1 -1
  5. package/dist/core/agent-session-auto-handoff.test.js +14 -4
  6. package/dist/core/agent-session-length-continuation.test.js +2 -1
  7. package/dist/core/agent-session.d.ts +58 -7
  8. package/dist/core/agent-session.js +339 -169
  9. package/dist/core/bug-report-upload.d.ts +11 -0
  10. package/dist/core/bug-report-upload.js +21 -0
  11. package/dist/core/bug-report.d.ts +175 -0
  12. package/dist/core/bug-report.js +291 -0
  13. package/dist/core/cache-stats.js +12 -1
  14. package/dist/core/cache-warmer.d.ts +101 -0
  15. package/dist/core/cache-warmer.js +341 -0
  16. package/dist/core/compaction/branch-summarization.js +2 -2
  17. package/dist/core/compaction/compaction.d.ts +6 -5
  18. package/dist/core/compaction/compaction.js +49 -42
  19. package/dist/core/crash-log.d.ts +21 -0
  20. package/dist/core/crash-log.js +70 -0
  21. package/dist/core/experimental.d.ts +0 -4
  22. package/dist/core/experimental.js +0 -4
  23. package/dist/core/extensions/index.d.ts +1 -1
  24. package/dist/core/extensions/jiti-loader.d.ts +1 -0
  25. package/dist/core/extensions/jiti-loader.js +3 -0
  26. package/dist/core/extensions/jiti-static-loader.d.ts +1 -0
  27. package/dist/core/extensions/jiti-static-loader.js +3 -0
  28. package/dist/core/extensions/loader.js +39 -49
  29. package/dist/core/extensions/runner.d.ts +10 -7
  30. package/dist/core/extensions/runner.js +85 -67
  31. package/dist/core/extensions/types.d.ts +66 -52
  32. package/dist/core/extensions/virtual-modules.d.ts +2 -0
  33. package/dist/core/extensions/virtual-modules.js +39 -0
  34. package/dist/core/extensions/wrapper.js +1 -20
  35. package/dist/core/handoff.d.ts +1 -0
  36. package/dist/core/handoff.js +1 -0
  37. package/dist/core/index.d.ts +2 -1
  38. package/dist/core/keybindings.d.ts +1 -1
  39. package/dist/core/keybindings.js +1 -1
  40. package/dist/core/messages.js +1 -0
  41. package/dist/core/model-config.d.ts +101 -20
  42. package/dist/core/model-config.js +25 -18
  43. package/dist/core/model-resolver.js +2 -1
  44. package/dist/core/model-runtime.js +5 -3
  45. package/dist/core/provider-composer.d.ts +7 -2
  46. package/dist/core/provider-composer.js +10 -5
  47. package/dist/core/radius.d.ts +3 -0
  48. package/dist/core/radius.js +6 -0
  49. package/dist/core/sdk.js +60 -38
  50. package/dist/core/session-export.d.ts +6 -2
  51. package/dist/core/session-export.js +14 -13
  52. package/dist/core/session-format.test.d.ts +1 -0
  53. package/dist/core/session-format.test.js +96 -0
  54. package/dist/core/session-manager.d.ts +29 -6
  55. package/dist/core/session-manager.js +159 -96
  56. package/dist/core/settings-manager.d.ts +22 -5
  57. package/dist/core/settings-manager.js +56 -23
  58. package/dist/core/slash-commands.js +1 -0
  59. package/dist/core/system-prompt.d.ts +49 -4
  60. package/dist/core/system-prompt.js +155 -102
  61. package/dist/core/system-prompt.test.js +61 -23
  62. package/dist/core/tools/bash.d.ts +2 -1
  63. package/dist/core/tools/bash.js +10 -4
  64. package/dist/core/tools/edit.js +1 -2
  65. package/dist/core/tools/read.js +1 -2
  66. package/dist/core/tools/renderers/bash.js +9 -1
  67. package/dist/core/tools/write.js +1 -2
  68. package/dist/core/usage-totals.d.ts +1 -1
  69. package/dist/core/usage-totals.js +5 -1
  70. package/dist/defaults/models.json +2 -72
  71. package/dist/extensions/handoff-new.test.ts +13 -13
  72. package/dist/extensions/handoff-new.ts +17 -6
  73. package/dist/extensions/pi-web-agent/src/backends/factory.ts +14 -6
  74. package/dist/extensions/pi-web-agent/src/extension.ts +3 -1
  75. package/dist/extensions/pi-web-agent/src/orchestration/answer-synthesizer.ts +7 -3
  76. package/dist/extensions/pi-web-agent/src/orchestration/research-orchestrator.ts +3 -0
  77. package/dist/extensions/pi-web-agent/src/orchestration/research-types.ts +2 -0
  78. package/dist/extensions/pi-web-agent/src/presentation/explore-presentation.ts +4 -2
  79. package/dist/extensions/pi-web-agent/src/search/tokenin.ts +4 -1
  80. package/dist/extensions/pi-web-agent/src/tools/web-explore.ts +2 -1
  81. package/dist/extensions/pi-web-agent/src/types.ts +4 -0
  82. package/dist/index.d.ts +3 -2
  83. package/dist/main.js +13 -9
  84. package/dist/migrations.js +2 -2
  85. package/dist/modes/interactive/bug-report.d.ts +15 -0
  86. package/dist/modes/interactive/bug-report.js +205 -0
  87. package/dist/modes/interactive/chat-viewport.js +1 -1
  88. package/dist/modes/interactive/components/branch-summary-message.js +12 -5
  89. package/dist/modes/interactive/components/compaction-summary-message.js +12 -5
  90. package/dist/modes/interactive/components/custom-editor.d.ts +3 -3
  91. package/dist/modes/interactive/components/extension-editor.d.ts +4 -1
  92. package/dist/modes/interactive/components/extension-editor.js +7 -2
  93. package/dist/modes/interactive/components/extension-input.d.ts +2 -0
  94. package/dist/modes/interactive/components/extension-input.js +6 -0
  95. package/dist/modes/interactive/components/extension-selector.d.ts +1 -0
  96. package/dist/modes/interactive/components/extension-selector.js +4 -0
  97. package/dist/modes/interactive/components/footer.js +4 -1
  98. package/dist/modes/interactive/components/session-selector.d.ts +5 -5
  99. package/dist/modes/interactive/components/session-selector.js +74 -48
  100. package/dist/modes/interactive/components/settings-selector.d.ts +3 -1
  101. package/dist/modes/interactive/components/settings-selector.js +11 -0
  102. package/dist/modes/interactive/components/skill-invocation-message.js +11 -4
  103. package/dist/modes/interactive/components/status-indicator.d.ts +2 -2
  104. package/dist/modes/interactive/components/status-indicator.js +7 -7
  105. package/dist/modes/interactive/components/tool-execution.js +18 -9
  106. package/dist/modes/interactive/components/tree-selector.js +2 -0
  107. package/dist/modes/interactive/interactive-mode.d.ts +15 -2
  108. package/dist/modes/interactive/interactive-mode.js +257 -81
  109. package/dist/modes/interactive/session-share.d.ts +2 -0
  110. package/dist/modes/interactive/session-share.js +8 -4
  111. package/dist/modes/interactive/tui-renderer.js +2 -2
  112. package/dist/modes/rpc/rpc-mode.js +7 -2
  113. package/dist/utils/clipboard-command.d.ts +6 -0
  114. package/dist/utils/clipboard-command.js +44 -0
  115. package/dist/utils/clipboard-image.js +55 -89
  116. package/dist/utils/clipboard.d.ts +1 -1
  117. package/dist/utils/clipboard.js +118 -108
  118. package/dist/utils/wsl.d.ts +2 -0
  119. package/dist/utils/wsl.js +14 -0
  120. package/dist/utils/zip.d.ts +6 -0
  121. package/dist/utils/zip.js +59 -0
  122. package/package.json +9 -9
  123. package/dist/utils/clipboard-native.d.ts +0 -10
  124. 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
- _systemPromptOverride;
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
- return result
217
- ? { apiKey: result.auth.apiKey, headers: withoutDeletedHeaders(result.auth.headers), env: result.env }
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
- return {};
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("", ctx);
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 === "user" ||
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
- if (assistantMsg.stopReason !== "error") {
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 (includes any per-turn extension modifications) */
743
+ /** Current effective system prompt, including changes not yet sent to the model. */
691
744
  get systemPrompt() {
692
- return this.agent.state.systemPrompt;
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
- // Rebuild base system prompt with new tool set
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 promptGuidelines = [];
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
- promptGuidelines,
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
- return buildSystemPrompt(this._baseSystemPromptOptions);
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._systemPromptOverride = undefined;
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
- return true;
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 true;
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
- let currentText = text;
920
- let currentImages = options?.images;
921
- if (this._extensionRunner.hasHandlers("input")) {
922
- const inputResult = await this._extensionRunner.emitInput(currentText, currentImages, options?.source ?? "interactive", this.isStreaming ? options?.streamingBehavior : undefined);
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 result = await this._extensionRunner.emitBeforeAgentStart(expandedText, currentImages, this._baseSystemPrompt, this._baseSystemPromptOptions);
1004
- // Add all custom messages from extensions
1005
- if (result?.messages) {
1006
- for (const msg of result.messages) {
1007
- messages.push({
1008
- role: "custom",
1009
- customType: msg.customType,
1010
- // Untyped extensions can pass null/missing content; normalize at ingestion.
1011
- content: msg.content ?? [],
1012
- display: msg.display,
1013
- details: msg.details,
1014
- timestamp: Date.now(),
1015
- });
1016
- }
1017
- }
1018
- // Apply extension-modified system prompt, or reset to base
1019
- if (result?.systemPrompt !== undefined) {
1020
- this._systemPromptOverride = result.systemPrompt;
1021
- this.agent.state.systemPrompt = result.systemPrompt;
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
- // Check for extension commands (cannot be queued)
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
- // Check for extension commands (cannot be queued)
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
- if (!this.model) {
1865
+ const model = this.model;
1866
+ if (!model) {
1726
1867
  throw new Error(formatNoModelSelectedMessage());
1727
1868
  }
1728
- const { apiKey, headers, env } = await this._getSummarizationRequestAuth(this.model);
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
- // Generate compaction result
1775
- const result = await compact(preparation, this.model, apiKey, headers, customInstructions, this._compactionAbortController.signal, this.thinkingLevel, this.agent.streamFunction, env, this.settingsManager.getRetrySettings(), this._summarizationRetryCallbacks({ source: "compaction", reason: "manual" }));
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 = message === "Compaction cancelled" || (error instanceof Error && error.name === "AbortError");
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
- // Case 1: Overflow - LLM returned context overflow error, or reported usage exceeded
1893
- // the configured window. A successful response over the configured window should compact
1894
- // but must not retry: the assistant answer already completed and agent.continue() cannot
1895
- // continue from an assistant message.
1896
- if (sameModel && isContextOverflow(assistantMessage, contextWindow)) {
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: "Context overflow recovery failed after one compact-and-retry attempt. Try reducing context or switching to a larger-context model.",
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 settings = this.settingsManager.getCompactionSettings();
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 (!this.model) {
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
- this._emit({ type: "compaction_start", reason });
1977
- this._autoCompactionAbortController = new AbortController();
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: this._autoCompactionAbortController.signal,
2131
+ signal: abortController.signal,
1989
2132
  }));
1990
2133
  if (extensionResult?.cancel) {
1991
- this._emit({
1992
- type: "compaction_end",
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
- // Generate compaction result
2021
- const compactResult = await compact(preparation, this.model, apiKey, headers, undefined, this._autoCompactionAbortController.signal, this.thinkingLevel, this.agent.streamFunction, env, this.settingsManager.getRetrySettings(), this._summarizationRetryCallbacks({ source: "compaction", reason }));
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
- if (this._autoCompactionAbortController.signal.aborted) {
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
- if (lastMsg?.role === "assistant" && lastMsg.stopReason === "error") {
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 errorMessage = error instanceof Error ? error.message : "compaction failed";
2208
+ const message = error instanceof Error ? error.message : "compaction failed";
2209
+ const aborted = abortController?.signal.aborted === true || cancelledByExtension;
2078
2210
  if (started) {
2079
- const formattedErrorMessage = reason === "overflow"
2080
- ? `Context overflow recovery failed: ${errorMessage}`
2081
- : `Auto-compaction failed: ${errorMessage}`;
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: false,
2220
+ aborted,
2087
2221
  willRetry: false,
2088
- errorMessage: formattedErrorMessage,
2222
+ errorMessage,
2089
2223
  });
2090
2224
  await this._emitSessionCompactFailed({
2091
2225
  reason,
2092
- errorMessage: formattedErrorMessage,
2093
- aborted: false,
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 = undefined;
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._baseSystemPrompt = this._rebuildSystemPrompt(this.getActiveToolNames());
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._baseSystemPrompt = this._rebuildSystemPrompt(this.getActiveToolNames());
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.baseDelayMs * 2 ** (this._retryAttempt - 1);
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
- const attempt = this._retryAttempt;
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 ((entry.type === "branch_summary" || entry.type === "compaction") && entry.usage) {
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
  // =========================================================================