apex-code 0.0.1-alpha.8 → 0.0.1-alpha.9

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 (230) hide show
  1. package/CHANGELOG.md +83 -0
  2. package/dist/cli/args.d.ts +1 -0
  3. package/dist/cli/args.d.ts.map +1 -1
  4. package/dist/cli/args.js +29 -9
  5. package/dist/cli/args.js.map +1 -1
  6. package/dist/cli/file-processor.d.ts.map +1 -1
  7. package/dist/cli/file-processor.js +2 -1
  8. package/dist/cli/file-processor.js.map +1 -1
  9. package/dist/config.d.ts +3 -2
  10. package/dist/config.d.ts.map +1 -1
  11. package/dist/config.js +21 -13
  12. package/dist/config.js.map +1 -1
  13. package/dist/core/agent-session.d.ts +26 -7
  14. package/dist/core/agent-session.d.ts.map +1 -1
  15. package/dist/core/agent-session.js +163 -95
  16. package/dist/core/agent-session.js.map +1 -1
  17. package/dist/core/auth-storage.d.ts.map +1 -1
  18. package/dist/core/auth-storage.js +6 -7
  19. package/dist/core/auth-storage.js.map +1 -1
  20. package/dist/core/compaction/branch-summarization.d.ts.map +1 -1
  21. package/dist/core/compaction/branch-summarization.js +3 -0
  22. package/dist/core/compaction/branch-summarization.js.map +1 -1
  23. package/dist/core/compaction/compaction.d.ts +4 -3
  24. package/dist/core/compaction/compaction.d.ts.map +1 -1
  25. package/dist/core/compaction/compaction.js +41 -32
  26. package/dist/core/compaction/compaction.js.map +1 -1
  27. package/dist/core/defaults.d.ts +1 -0
  28. package/dist/core/defaults.d.ts.map +1 -1
  29. package/dist/core/defaults.js +9 -0
  30. package/dist/core/defaults.js.map +1 -1
  31. package/dist/core/durable-state/sqlite.d.ts.map +1 -1
  32. package/dist/core/durable-state/sqlite.js +26 -2
  33. package/dist/core/durable-state/sqlite.js.map +1 -1
  34. package/dist/core/extensions/index.d.ts +2 -2
  35. package/dist/core/extensions/index.d.ts.map +1 -1
  36. package/dist/core/extensions/index.js +1 -1
  37. package/dist/core/extensions/index.js.map +1 -1
  38. package/dist/core/extensions/loader.d.ts.map +1 -1
  39. package/dist/core/extensions/loader.js +115 -48
  40. package/dist/core/extensions/loader.js.map +1 -1
  41. package/dist/core/extensions/types.d.ts +38 -9
  42. package/dist/core/extensions/types.d.ts.map +1 -1
  43. package/dist/core/extensions/types.js +3 -0
  44. package/dist/core/extensions/types.js.map +1 -1
  45. package/dist/core/footer-data-provider.d.ts +1 -1
  46. package/dist/core/footer-data-provider.d.ts.map +1 -1
  47. package/dist/core/footer-data-provider.js +1 -1
  48. package/dist/core/footer-data-provider.js.map +1 -1
  49. package/dist/core/keybindings.d.ts +20 -19
  50. package/dist/core/keybindings.d.ts.map +1 -1
  51. package/dist/core/keybindings.js +26 -5
  52. package/dist/core/keybindings.js.map +1 -1
  53. package/dist/core/model-config.d.ts +5 -0
  54. package/dist/core/model-config.d.ts.map +1 -1
  55. package/dist/core/model-config.js +3 -1
  56. package/dist/core/model-config.js.map +1 -1
  57. package/dist/core/model-resolver.d.ts +1 -0
  58. package/dist/core/model-resolver.d.ts.map +1 -1
  59. package/dist/core/model-resolver.js +14 -8
  60. package/dist/core/model-resolver.js.map +1 -1
  61. package/dist/core/models-store.d.ts.map +1 -1
  62. package/dist/core/models-store.js +2 -1
  63. package/dist/core/models-store.js.map +1 -1
  64. package/dist/core/package-manager.d.ts.map +1 -1
  65. package/dist/core/package-manager.js +22 -14
  66. package/dist/core/package-manager.js.map +1 -1
  67. package/dist/core/pi-manifest.d.ts.map +1 -1
  68. package/dist/core/pi-manifest.js +2 -1
  69. package/dist/core/pi-manifest.js.map +1 -1
  70. package/dist/core/remote-catalog-provider.d.ts.map +1 -1
  71. package/dist/core/remote-catalog-provider.js +2 -1
  72. package/dist/core/remote-catalog-provider.js.map +1 -1
  73. package/dist/core/resource-loader.d.ts.map +1 -1
  74. package/dist/core/resource-loader.js +3 -2
  75. package/dist/core/resource-loader.js.map +1 -1
  76. package/dist/core/sdk.d.ts +2 -2
  77. package/dist/core/sdk.d.ts.map +1 -1
  78. package/dist/core/sdk.js +10 -3
  79. package/dist/core/sdk.js.map +1 -1
  80. package/dist/core/session-export.d.ts +4 -0
  81. package/dist/core/session-export.d.ts.map +1 -0
  82. package/dist/core/session-export.js +32 -0
  83. package/dist/core/session-export.js.map +1 -0
  84. package/dist/core/session-manager.d.ts.map +1 -1
  85. package/dist/core/session-manager.js +2 -1
  86. package/dist/core/session-manager.js.map +1 -1
  87. package/dist/core/settings-diagnostics.d.ts +9 -0
  88. package/dist/core/settings-diagnostics.d.ts.map +1 -0
  89. package/dist/core/settings-diagnostics.js +21 -0
  90. package/dist/core/settings-diagnostics.js.map +1 -0
  91. package/dist/core/settings-manager.d.ts +9 -0
  92. package/dist/core/settings-manager.d.ts.map +1 -1
  93. package/dist/core/settings-manager.js +53 -11
  94. package/dist/core/settings-manager.js.map +1 -1
  95. package/dist/core/skills.d.ts.map +1 -1
  96. package/dist/core/skills.js +51 -33
  97. package/dist/core/skills.js.map +1 -1
  98. package/dist/core/slash-commands.d.ts.map +1 -1
  99. package/dist/core/slash-commands.js +2 -1
  100. package/dist/core/slash-commands.js.map +1 -1
  101. package/dist/core/system-prompt.d.ts.map +1 -1
  102. package/dist/core/system-prompt.js +8 -2
  103. package/dist/core/system-prompt.js.map +1 -1
  104. package/dist/core/tools/bash.d.ts +14 -1
  105. package/dist/core/tools/bash.d.ts.map +1 -1
  106. package/dist/core/tools/bash.js +36 -20
  107. package/dist/core/tools/bash.js.map +1 -1
  108. package/dist/core/tools/edit-diff.d.ts +0 -5
  109. package/dist/core/tools/edit-diff.d.ts.map +1 -1
  110. package/dist/core/tools/edit-diff.js +2 -5
  111. package/dist/core/tools/edit-diff.js.map +1 -1
  112. package/dist/core/tools/edit.d.ts.map +1 -1
  113. package/dist/core/tools/edit.js +20 -4
  114. package/dist/core/tools/edit.js.map +1 -1
  115. package/dist/core/tools/index.d.ts +4 -1
  116. package/dist/core/tools/index.d.ts.map +1 -1
  117. package/dist/core/tools/index.js +9 -0
  118. package/dist/core/tools/index.js.map +1 -1
  119. package/dist/core/tools/powershell.d.ts +16 -0
  120. package/dist/core/tools/powershell.d.ts.map +1 -0
  121. package/dist/core/tools/powershell.js +39 -0
  122. package/dist/core/tools/powershell.js.map +1 -0
  123. package/dist/core/trust-manager.d.ts.map +1 -1
  124. package/dist/core/trust-manager.js +2 -1
  125. package/dist/core/trust-manager.js.map +1 -1
  126. package/dist/extensions/llama/index.d.ts.map +1 -1
  127. package/dist/extensions/llama/index.js +2 -0
  128. package/dist/extensions/llama/index.js.map +1 -1
  129. package/dist/extensions/llama/provider.d.ts.map +1 -1
  130. package/dist/extensions/llama/provider.js +6 -2
  131. package/dist/extensions/llama/provider.js.map +1 -1
  132. package/dist/index.d.ts +5 -5
  133. package/dist/index.d.ts.map +1 -1
  134. package/dist/index.js +4 -4
  135. package/dist/index.js.map +1 -1
  136. package/dist/main.d.ts +4 -0
  137. package/dist/main.d.ts.map +1 -1
  138. package/dist/main.js +18 -15
  139. package/dist/main.js.map +1 -1
  140. package/dist/migrations.d.ts.map +1 -1
  141. package/dist/migrations.js +4 -3
  142. package/dist/migrations.js.map +1 -1
  143. package/dist/modes/interactive/components/model-selector.d.ts +10 -3
  144. package/dist/modes/interactive/components/model-selector.d.ts.map +1 -1
  145. package/dist/modes/interactive/components/model-selector.js +51 -9
  146. package/dist/modes/interactive/components/model-selector.js.map +1 -1
  147. package/dist/modes/interactive/components/settings-selector.d.ts +7 -2
  148. package/dist/modes/interactive/components/settings-selector.d.ts.map +1 -1
  149. package/dist/modes/interactive/components/settings-selector.js +118 -56
  150. package/dist/modes/interactive/components/settings-selector.js.map +1 -1
  151. package/dist/modes/interactive/components/settings-submenu.d.ts +72 -0
  152. package/dist/modes/interactive/components/settings-submenu.d.ts.map +1 -0
  153. package/dist/modes/interactive/components/settings-submenu.js +165 -0
  154. package/dist/modes/interactive/components/settings-submenu.js.map +1 -0
  155. package/dist/modes/interactive/components/thinking-selector.d.ts +15 -3
  156. package/dist/modes/interactive/components/thinking-selector.d.ts.map +1 -1
  157. package/dist/modes/interactive/components/thinking-selector.js +74 -17
  158. package/dist/modes/interactive/components/thinking-selector.js.map +1 -1
  159. package/dist/modes/interactive/external-editor.d.ts.map +1 -1
  160. package/dist/modes/interactive/external-editor.js +2 -1
  161. package/dist/modes/interactive/external-editor.js.map +1 -1
  162. package/dist/modes/interactive/interactive-mode.d.ts +11 -0
  163. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  164. package/dist/modes/interactive/interactive-mode.js +164 -17
  165. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  166. package/dist/modes/interactive/theme/theme.d.ts.map +1 -1
  167. package/dist/modes/interactive/theme/theme.js +5 -4
  168. package/dist/modes/interactive/theme/theme.js.map +1 -1
  169. package/dist/modes/json-event.d.ts +16 -13
  170. package/dist/modes/json-event.d.ts.map +1 -1
  171. package/dist/modes/json-event.js +20 -6
  172. package/dist/modes/json-event.js.map +1 -1
  173. package/dist/package-manager-cli.d.ts +1 -0
  174. package/dist/package-manager-cli.d.ts.map +1 -1
  175. package/dist/package-manager-cli.js +197 -5
  176. package/dist/package-manager-cli.js.map +1 -1
  177. package/dist/utils/frontmatter.d.ts.map +1 -1
  178. package/dist/utils/frontmatter.js +2 -1
  179. package/dist/utils/frontmatter.js.map +1 -1
  180. package/dist/utils/management-http.d.ts +5 -3
  181. package/dist/utils/management-http.d.ts.map +1 -1
  182. package/dist/utils/management-http.js +14 -10
  183. package/dist/utils/management-http.js.map +1 -1
  184. package/dist/utils/shell.d.ts +3 -0
  185. package/dist/utils/shell.d.ts.map +1 -1
  186. package/dist/utils/shell.js +17 -5
  187. package/dist/utils/shell.js.map +1 -1
  188. package/dist/utils/syntax-highlight.d.ts +1 -0
  189. package/dist/utils/syntax-highlight.d.ts.map +1 -1
  190. package/dist/utils/syntax-highlight.js +60 -1
  191. package/dist/utils/syntax-highlight.js.map +1 -1
  192. package/dist/utils/text.d.ts +8 -0
  193. package/dist/utils/text.d.ts.map +1 -0
  194. package/dist/utils/text.js +9 -0
  195. package/dist/utils/text.js.map +1 -0
  196. package/docs/compaction.md +15 -0
  197. package/docs/custom-provider.md +5 -2
  198. package/docs/extensions.md +17 -7
  199. package/docs/json.md +7 -2
  200. package/docs/keybindings.md +8 -8
  201. package/docs/llama-cpp.md +2 -0
  202. package/docs/models.md +8 -2
  203. package/docs/packages.md +2 -2
  204. package/docs/rpc.md +9 -3
  205. package/docs/sdk.md +7 -2
  206. package/docs/settings.md +11 -3
  207. package/docs/skills.md +4 -3
  208. package/docs/terminal-setup.md +11 -7
  209. package/docs/usage.md +6 -2
  210. package/docs/windows.md +24 -2
  211. package/examples/extensions/border-status-editor.ts +1 -1
  212. package/examples/extensions/custom-provider-anthropic/package-lock.json +2 -2
  213. package/examples/extensions/custom-provider-anthropic/package.json +1 -1
  214. package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
  215. package/examples/extensions/git-checkpoint.ts +2 -2
  216. package/examples/extensions/gondolin/package-lock.json +2 -2
  217. package/examples/extensions/gondolin/package.json +1 -1
  218. package/examples/extensions/notify.ts +3 -1
  219. package/examples/extensions/sandbox/package-lock.json +2 -2
  220. package/examples/extensions/sandbox/package.json +1 -1
  221. package/examples/extensions/subagent/README.md +1 -1
  222. package/examples/extensions/subagent/index.ts +6 -1
  223. package/examples/extensions/titlebar-spinner.ts +1 -1
  224. package/examples/extensions/with-deps/package-lock.json +2 -2
  225. package/examples/extensions/with-deps/package.json +1 -1
  226. package/examples/rpc-extension-ui.ts +1 -1
  227. package/examples/sdk/06-extensions.ts +1 -1
  228. package/examples/sdk/README.md +1 -1
  229. package/npm-shrinkwrap.json +276 -390
  230. package/package.json +9 -11
@@ -12,20 +12,19 @@
12
12
  *
13
13
  * Modes use this class and add their own I/O layer on top.
14
14
  */
15
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
15
+ import { readFileSync } from "node:fs";
16
16
  import { basename, dirname } from "node:path";
17
17
  import { contentText } from "@earendil-works/pi-ai";
18
18
  import { clampThinkingLevel, cleanupSessionResources, getSupportedThinkingLevels, isContextOverflow, isRecoverableLength, isRetryableAssistantError, modelsAreEqual, resetApiProviders, } 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
- import { resolvePath } from "../utils/paths.js";
22
21
  import { sleep } from "../utils/sleep.js";
23
22
  import { normalizeToolResultImages } from "../utils/tool-result-images.js";
24
23
  import { formatNoApiKeyFoundMessage, formatNoModelSelectedMessage } from "./auth-guidance.js";
25
24
  import { executeBashWithOperations } from "./bash-executor.js";
26
25
  import { calculateContextTokens, collectEntriesForBranchSummary, compact, estimateContextTokens, estimateTokens, generateBranchSummary, prepareCompaction, shouldCompact, } from "./compaction/index.js";
27
26
  import { evictionBudget, installContextPipeline, isDefaultStreamFunction } from "./context/pipeline.js";
28
- import { DEFAULT_THINKING_LEVEL } from "./defaults.js";
27
+ import { DEFAULT_THINKING_LEVEL, THINKING_LEVEL_OPTIONS } from "./defaults.js";
29
28
  import { SessionEvidenceSink } from "./evidence.js";
30
29
  import { exportSessionToHtml } from "./export-html/index.js";
31
30
  import { createToolHtmlRenderer } from "./export-html/tool-renderer.js";
@@ -36,7 +35,8 @@ import { evaluateToolCall } from "./permissions/gate.js";
36
35
  import { createInteractiveResponder } from "./permissions/responder.js";
37
36
  import { resolveEffectiveModeWithOrigin } from "./permissions/startup.js";
38
37
  import { expandPromptTemplate } from "./prompt-templates.js";
39
- import { CURRENT_SESSION_VERSION, getLatestCompactionEntry, TODO_CUSTOM_ENTRY_TYPE, } from "./session-manager.js";
38
+ import { exportSessionToJsonl } from "./session-export.js";
39
+ import { getLatestCompactionEntry, TODO_CUSTOM_ENTRY_TYPE } from "./session-manager.js";
40
40
  import { slugifySkillCommandName } from "./skills.js";
41
41
  import { createSyntheticSourceInfo } from "./source-info.js";
42
42
  import { buildSystemPrompt } from "./system-prompt.js";
@@ -80,8 +80,6 @@ function estimateMessagesTokens(messages) {
80
80
  // ============================================================================
81
81
  // Constants
82
82
  // ============================================================================
83
- /** Standard thinking levels */
84
- const THINKING_LEVELS = ["off", "minimal", "low", "medium", "high"];
85
83
  // ============================================================================
86
84
  // AgentSession Class
87
85
  // ============================================================================
@@ -406,6 +404,11 @@ export class AgentSession {
406
404
  followUp: [...this._followUpMessages],
407
405
  });
408
406
  }
407
+ async _emitSessionCompactFailed(event) {
408
+ if (this._extensionRunner.hasHandlers("session_compact_failed")) {
409
+ await this._extensionRunner.emit({ type: "session_compact_failed", ...event });
410
+ }
411
+ }
409
412
  _getIdleWaitPromise() {
410
413
  if (!this._idleWaitPromise) {
411
414
  this._idleWaitPromise = new Promise((resolve) => {
@@ -1328,19 +1331,24 @@ export class AgentSession {
1328
1331
  }
1329
1332
  /**
1330
1333
  * Set model directly.
1331
- * Validates that auth is configured, saves to session and settings.
1334
+ * Validates that auth is configured and saves to the session transcript.
1335
+ * Persists to global defaults only when options.persist is true.
1332
1336
  * @throws Error if no auth is configured for the model
1333
1337
  */
1334
- async setModel(model) {
1338
+ async setModel(model, options = {}) {
1335
1339
  if (!(await this._modelRuntime.checkAuth(model.provider))) {
1336
1340
  throw new Error(`No API key for ${model.provider}/${model.id}`);
1337
1341
  }
1338
1342
  const previousModel = this.model;
1339
- const thinkingLevel = this._getThinkingLevelForModelSwitch();
1343
+ const thinkingLevel = this._getThinkingLevelForModelSwitch(model);
1340
1344
  this.agent.state.model = model;
1341
1345
  this.sessionManager.appendModelChange(model.provider, model.id);
1342
- this.settingsManager.setDefaultModelAndProvider(model.provider, model.id);
1343
- // Re-clamp thinking level for new model's capabilities
1346
+ if (options.persist) {
1347
+ this.settingsManager.setDefaultModelAndProvider(model.provider, model.id);
1348
+ }
1349
+ // Apply thinking level for the new model.
1350
+ // Per-model thinking level overrides take priority over the global default.
1351
+ // Model persistence does not implicitly rewrite the global thinking default.
1344
1352
  this.setThinkingLevel(thinkingLevel);
1345
1353
  await this._emitModelSelect(model, previousModel, "set");
1346
1354
  }
@@ -1350,13 +1358,13 @@ export class AgentSession {
1350
1358
  * @param direction - "forward" (default) or "backward"
1351
1359
  * @returns The new model info, or undefined if only one model available
1352
1360
  */
1353
- async cycleModel(direction = "forward") {
1361
+ async cycleModel(direction = "forward", options = {}) {
1354
1362
  if (this._scopedModels.length > 0) {
1355
- return this._cycleScopedModel(direction);
1363
+ return this._cycleScopedModel(direction, options);
1356
1364
  }
1357
- return this._cycleAvailableModel(direction);
1365
+ return this._cycleAvailableModel(direction, options);
1358
1366
  }
1359
- async _cycleScopedModel(direction) {
1367
+ async _cycleScopedModel(direction, options) {
1360
1368
  const availableIds = new Set(this._modelRuntime.getAvailableSnapshot().map((model) => `${model.provider}\0${model.id}`));
1361
1369
  const scopedModels = this._scopedModels.filter((scoped) => availableIds.has(`${scoped.model.provider}\0${scoped.model.id}`));
1362
1370
  if (scopedModels.length <= 1)
@@ -1368,20 +1376,23 @@ export class AgentSession {
1368
1376
  const len = scopedModels.length;
1369
1377
  const nextIndex = direction === "forward" ? (currentIndex + 1) % len : (currentIndex - 1 + len) % len;
1370
1378
  const next = scopedModels[nextIndex];
1371
- const thinkingLevel = this._getThinkingLevelForModelSwitch(next.thinkingLevel);
1379
+ const thinkingLevel = this._getThinkingLevelForModelSwitch(next.model, next.thinkingLevel);
1372
1380
  // Apply model
1373
1381
  this.agent.state.model = next.model;
1374
1382
  this.sessionManager.appendModelChange(next.model.provider, next.model.id);
1375
- this.settingsManager.setDefaultModelAndProvider(next.model.provider, next.model.id);
1376
- // Apply thinking level.
1377
- // - Explicit scoped model thinking level overrides current session level
1378
- // - Undefined scoped model thinking level inherits the current session preference
1383
+ if (options.persist) {
1384
+ this.settingsManager.setDefaultModelAndProvider(next.model.provider, next.model.id);
1385
+ }
1386
+ // Apply thinking level for the new model.
1387
+ // - Explicit scoped model thinking level overrides defaults
1388
+ // - Per-model thinking level overrides take priority over the global default
1379
1389
  // setThinkingLevel clamps to model capabilities.
1390
+ // Model persistence does not implicitly rewrite the global thinking default.
1380
1391
  this.setThinkingLevel(thinkingLevel);
1381
1392
  await this._emitModelSelect(next.model, currentModel, "cycle");
1382
1393
  return { model: next.model, thinkingLevel: this.thinkingLevel, isScoped: true };
1383
1394
  }
1384
- async _cycleAvailableModel(direction) {
1395
+ async _cycleAvailableModel(direction, options) {
1385
1396
  const availableModels = this._modelRuntime.getAvailableSnapshot();
1386
1397
  if (availableModels.length <= 1)
1387
1398
  return undefined;
@@ -1392,11 +1403,14 @@ export class AgentSession {
1392
1403
  const len = availableModels.length;
1393
1404
  const nextIndex = direction === "forward" ? (currentIndex + 1) % len : (currentIndex - 1 + len) % len;
1394
1405
  const nextModel = availableModels[nextIndex];
1395
- const thinkingLevel = this._getThinkingLevelForModelSwitch();
1406
+ const thinkingLevel = this._getThinkingLevelForModelSwitch(nextModel);
1396
1407
  this.agent.state.model = nextModel;
1397
1408
  this.sessionManager.appendModelChange(nextModel.provider, nextModel.id);
1398
- this.settingsManager.setDefaultModelAndProvider(nextModel.provider, nextModel.id);
1399
- // Re-clamp thinking level for new model's capabilities
1409
+ if (options.persist) {
1410
+ this.settingsManager.setDefaultModelAndProvider(nextModel.provider, nextModel.id);
1411
+ }
1412
+ // Apply thinking level for the new model.
1413
+ // Model persistence does not implicitly rewrite the global thinking default.
1400
1414
  this.setThinkingLevel(thinkingLevel);
1401
1415
  await this._emitModelSelect(nextModel, currentModel, "cycle");
1402
1416
  return { model: nextModel, thinkingLevel: this.thinkingLevel, isScoped: false };
@@ -1407,20 +1421,21 @@ export class AgentSession {
1407
1421
  /**
1408
1422
  * Set thinking level.
1409
1423
  * Clamps to model capabilities based on available thinking levels.
1410
- * Saves to session and settings only if the level actually changes.
1424
+ * Saves the clamped level to the session transcript only if the level actually changes.
1425
+ * Persists the requested level to global defaults only when options.persist is true.
1411
1426
  */
1412
- setThinkingLevel(level) {
1427
+ setThinkingLevel(level, options = {}) {
1413
1428
  const availableLevels = this.getAvailableThinkingLevels();
1414
1429
  const effectiveLevel = availableLevels.includes(level) ? level : this._clampThinkingLevel(level, availableLevels);
1415
1430
  // Only persist if actually changing
1416
1431
  const previousLevel = this.agent.state.thinkingLevel;
1417
1432
  const isChanging = effectiveLevel !== previousLevel;
1418
1433
  this.agent.state.thinkingLevel = effectiveLevel;
1434
+ if (options.persist) {
1435
+ this.settingsManager.setDefaultThinkingLevel(level);
1436
+ }
1419
1437
  if (isChanging) {
1420
1438
  this.sessionManager.appendThinkingLevelChange(effectiveLevel);
1421
- if (this.supportsThinking() || effectiveLevel !== "off") {
1422
- this.settingsManager.setDefaultThinkingLevel(effectiveLevel);
1423
- }
1424
1439
  this._emit({ type: "thinking_level_changed", level: effectiveLevel });
1425
1440
  void this._extensionRunner.emit({
1426
1441
  type: "thinking_level_select",
@@ -1433,14 +1448,14 @@ export class AgentSession {
1433
1448
  * Cycle to next thinking level.
1434
1449
  * @returns New level, or undefined if model doesn't support thinking
1435
1450
  */
1436
- cycleThinkingLevel() {
1451
+ cycleThinkingLevel(options = {}) {
1437
1452
  if (!this.supportsThinking())
1438
1453
  return undefined;
1439
1454
  const levels = this.getAvailableThinkingLevels();
1440
1455
  const currentIndex = levels.indexOf(this.thinkingLevel);
1441
1456
  const nextIndex = (currentIndex + 1) % levels.length;
1442
1457
  const nextLevel = levels[nextIndex];
1443
- this.setThinkingLevel(nextLevel);
1458
+ this.setThinkingLevel(nextLevel, options);
1444
1459
  return nextLevel;
1445
1460
  }
1446
1461
  /**
@@ -1449,7 +1464,7 @@ export class AgentSession {
1449
1464
  */
1450
1465
  getAvailableThinkingLevels() {
1451
1466
  if (!this.model)
1452
- return THINKING_LEVELS;
1467
+ return [...THINKING_LEVEL_OPTIONS];
1453
1468
  return getSupportedThinkingLevels(this.model);
1454
1469
  }
1455
1470
  /**
@@ -1458,14 +1473,18 @@ export class AgentSession {
1458
1473
  supportsThinking() {
1459
1474
  return !!this.model?.reasoning;
1460
1475
  }
1461
- _getThinkingLevelForModelSwitch(explicitLevel) {
1476
+ _getThinkingLevelForModelSwitch(targetModel, explicitLevel) {
1462
1477
  if (explicitLevel !== undefined) {
1463
1478
  return explicitLevel;
1464
1479
  }
1465
- if (!this.supportsThinking()) {
1466
- return this.settingsManager.getDefaultThinkingLevel() ?? DEFAULT_THINKING_LEVEL;
1480
+ // Per-model default takes priority when switching to a model that has one
1481
+ if (targetModel) {
1482
+ const perModel = this.settingsManager.getModelThinkingLevel(targetModel.provider, targetModel.id);
1483
+ if (perModel !== undefined) {
1484
+ return perModel;
1485
+ }
1467
1486
  }
1468
- return this.thinkingLevel;
1487
+ return this.settingsManager.getDefaultThinkingLevel() ?? this.thinkingLevel ?? DEFAULT_THINKING_LEVEL;
1469
1488
  }
1470
1489
  _clampThinkingLevel(level, _availableLevels) {
1471
1490
  return this.model ? clampThinkingLevel(this.model, level) : "off";
@@ -1496,15 +1515,30 @@ export class AgentSession {
1496
1515
  // =========================================================================
1497
1516
  // Compaction
1498
1517
  // =========================================================================
1518
+ /** Generate Pi's built-in compaction summary for manual and automatic compaction. */
1519
+ async _runDefaultCompaction(preparation, requestModel, apiKey, headers, customInstructions, signal, env, reason) {
1520
+ return compact(preparation, requestModel, apiKey, headers, customInstructions, signal, this.thinkingLevel, this.agent.streamFunction, env, this.settingsManager.getRetrySettings(), this._summarizationRetryCallbacks({ source: "compaction", reason }), undefined);
1521
+ }
1499
1522
  /**
1500
1523
  * Manually compact the session context.
1501
- * Aborts current agent operation first.
1524
+ *
1525
+ * This is the manual entry point used by `/compact`, RPC, and extensions. It is
1526
+ * separate from automatic threshold/overflow compaction, which enters through
1527
+ * `_checkCompaction()` and `_runAutoCompaction()`. After preparation and the
1528
+ * `session_before_compact` hook, both paths call the lower-level `compact()`
1529
+ * function imported from `./compaction/index.ts`, unless the hook cancels or
1530
+ * supplies a custom result.
1531
+ *
1532
+ * Aborts the current agent operation first. Manual compaction never retries or
1533
+ * continues the interrupted agent turn.
1534
+ *
1502
1535
  * @param customInstructions Optional instructions for the compaction summary
1503
1536
  */
1504
1537
  async compact(customInstructions) {
1505
1538
  await this.abort();
1506
1539
  this._compactionAbortController = new AbortController();
1507
1540
  this._emit({ type: "compaction_start", reason: "manual" });
1541
+ let fromExtension = false;
1508
1542
  try {
1509
1543
  if (!this.model) {
1510
1544
  throw new Error(formatNoModelSelectedMessage());
@@ -1522,7 +1556,6 @@ export class AgentSession {
1522
1556
  throw new Error("Nothing to compact (session too small)");
1523
1557
  }
1524
1558
  let extensionCompaction;
1525
- let fromExtension = false;
1526
1559
  if (this._extensionRunner.hasHandlers("session_before_compact")) {
1527
1560
  const result = (await this._extensionRunner.emit({
1528
1561
  type: "session_before_compact",
@@ -1555,8 +1588,8 @@ export class AgentSession {
1555
1588
  details = extensionCompaction.details;
1556
1589
  }
1557
1590
  else {
1558
- // Generate compaction result
1559
- const result = await compact(preparation, requestModel, apiKey, headers, customInstructions, this._compactionAbortController.signal, this.thinkingLevel, this.agent.streamFunction, env, this.settingsManager.getRetrySettings(), this._summarizationRetryCallbacks({ source: "compaction", reason: "manual" }));
1591
+ // Shared default summary generator, also used by automatic compaction.
1592
+ const result = await this._runDefaultCompaction(preparation, requestModel, apiKey, headers, customInstructions, this._compactionAbortController.signal, env, "manual");
1560
1593
  summary = result.summary;
1561
1594
  firstKeptEntryId = result.firstKeptEntryId;
1562
1595
  tokensBefore = result.tokensBefore;
@@ -1604,6 +1637,7 @@ export class AgentSession {
1604
1637
  catch (error) {
1605
1638
  const message = error instanceof Error ? error.message : String(error);
1606
1639
  const aborted = message === "Compaction cancelled" || (error instanceof Error && error.name === "AbortError");
1640
+ const errorMessage = aborted ? undefined : `Compaction failed: ${message}`;
1607
1641
  this._compactionAbortController = undefined;
1608
1642
  this._emit({
1609
1643
  type: "compaction_end",
@@ -1611,7 +1645,14 @@ export class AgentSession {
1611
1645
  result: undefined,
1612
1646
  aborted,
1613
1647
  willRetry: false,
1614
- errorMessage: aborted ? undefined : `Compaction failed: ${message}`,
1648
+ errorMessage,
1649
+ });
1650
+ await this._emitSessionCompactFailed({
1651
+ reason: "manual",
1652
+ errorMessage,
1653
+ aborted,
1654
+ willRetry: false,
1655
+ fromExtension,
1615
1656
  });
1616
1657
  throw error;
1617
1658
  }
@@ -1633,16 +1674,25 @@ export class AgentSession {
1633
1674
  this._branchSummaryAbortController?.abort();
1634
1675
  }
1635
1676
  /**
1636
- * Check if compaction is needed and run it.
1637
- * Called after agent_end and before prompt submission.
1677
+ * Dispatch automatic compaction after `agent_end` or before prompt submission.
1678
+ * Manual compaction does not call this method; it enters through `compact()`.
1679
+ *
1680
+ * Automatic cases:
1681
+ * 1. Overflow with retry: a context-overflow error or recoverable length stop;
1682
+ * remove the failed assistant message, compact, and retry the turn once.
1683
+ * 2. Overflow without retry: a successful response exceeded the configured
1684
+ * context window; compact but preserve the completed response.
1685
+ * 3. Threshold without retry: valid or estimated context usage crossed the
1686
+ * configured threshold; compact without retrying the completed response.
1638
1687
  *
1639
- * Two cases:
1640
- * 1. Recoverable failure: LLM returned context overflow or stopped below its desired output limit;
1641
- * remove the assistant message, compact, and auto-retry once
1642
- * 2. Threshold: Context over threshold, compact, NO auto-retry (user continues manually)
1688
+ * Each case calls `_runAutoCompaction()`. After preparation and the
1689
+ * `session_before_compact` hook, that method calls the lower-level `compact()`
1690
+ * function imported from `./compaction/index.ts`, unless the hook cancels or
1691
+ * supplies a custom result.
1643
1692
  *
1644
1693
  * @param assistantMessage The assistant message to check
1645
1694
  * @param skipAbortedCheck If false, include aborted messages (for pre-prompt check). Default: true
1695
+ * @returns Whether the post-run loop should call `agent.continue()` for overflow recovery or queued messages
1646
1696
  */
1647
1697
  async _checkCompaction(assistantMessage, skipAbortedCheck = true) {
1648
1698
  const settings = this.settingsManager.getCompactionSettings();
@@ -1665,38 +1715,49 @@ export class AgentSession {
1665
1715
  if (assistantIsFromBeforeCompaction) {
1666
1716
  return false;
1667
1717
  }
1668
- // Case 1: Recoverable failure. Explicit/silent context overflow still uses context metadata.
1718
+ // Automatic cases 1 and 2: context overflow.
1669
1719
  // A length stop is recoverable when output ended below the model's original desired limit,
1670
1720
  // independent of the configured context size or any context-clamped provider request limit.
1671
- // A successful response over the configured window should compact but must not retry: the
1672
- // assistant answer already completed and agent.continue() cannot continue from an assistant.
1721
+ const contextOverflow = sameModel && isContextOverflow(assistantMessage, contextWindow);
1673
1722
  const recoverableLength = sameModel && isRecoverableLength(assistantMessage, this.model?.maxTokens ?? 0);
1674
- if (sameModel && (isContextOverflow(assistantMessage, contextWindow) || recoverableLength)) {
1723
+ if (contextOverflow || recoverableLength) {
1675
1724
  const willRetry = assistantMessage.stopReason !== "stop";
1725
+ // Case 2: the response completed successfully. Compact, but do not retry because
1726
+ // agent.continue() cannot continue from a completed assistant response.
1676
1727
  if (!willRetry) {
1677
1728
  return await this._runAutoCompaction("overflow", false);
1678
1729
  }
1679
1730
  if (this._overflowRecoveryAttempted) {
1731
+ const errorMessage = contextOverflow
1732
+ ? "Context overflow recovery failed after one compact-and-retry attempt. Try reducing context or switching to a larger-context model."
1733
+ : "Truncated response recovery failed after one compact-and-retry attempt.";
1680
1734
  this._emit({
1681
1735
  type: "compaction_end",
1682
1736
  reason: "overflow",
1683
1737
  result: undefined,
1684
1738
  aborted: false,
1685
1739
  willRetry: false,
1686
- errorMessage: "Context overflow recovery failed after one compact-and-retry attempt. Try reducing context or switching to a larger-context model.",
1740
+ errorMessage,
1741
+ });
1742
+ await this._emitSessionCompactFailed({
1743
+ reason: "overflow",
1744
+ errorMessage,
1745
+ aborted: false,
1746
+ willRetry: false,
1747
+ fromExtension: false,
1687
1748
  });
1688
1749
  return false;
1689
1750
  }
1751
+ // Case 1: remove the failed or truncated message from agent state, compact, and
1752
+ // retry once. The message remains in session history but is excluded from retry context.
1690
1753
  this._overflowRecoveryAttempted = true;
1691
- // Remove the failed or truncated message from agent state. It remains in session history,
1692
- // but must not be included in the compact-and-retry context.
1693
1754
  const messages = this.agent.state.messages;
1694
1755
  if (messages.length > 0 && messages[messages.length - 1].role === "assistant") {
1695
1756
  this.agent.state.messages = messages.slice(0, -1);
1696
1757
  }
1697
1758
  return await this._runAutoCompaction("overflow", willRetry);
1698
1759
  }
1699
- // Case 2: Threshold - context is getting large
1760
+ // Case 3: threshold compaction without retry.
1700
1761
  // For error messages or all-zero usage messages, estimate from the last valid response.
1701
1762
  // This ensures sessions that hit persistent API errors (e.g. 529) or malformed zero-usage
1702
1763
  // responses can still compact and do not reset context accounting.
@@ -1705,16 +1766,18 @@ export class AgentSession {
1705
1766
  if (assistantMessage.stopReason === "error" || directContextTokens === 0) {
1706
1767
  const messages = this.agent.state.messages;
1707
1768
  const estimate = estimateContextTokens(messages);
1708
- if (estimate.lastUsageIndex === null)
1709
- return false; // No usage data at all
1710
- // Verify the usage source is post-compaction. Kept pre-compaction messages
1711
- // have stale usage reflecting the old (larger) context and would falsely
1712
- // trigger compaction right after one just finished.
1713
- const usageMsg = messages[estimate.lastUsageIndex];
1714
- if (compactionEntry &&
1715
- usageMsg.role === "assistant" &&
1716
- usageMsg.timestamp <= new Date(compactionEntry.timestamp).getTime()) {
1717
- return false;
1769
+ // Without provider usage, estimate.tokens is the pure message-size estimate.
1770
+ // Only usage-backed estimates need the stale pre-compaction check.
1771
+ if (estimate.lastUsageIndex !== null) {
1772
+ // Verify the usage source is post-compaction. Kept pre-compaction messages
1773
+ // have stale usage reflecting the old (larger) context and would falsely
1774
+ // trigger compaction right after one just finished.
1775
+ const usageMsg = messages[estimate.lastUsageIndex];
1776
+ if (compactionEntry &&
1777
+ usageMsg.role === "assistant" &&
1778
+ usageMsg.timestamp <= new Date(compactionEntry.timestamp).getTime()) {
1779
+ return false;
1780
+ }
1718
1781
  }
1719
1782
  contextTokens = estimate.tokens;
1720
1783
  }
@@ -1727,11 +1790,19 @@ export class AgentSession {
1727
1790
  return false;
1728
1791
  }
1729
1792
  /**
1730
- * Internal: Run auto-compaction with events.
1793
+ * Execute threshold or overflow compaction. Manual compaction uses
1794
+ * `AgentSession.compact()` instead. Both paths call the lower-level `compact()`
1795
+ * function imported from `./compaction/index.ts` after preparation and extension
1796
+ * interception.
1797
+ *
1798
+ * @param reason Automatic trigger selected by `_checkCompaction()`
1799
+ * @param willRetry Whether to continue the interrupted turn after overflow compaction
1800
+ * @returns Whether the post-run loop should call `agent.continue()`
1731
1801
  */
1732
1802
  async _runAutoCompaction(reason, willRetry) {
1733
1803
  const settings = this.settingsManager.getCompactionSettings();
1734
1804
  let started = false;
1805
+ let fromExtension = false;
1735
1806
  try {
1736
1807
  if (!this.model) {
1737
1808
  return false;
@@ -1746,7 +1817,6 @@ export class AgentSession {
1746
1817
  this._autoCompactionAbortController = new AbortController();
1747
1818
  started = true;
1748
1819
  let extensionCompaction;
1749
- let fromExtension = false;
1750
1820
  if (this._extensionRunner.hasHandlers("session_before_compact")) {
1751
1821
  const extensionResult = (await this._extensionRunner.emit({
1752
1822
  type: "session_before_compact",
@@ -1765,6 +1835,12 @@ export class AgentSession {
1765
1835
  aborted: true,
1766
1836
  willRetry: false,
1767
1837
  });
1838
+ await this._emitSessionCompactFailed({
1839
+ reason,
1840
+ aborted: true,
1841
+ willRetry: false,
1842
+ fromExtension: false,
1843
+ });
1768
1844
  return false;
1769
1845
  }
1770
1846
  if (extensionResult?.compaction) {
@@ -1786,8 +1862,8 @@ export class AgentSession {
1786
1862
  details = extensionCompaction.details;
1787
1863
  }
1788
1864
  else {
1789
- // Generate compaction result
1790
- const compactResult = await compact(preparation, requestModel, apiKey, headers, undefined, this._autoCompactionAbortController.signal, this.thinkingLevel, this.agent.streamFunction, env, this.settingsManager.getRetrySettings(), this._summarizationRetryCallbacks({ source: "compaction", reason }));
1865
+ // Shared default summary generator, also used by manual compaction.
1866
+ const compactResult = await this._runDefaultCompaction(preparation, requestModel, apiKey, headers, undefined, this._autoCompactionAbortController.signal, env, reason);
1791
1867
  summary = compactResult.summary;
1792
1868
  firstKeptEntryId = compactResult.firstKeptEntryId;
1793
1869
  tokensBefore = compactResult.tokensBefore;
@@ -1802,6 +1878,12 @@ export class AgentSession {
1802
1878
  aborted: true,
1803
1879
  willRetry: false,
1804
1880
  });
1881
+ await this._emitSessionCompactFailed({
1882
+ reason,
1883
+ aborted: true,
1884
+ willRetry: false,
1885
+ fromExtension,
1886
+ });
1805
1887
  return false;
1806
1888
  }
1807
1889
  this.sessionManager.appendCompaction(summary, firstKeptEntryId, tokensBefore, details, fromExtension, usage);
@@ -1848,15 +1930,23 @@ export class AgentSession {
1848
1930
  catch (error) {
1849
1931
  const errorMessage = error instanceof Error ? error.message : "compaction failed";
1850
1932
  if (started) {
1933
+ const formattedErrorMessage = reason === "overflow"
1934
+ ? `Context overflow recovery failed: ${errorMessage}`
1935
+ : `Auto-compaction failed: ${errorMessage}`;
1851
1936
  this._emit({
1852
1937
  type: "compaction_end",
1853
1938
  reason,
1854
1939
  result: undefined,
1855
1940
  aborted: false,
1856
1941
  willRetry: false,
1857
- errorMessage: reason === "overflow"
1858
- ? `Context overflow recovery failed: ${errorMessage}`
1859
- : `Auto-compaction failed: ${errorMessage}`,
1942
+ errorMessage: formattedErrorMessage,
1943
+ });
1944
+ await this._emitSessionCompactFailed({
1945
+ reason,
1946
+ errorMessage: formattedErrorMessage,
1947
+ aborted: false,
1948
+ willRetry: false,
1949
+ fromExtension,
1860
1950
  });
1861
1951
  }
1862
1952
  return false;
@@ -2780,29 +2870,7 @@ export class AgentSession {
2780
2870
  * @returns The resolved output file path.
2781
2871
  */
2782
2872
  exportToJsonl(outputPath) {
2783
- const filePath = resolvePath(outputPath ?? `session-${new Date().toISOString().replace(/[:.]/g, "-")}.jsonl`, process.cwd());
2784
- const dir = dirname(filePath);
2785
- if (!existsSync(dir)) {
2786
- mkdirSync(dir, { recursive: true });
2787
- }
2788
- const header = {
2789
- type: "session",
2790
- version: CURRENT_SESSION_VERSION,
2791
- id: this.sessionManager.getSessionId(),
2792
- timestamp: new Date().toISOString(),
2793
- cwd: this.sessionManager.getCwd(),
2794
- };
2795
- const branchEntries = this.sessionManager.getBranch();
2796
- const lines = [JSON.stringify(header)];
2797
- // Re-chain parentIds to form a linear sequence
2798
- let prevId = null;
2799
- for (const entry of branchEntries) {
2800
- const linear = { ...entry, parentId: prevId };
2801
- lines.push(JSON.stringify(linear));
2802
- prevId = entry.id;
2803
- }
2804
- writeFileSync(filePath, `${lines.join("\n")}\n`);
2805
- return filePath;
2873
+ return exportSessionToJsonl(this.sessionManager, outputPath);
2806
2874
  }
2807
2875
  // =========================================================================
2808
2876
  // Utilities