@oh-my-pi/pi-coding-agent 13.18.0 → 14.0.2

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 (235) hide show
  1. package/CHANGELOG.md +316 -1
  2. package/package.json +86 -24
  3. package/scripts/format-prompts.ts +2 -2
  4. package/src/autoresearch/apply-contract-to-state.ts +24 -0
  5. package/src/autoresearch/contract.ts +0 -44
  6. package/src/autoresearch/dashboard.ts +1 -2
  7. package/src/autoresearch/git.ts +116 -30
  8. package/src/autoresearch/helpers.ts +49 -0
  9. package/src/autoresearch/index.ts +28 -187
  10. package/src/autoresearch/prompt.md +26 -9
  11. package/src/autoresearch/state.ts +0 -6
  12. package/src/autoresearch/tools/init-experiment.ts +202 -117
  13. package/src/autoresearch/tools/log-experiment.ts +123 -178
  14. package/src/autoresearch/tools/run-experiment.ts +48 -10
  15. package/src/autoresearch/types.ts +2 -2
  16. package/src/capability/index.ts +4 -2
  17. package/src/cli/file-processor.ts +3 -3
  18. package/src/cli/grep-cli.ts +8 -8
  19. package/src/cli/grievances-cli.ts +78 -0
  20. package/src/cli/read-cli.ts +67 -0
  21. package/src/cli/setup-cli.ts +4 -4
  22. package/src/cli/update-cli.ts +3 -3
  23. package/src/cli.ts +2 -0
  24. package/src/commands/grep.ts +6 -1
  25. package/src/commands/grievances.ts +20 -0
  26. package/src/commands/read.ts +33 -0
  27. package/src/commit/agentic/agent.ts +5 -8
  28. package/src/commit/agentic/index.ts +22 -26
  29. package/src/commit/agentic/tools/analyze-file.ts +3 -3
  30. package/src/commit/agentic/tools/git-file-diff.ts +3 -6
  31. package/src/commit/agentic/tools/git-hunk.ts +3 -3
  32. package/src/commit/agentic/tools/git-overview.ts +6 -9
  33. package/src/commit/agentic/tools/index.ts +6 -8
  34. package/src/commit/agentic/tools/propose-commit.ts +4 -7
  35. package/src/commit/agentic/tools/recent-commits.ts +3 -3
  36. package/src/commit/agentic/tools/split-commit.ts +4 -4
  37. package/src/commit/agentic/validation.ts +1 -1
  38. package/src/commit/analysis/conventional.ts +4 -4
  39. package/src/commit/analysis/summary.ts +3 -3
  40. package/src/commit/changelog/generate.ts +4 -4
  41. package/src/commit/changelog/index.ts +5 -9
  42. package/src/commit/map-reduce/map-phase.ts +4 -4
  43. package/src/commit/map-reduce/reduce-phase.ts +4 -4
  44. package/src/commit/pipeline.ts +13 -16
  45. package/src/config/keybindings.ts +7 -6
  46. package/src/config/prompt-templates.ts +44 -226
  47. package/src/config/resolve-config-value.ts +4 -2
  48. package/src/config/settings-schema.ts +98 -2
  49. package/src/config/settings.ts +25 -26
  50. package/src/dap/client.ts +674 -0
  51. package/src/dap/config.ts +150 -0
  52. package/src/dap/defaults.json +211 -0
  53. package/src/dap/index.ts +4 -0
  54. package/src/dap/session.ts +1255 -0
  55. package/src/dap/types.ts +600 -0
  56. package/src/debug/log-viewer.ts +3 -2
  57. package/src/discovery/builtin.ts +1 -2
  58. package/src/discovery/codex.ts +2 -2
  59. package/src/discovery/github.ts +2 -1
  60. package/src/discovery/helpers.ts +2 -2
  61. package/src/discovery/opencode.ts +2 -2
  62. package/src/edit/diff.ts +818 -0
  63. package/src/edit/index.ts +309 -0
  64. package/src/edit/line-hash.ts +67 -0
  65. package/src/edit/modes/chunk.ts +454 -0
  66. package/src/{patch → edit/modes}/hashline.ts +741 -361
  67. package/src/{patch/applicator.ts → edit/modes/patch.ts} +420 -117
  68. package/src/{patch/fuzzy.ts → edit/modes/replace.ts} +519 -197
  69. package/src/{patch → edit}/normalize.ts +97 -76
  70. package/src/{patch/shared.ts → edit/renderer.ts} +181 -108
  71. package/src/exec/bash-executor.ts +4 -2
  72. package/src/exec/idle-timeout-watchdog.ts +126 -0
  73. package/src/exec/non-interactive-env.ts +5 -0
  74. package/src/extensibility/custom-commands/bundled/ci-green/index.ts +6 -18
  75. package/src/extensibility/custom-commands/bundled/review/index.ts +45 -43
  76. package/src/extensibility/custom-commands/loader.ts +1 -2
  77. package/src/extensibility/custom-tools/loader.ts +34 -11
  78. package/src/extensibility/custom-tools/types.ts +1 -1
  79. package/src/extensibility/extensions/loader.ts +9 -4
  80. package/src/extensibility/extensions/runner.ts +24 -1
  81. package/src/extensibility/extensions/types.ts +4 -2
  82. package/src/extensibility/hooks/loader.ts +5 -6
  83. package/src/extensibility/hooks/types.ts +2 -2
  84. package/src/extensibility/plugins/doctor.ts +2 -1
  85. package/src/extensibility/plugins/marketplace/fetcher.ts +2 -57
  86. package/src/extensibility/plugins/marketplace/source-resolver.ts +4 -4
  87. package/src/extensibility/slash-commands.ts +3 -7
  88. package/src/index.ts +3 -1
  89. package/src/internal-urls/docs-index.generated.ts +11 -11
  90. package/src/ipy/executor.ts +58 -17
  91. package/src/ipy/gateway-coordinator.ts +6 -4
  92. package/src/ipy/kernel.ts +45 -22
  93. package/src/ipy/runtime.ts +2 -2
  94. package/src/lsp/client.ts +7 -4
  95. package/src/lsp/clients/lsp-linter-client.ts +4 -4
  96. package/src/lsp/config.ts +2 -2
  97. package/src/lsp/defaults.json +688 -154
  98. package/src/lsp/index.ts +234 -45
  99. package/src/lsp/lspmux.ts +2 -2
  100. package/src/lsp/startup-events.ts +13 -0
  101. package/src/lsp/types.ts +12 -1
  102. package/src/lsp/utils.ts +8 -1
  103. package/src/main.ts +125 -47
  104. package/src/memories/index.ts +4 -5
  105. package/src/modes/acp/acp-agent.ts +563 -163
  106. package/src/modes/acp/acp-event-mapper.ts +9 -1
  107. package/src/modes/acp/acp-mode.ts +4 -2
  108. package/src/modes/components/agent-dashboard.ts +3 -4
  109. package/src/modes/components/diff.ts +6 -7
  110. package/src/modes/components/footer.ts +9 -29
  111. package/src/modes/components/hook-editor.ts +3 -3
  112. package/src/modes/components/hook-selector.ts +6 -1
  113. package/src/modes/components/read-tool-group.ts +6 -12
  114. package/src/modes/components/session-observer-overlay.ts +472 -0
  115. package/src/modes/components/settings-defs.ts +24 -0
  116. package/src/modes/components/status-line.ts +15 -61
  117. package/src/modes/components/tool-execution.ts +1 -1
  118. package/src/modes/components/welcome.ts +1 -1
  119. package/src/modes/controllers/btw-controller.ts +2 -2
  120. package/src/modes/controllers/command-controller.ts +4 -2
  121. package/src/modes/controllers/event-controller.ts +59 -2
  122. package/src/modes/controllers/extension-ui-controller.ts +1 -0
  123. package/src/modes/controllers/input-controller.ts +15 -8
  124. package/src/modes/controllers/selector-controller.ts +26 -0
  125. package/src/modes/index.ts +20 -2
  126. package/src/modes/interactive-mode.ts +278 -69
  127. package/src/modes/rpc/host-tools.ts +186 -0
  128. package/src/modes/rpc/rpc-client.ts +178 -13
  129. package/src/modes/rpc/rpc-mode.ts +73 -3
  130. package/src/modes/rpc/rpc-types.ts +53 -1
  131. package/src/modes/session-observer-registry.ts +146 -0
  132. package/src/modes/shared.ts +0 -42
  133. package/src/modes/theme/theme.ts +80 -8
  134. package/src/modes/types.ts +4 -2
  135. package/src/modes/utils/keybinding-matchers.ts +9 -0
  136. package/src/prompts/system/custom-system-prompt.md +5 -0
  137. package/src/prompts/system/system-prompt.md +8 -1
  138. package/src/prompts/tools/chunk-edit.md +219 -0
  139. package/src/prompts/tools/debug.md +43 -0
  140. package/src/prompts/tools/grep.md +3 -0
  141. package/src/prompts/tools/lsp.md +5 -5
  142. package/src/prompts/tools/read-chunk.md +17 -0
  143. package/src/prompts/tools/read.md +19 -5
  144. package/src/sdk.ts +216 -165
  145. package/src/secrets/index.ts +1 -1
  146. package/src/secrets/obfuscator.ts +25 -17
  147. package/src/session/agent-session.ts +381 -286
  148. package/src/session/agent-storage.ts +12 -12
  149. package/src/session/compaction/branch-summarization.ts +3 -3
  150. package/src/session/compaction/compaction.ts +5 -6
  151. package/src/session/compaction/utils.ts +3 -3
  152. package/src/session/history-storage.ts +62 -19
  153. package/src/session/messages.ts +3 -3
  154. package/src/session/session-dump-format.ts +203 -0
  155. package/src/session/session-manager.ts +15 -5
  156. package/src/session/session-storage.ts +4 -2
  157. package/src/session/streaming-output.ts +1 -1
  158. package/src/session/tool-choice-queue.ts +213 -0
  159. package/src/slash-commands/builtin-registry.ts +56 -8
  160. package/src/ssh/connection-manager.ts +2 -2
  161. package/src/ssh/sshfs-mount.ts +5 -5
  162. package/src/stt/downloader.ts +4 -4
  163. package/src/stt/recorder.ts +4 -4
  164. package/src/stt/transcriber.ts +2 -2
  165. package/src/system-prompt.ts +25 -13
  166. package/src/task/agents.ts +5 -6
  167. package/src/task/commands.ts +2 -5
  168. package/src/task/executor.ts +32 -4
  169. package/src/task/index.ts +91 -82
  170. package/src/task/template.ts +2 -2
  171. package/src/task/types.ts +25 -0
  172. package/src/task/worktree.ts +131 -149
  173. package/src/tools/ask.ts +2 -3
  174. package/src/tools/ast-edit.ts +7 -7
  175. package/src/tools/ast-grep.ts +7 -7
  176. package/src/tools/auto-generated-guard.ts +36 -41
  177. package/src/tools/await-tool.ts +2 -2
  178. package/src/tools/bash.ts +5 -23
  179. package/src/tools/browser.ts +4 -5
  180. package/src/tools/calculator.ts +2 -3
  181. package/src/tools/cancel-job.ts +2 -2
  182. package/src/tools/checkpoint.ts +3 -3
  183. package/src/tools/debug.ts +1007 -0
  184. package/src/tools/exit-plan-mode.ts +3 -3
  185. package/src/tools/fetch.ts +67 -3
  186. package/src/tools/find.ts +4 -5
  187. package/src/tools/fs-cache-invalidation.ts +5 -0
  188. package/src/tools/gemini-image.ts +13 -5
  189. package/src/tools/gh.ts +130 -308
  190. package/src/tools/grep.ts +57 -9
  191. package/src/tools/index.ts +44 -22
  192. package/src/tools/inspect-image.ts +4 -4
  193. package/src/tools/output-meta.ts +1 -1
  194. package/src/tools/python.ts +19 -6
  195. package/src/tools/read.ts +211 -146
  196. package/src/tools/render-mermaid.ts +2 -3
  197. package/src/tools/render-utils.ts +20 -6
  198. package/src/tools/renderers.ts +3 -1
  199. package/src/tools/report-tool-issue.ts +80 -0
  200. package/src/tools/resolve.ts +70 -39
  201. package/src/tools/search-tool-bm25.ts +2 -2
  202. package/src/tools/ssh.ts +2 -2
  203. package/src/tools/todo-write.ts +2 -2
  204. package/src/tools/tool-timeouts.ts +1 -0
  205. package/src/tools/write.ts +5 -6
  206. package/src/tui/tree-list.ts +3 -1
  207. package/src/utils/clipboard.ts +80 -0
  208. package/src/utils/commit-message-generator.ts +2 -3
  209. package/src/utils/edit-mode.ts +49 -0
  210. package/src/utils/external-editor.ts +11 -5
  211. package/src/utils/file-display-mode.ts +6 -5
  212. package/src/utils/file-mentions.ts +8 -7
  213. package/src/utils/git.ts +1400 -0
  214. package/src/utils/image-loading.ts +98 -0
  215. package/src/utils/title-generator.ts +2 -3
  216. package/src/utils/tools-manager.ts +6 -6
  217. package/src/web/scrapers/choosealicense.ts +1 -1
  218. package/src/web/search/index.ts +3 -3
  219. package/src/web/search/render.ts +6 -4
  220. package/src/autoresearch/command-initialize.md +0 -34
  221. package/src/commit/git/errors.ts +0 -9
  222. package/src/commit/git/index.ts +0 -210
  223. package/src/commit/git/operations.ts +0 -54
  224. package/src/patch/diff.ts +0 -433
  225. package/src/patch/index.ts +0 -888
  226. package/src/patch/parser.ts +0 -532
  227. package/src/patch/types.ts +0 -292
  228. package/src/prompts/agents/oracle.md +0 -77
  229. package/src/tools/gh-cli.ts +0 -125
  230. package/src/tools/pending-action.ts +0 -49
  231. package/src/utils/child-process.ts +0 -88
  232. package/src/utils/frontmatter.ts +0 -117
  233. package/src/utils/image-input.ts +0 -274
  234. package/src/utils/mime.ts +0 -53
  235. package/src/utils/prompt-format.ts +0 -170
@@ -23,7 +23,6 @@ import {
23
23
  type AgentMessage,
24
24
  type AgentState,
25
25
  type AgentTool,
26
- INTENT_FIELD,
27
26
  ThinkingLevel,
28
27
  } from "@oh-my-pi/pi-agent-core";
29
28
  import type {
@@ -50,8 +49,8 @@ import {
50
49
  modelsAreEqual,
51
50
  parseRateLimitReason,
52
51
  } from "@oh-my-pi/pi-ai";
53
- import type { SearchDb } from "@oh-my-pi/pi-natives";
54
- import { abortableSleep, getAgentDbPath, isEnoent, logger } from "@oh-my-pi/pi-utils";
52
+ import { killTree, MacOSPowerAssertion, type SearchDb } from "@oh-my-pi/pi-natives";
53
+ import { abortableSleep, getAgentDbPath, isEnoent, logger, prompt, setNativeKillTree } from "@oh-my-pi/pi-utils";
55
54
  import type { AsyncJob, AsyncJobManager } from "../async";
56
55
  import type { Rule } from "../capability/rule";
57
56
  import { MODEL_ROLE_IDS, type ModelRegistry } from "../config/model-registry";
@@ -59,10 +58,12 @@ import {
59
58
  extractExplicitThinkingSelector,
60
59
  formatModelString,
61
60
  parseModelString,
61
+ type ResolvedModelRoleValue,
62
62
  resolveModelRoleValue,
63
63
  } from "../config/model-resolver";
64
- import { expandPromptTemplate, type PromptTemplate, renderPromptTemplate } from "../config/prompt-templates";
64
+ import { expandPromptTemplate, type PromptTemplate } from "../config/prompt-templates";
65
65
  import type { Settings, SkillsSettings } from "../config/settings";
66
+ import { normalizeDiff, normalizeToLF, ParseError, previewPatch, stripBom } from "../edit";
66
67
  import { type BashResult, executeBash as executeBashCommand } from "../exec/bash-executor";
67
68
  import { exportSessionToHtml } from "../export/html";
68
69
  import type { TtsrManager, TtsrMatchContext } from "../export/ttsr";
@@ -103,7 +104,6 @@ import {
103
104
  selectDiscoverableMCPToolNamesByServer,
104
105
  } from "../mcp/discoverable-tool-metadata";
105
106
  import { getCurrentThemeName, theme } from "../modes/theme/theme";
106
- import { normalizeDiff, normalizeToLF, ParseError, previewPatch, stripBom } from "../patch";
107
107
  import type { PlanModeState } from "../plan-mode/state";
108
108
  import autoHandoffThresholdFocusPrompt from "../prompts/system/auto-handoff-threshold-focus.md" with { type: "text" };
109
109
  import eagerTodoPrompt from "../prompts/system/eager-todo.md" with { type: "text" };
@@ -114,13 +114,15 @@ import planModeToolDecisionReminderPrompt from "../prompts/system/plan-mode-tool
114
114
  type: "text",
115
115
  };
116
116
  import ttsrInterruptTemplate from "../prompts/system/ttsr-interrupt.md" with { type: "text" };
117
- import type { SecretObfuscator } from "../secrets/obfuscator";
117
+ import { deobfuscateSessionContext, type SecretObfuscator } from "../secrets/obfuscator";
118
118
  import { resolveThinkingLevelForModel, toReasoningEffort } from "../thinking";
119
+ import { assertEditableFile } from "../tools/auto-generated-guard";
119
120
  import type { CheckpointState } from "../tools/checkpoint";
120
121
  import { outputMeta } from "../tools/output-meta";
121
122
  import { resolveToCwd } from "../tools/path-utils";
122
- import type { PendingActionStore } from "../tools/pending-action";
123
+ import { isAutoQaEnabled } from "../tools/report-tool-issue";
123
124
  import { getLatestTodoPhasesFromEntries, type TodoItem, type TodoPhase } from "../tools/todo-write";
125
+ import { ToolError } from "../tools/tool-errors";
124
126
  import { clampTimeout } from "../tools/tool-timeouts";
125
127
  import { parseCommandArgs } from "../utils/command-args";
126
128
  import { resolveFileDisplayMode } from "../utils/file-display-mode";
@@ -140,16 +142,13 @@ import {
140
142
  import { DEFAULT_PRUNE_CONFIG, pruneToolOutputs } from "./compaction/pruning";
141
143
  import {
142
144
  type BashExecutionMessage,
143
- type BranchSummaryMessage,
144
- bashExecutionToText,
145
145
  type CompactionSummaryMessage,
146
146
  type CustomMessage,
147
147
  convertToLlm,
148
148
  type FileMentionMessage,
149
- type HookMessage,
150
149
  type PythonExecutionMessage,
151
- pythonExecutionToText,
152
150
  } from "./messages";
151
+ import { formatSessionDumpText } from "./session-dump-format";
153
152
  import type {
154
153
  BranchSummaryEntry,
155
154
  CompactionEntry,
@@ -158,11 +157,12 @@ import type {
158
157
  SessionManager,
159
158
  } from "./session-manager";
160
159
  import { getLatestCompactionEntry } from "./session-manager";
160
+ import { ToolChoiceQueue } from "./tool-choice-queue";
161
161
 
162
162
  /** Session-specific events that extend the core AgentEvent */
163
163
  export type AgentSessionEvent =
164
164
  | AgentEvent
165
- | { type: "auto_compaction_start"; reason: "threshold" | "overflow"; action: "context-full" | "handoff" }
165
+ | { type: "auto_compaction_start"; reason: "threshold" | "overflow" | "idle"; action: "context-full" | "handoff" }
166
166
  | {
167
167
  type: "auto_compaction_end";
168
168
  action: "context-full" | "handoff";
@@ -243,8 +243,6 @@ export interface AgentSessionConfig {
243
243
  ttsrManager?: TtsrManager;
244
244
  /** Secret obfuscator for deobfuscating streaming edit content */
245
245
  obfuscator?: SecretObfuscator;
246
- /** Pending action store for preview/apply workflows */
247
- pendingActionStore?: PendingActionStore;
248
246
  /** Shared native search DB for grep/glob/fuzzyFind-backed workflows. */
249
247
  searchDb?: SearchDb;
250
248
  }
@@ -320,7 +318,7 @@ interface HandoffOptions {
320
318
 
321
319
  /** Standard thinking levels */
322
320
 
323
- const AUTO_HANDOFF_THRESHOLD_FOCUS = renderPromptTemplate(autoHandoffThresholdFocusPrompt);
321
+ const AUTO_HANDOFF_THRESHOLD_FOCUS = prompt.render(autoHandoffThresholdFocusPrompt);
324
322
 
325
323
  type RetryFallbackChains = Record<string, string[]>;
326
324
 
@@ -399,6 +397,9 @@ export class AgentSession {
399
397
  readonly sessionManager: SessionManager;
400
398
  readonly settings: Settings;
401
399
  readonly searchDb: SearchDb | undefined;
400
+
401
+ #powerAssertion: MacOSPowerAssertion | undefined;
402
+
402
403
  readonly configWarnings: string[] = [];
403
404
 
404
405
  #asyncJobManager: AsyncJobManager | undefined = undefined;
@@ -409,7 +410,6 @@ export class AgentSession {
409
410
 
410
411
  // Event subscription state
411
412
  #unsubscribeAgent?: () => void;
412
- #unsubscribePendingActionPush?: () => void;
413
413
  #eventListeners: AgentSessionEventListener[] = [];
414
414
 
415
415
  /** Tracks pending steering messages for UI display. Removed when delivered. */
@@ -444,7 +444,7 @@ export class AgentSession {
444
444
  #todoReminderCount = 0;
445
445
  #todoPhases: TodoPhase[] = [];
446
446
  #todoClearTimers = new Map<string, Timer>();
447
- #nextToolChoiceOverride: ToolChoice | undefined = undefined;
447
+ #toolChoiceQueue = new ToolChoiceQueue();
448
448
 
449
449
  // Bash execution state
450
450
  #bashAbortController: AbortController | undefined = undefined;
@@ -482,6 +482,7 @@ export class AgentSession {
482
482
  #discoverableMCPTools = new Map<string, DiscoverableMCPTool>();
483
483
  #discoverableMCPSearchIndex: DiscoverableMCPSearchIndex | null = null;
484
484
  #selectedMCPToolNames = new Set<string>();
485
+ #rpcHostToolNames = new Set<string>();
485
486
  #defaultSelectedMCPServerNames = new Set<string>();
486
487
  #defaultSelectedMCPToolNames = new Set<string>();
487
488
  #sessionDefaultSelectedMCPToolNames = new Map<string, string[]>();
@@ -501,20 +502,49 @@ export class AgentSession {
501
502
 
502
503
  #streamingEditAbortTriggered = false;
503
504
  #streamingEditCheckedLineCounts = new Map<string, number>();
505
+
506
+ #streamingEditPrecheckedToolCallIds = new Set<string>();
507
+
504
508
  #streamingEditFileCache = new Map<string, string>();
505
509
  #promptInFlightCount = 0;
506
510
  #obfuscator: SecretObfuscator | undefined;
507
- #pendingActionStore: PendingActionStore | undefined;
508
511
  #checkpointState: CheckpointState | undefined = undefined;
509
512
  #pendingRewindReport: string | undefined = undefined;
510
513
  #promptGeneration = 0;
511
514
  #providerSessionState = new Map<string, ProviderSessionState>();
512
515
 
516
+ #startPowerAssertion(): void {
517
+ if (process.platform !== "darwin") {
518
+ return;
519
+ }
520
+ try {
521
+ this.#powerAssertion = MacOSPowerAssertion.start({ reason: "Oh My Pi agent session" });
522
+ } catch (error) {
523
+ logger.warn("Failed to acquire macOS power assertion", { error: String(error) });
524
+ }
525
+ }
526
+
527
+ #stopPowerAssertion(): void {
528
+ const assertion = this.#powerAssertion;
529
+ this.#powerAssertion = undefined;
530
+ if (!assertion) {
531
+ return;
532
+ }
533
+ try {
534
+ assertion.stop();
535
+ } catch (error) {
536
+ logger.warn("Failed to release macOS power assertion", { error: String(error) });
537
+ }
538
+ }
539
+
513
540
  constructor(config: AgentSessionConfig) {
541
+ setNativeKillTree(killTree);
542
+
514
543
  this.agent = config.agent;
515
544
  this.sessionManager = config.sessionManager;
516
545
  this.settings = config.settings;
517
546
  this.searchDb = config.searchDb;
547
+ this.#startPowerAssertion();
518
548
  this.#asyncJobManager = config.asyncJobManager;
519
549
  this.#scopedModels = config.scopedModels ?? [];
520
550
  this.#thinkingLevel = config.thinkingLevel;
@@ -539,7 +569,7 @@ export class AgentSession {
539
569
  this.#defaultSelectedMCPServerNames = new Set(config.defaultSelectedMCPServerNames ?? []);
540
570
  this.#defaultSelectedMCPToolNames = new Set(config.defaultSelectedMCPToolNames ?? []);
541
571
  this.#pruneSelectedMCPToolNames();
542
- const persistedSelectedMCPToolNames = this.sessionManager.buildSessionContext().selectedMCPToolNames;
572
+ const persistedSelectedMCPToolNames = this.buildDisplaySessionContext().selectedMCPToolNames;
543
573
  const currentSelectedMCPToolNames = this.getSelectedMCPToolNames();
544
574
  const persistInitialMCPToolSelection =
545
575
  config.persistInitialMCPToolSelection ?? this.sessionManager.getBranch().length === 0;
@@ -556,24 +586,16 @@ export class AgentSession {
556
586
  );
557
587
  this.#ttsrManager = config.ttsrManager;
558
588
  this.#obfuscator = config.obfuscator;
559
- this.agent.providerSessionState = this.#providerSessionState;
560
- this.#pendingActionStore = config.pendingActionStore;
561
- this.#unsubscribePendingActionPush = this.#pendingActionStore?.subscribePush(action => {
562
- const reminderText = [
563
- "<system-reminder>",
564
- "This is a preview. Call the `resolve` tool to apply or discard these changes.",
565
- "</system-reminder>",
566
- ].join("\n");
567
- this.agent.steer({
568
- role: "custom",
569
- customType: "resolve-reminder",
570
- content: reminderText,
571
- display: false,
572
- details: { toolName: action.sourceToolName },
573
- attribution: "agent",
574
- timestamp: Date.now(),
575
- });
589
+ this.agent.setAssistantMessageEventInterceptor((message, assistantMessageEvent) => {
590
+ const event: AgentEvent = {
591
+ type: "message_update",
592
+ message,
593
+ assistantMessageEvent,
594
+ };
595
+ this.#preCacheStreamingEditFile(event);
596
+ this.#maybeAbortStreamingEdit(event);
576
597
  });
598
+ this.agent.providerSessionState = this.#providerSessionState;
577
599
  this.#syncTodoPhasesFromBranch();
578
600
 
579
601
  // Always subscribe to agent events for internal handling
@@ -586,10 +608,40 @@ export class AgentSession {
586
608
  return this.#modelRegistry;
587
609
  }
588
610
 
589
- consumeNextToolChoiceOverride(): ToolChoice | undefined {
590
- const toolChoice = this.#nextToolChoiceOverride;
591
- this.#nextToolChoiceOverride = undefined;
592
- return toolChoice;
611
+ /** Advance the tool-choice queue and return the next directive for the upcoming LLM call. */
612
+ nextToolChoice(): ToolChoice | undefined {
613
+ return this.#toolChoiceQueue.nextToolChoice();
614
+ }
615
+
616
+ /**
617
+ * Force the next model call to target a specific active tool, then terminate
618
+ * the agent loop. Pushes a two-step sequence [forced, "none"] so the model
619
+ * calls exactly the forced tool once and then cannot call another.
620
+ */
621
+ setForcedToolChoice(toolName: string): void {
622
+ if (!this.getActiveToolNames().includes(toolName)) {
623
+ throw new Error(`Tool "${toolName}" is not currently active.`);
624
+ }
625
+
626
+ const forced = buildNamedToolChoice(toolName, this.model);
627
+ if (!forced || typeof forced === "string") {
628
+ throw new Error("Current model does not support forcing a specific tool.");
629
+ }
630
+
631
+ this.#toolChoiceQueue.pushSequence([forced, "none"], {
632
+ label: "user-force",
633
+ onRejected: () => "requeue",
634
+ });
635
+ }
636
+
637
+ /** The tool-choice queue: forces forthcoming tool invocations and carries handlers. */
638
+ get toolChoiceQueue(): ToolChoiceQueue {
639
+ return this.#toolChoiceQueue;
640
+ }
641
+
642
+ /** Peek the in-flight directive's invocation handler for use by the resolve tool. */
643
+ peekQueueInvoker(): ((input: unknown) => Promise<unknown> | unknown) | undefined {
644
+ return this.#toolChoiceQueue.peekInFlightInvoker();
593
645
  }
594
646
 
595
647
  /** Provider-scoped mutable state store for transport/session caches. */
@@ -668,7 +720,22 @@ export class AgentSession {
668
720
  }
669
721
  }
670
722
 
671
- await this.#emitSessionEvent(event);
723
+ // Deobfuscate assistant message content for display emission — the LLM echoes back
724
+ // obfuscated placeholders, but listeners (TUI, extensions, exporters) must see real
725
+ // values. The original event.message stays obfuscated so the persistence path below
726
+ // writes `#HASH#` tokens to the session file; convertToLlm re-obfuscates outbound
727
+ // traffic on the next turn. Walks text, thinking, and toolCall arguments/intent.
728
+ let displayEvent: AgentEvent = event;
729
+ const obfuscator = this.#obfuscator;
730
+ if (obfuscator && event.type === "message_end" && event.message.role === "assistant") {
731
+ const message = event.message;
732
+ const deobfuscatedContent = obfuscator.deobfuscateObject(message.content);
733
+ if (deobfuscatedContent !== message.content) {
734
+ displayEvent = { ...event, message: { ...message, content: deobfuscatedContent } };
735
+ }
736
+ }
737
+
738
+ await this.#emitSessionEvent(displayEvent);
672
739
 
673
740
  if (event.type === "turn_start") {
674
741
  this.#resetStreamingEditState();
@@ -680,6 +747,17 @@ export class AgentSession {
680
747
  if (event.type === "turn_end" && this.#ttsrManager) {
681
748
  this.#ttsrManager.incrementMessageCount();
682
749
  }
750
+ // Finalize the tool-choice queue's in-flight yield after tools have executed.
751
+ // This must happen at turn_end (not message_end) because onInvoked handlers
752
+ // run during tool execution, which happens between message_end and turn_end.
753
+ if (event.type === "turn_end" && this.#toolChoiceQueue.hasInFlight) {
754
+ const msg = event.message as AssistantMessage;
755
+ if (msg.stopReason === "aborted" || msg.stopReason === "error") {
756
+ this.#toolChoiceQueue.reject(msg.stopReason === "error" ? "error" : "aborted");
757
+ } else {
758
+ this.#toolChoiceQueue.resolve();
759
+ }
760
+ }
683
761
  if (event.type === "turn_end" && this.#pendingRewindReport) {
684
762
  const report = this.#pendingRewindReport;
685
763
  this.#pendingRewindReport = undefined;
@@ -778,8 +856,13 @@ export class AgentSession {
778
856
  }
779
857
  }
780
858
 
781
- if (event.type === "message_update" && event.assistantMessageEvent.type === "toolcall_start") {
782
- this.#preCacheStreamingEditFile(event);
859
+ if (
860
+ event.type === "message_update" &&
861
+ (event.assistantMessageEvent.type === "toolcall_start" ||
862
+ event.assistantMessageEvent.type === "toolcall_delta" ||
863
+ event.assistantMessageEvent.type === "toolcall_end")
864
+ ) {
865
+ void this.#preCacheStreamingEditFile(event);
783
866
  }
784
867
 
785
868
  if (
@@ -1097,7 +1180,7 @@ export class AgentSession {
1097
1180
  if (this.#pendingTtsrInjections.length === 0) return undefined;
1098
1181
  const rules = this.#pendingTtsrInjections;
1099
1182
  const content = rules
1100
- .map(r => renderPromptTemplate(ttsrInterruptTemplate, { name: r.name, path: r.path, content: r.content }))
1183
+ .map(r => prompt.render(ttsrInterruptTemplate, { name: r.name, path: r.path, content: r.content }))
1101
1184
  .join("\n\n");
1102
1185
  this.#pendingTtsrInjections = [];
1103
1186
  return { content, rules };
@@ -1310,31 +1393,101 @@ export class AgentSession {
1310
1393
  #resetStreamingEditState(): void {
1311
1394
  this.#streamingEditAbortTriggered = false;
1312
1395
  this.#streamingEditCheckedLineCounts.clear();
1396
+ this.#streamingEditPrecheckedToolCallIds.clear();
1313
1397
  this.#streamingEditFileCache.clear();
1314
1398
  }
1315
1399
 
1316
- async #preCacheStreamingEditFile(event: AgentEvent): Promise<void> {
1317
- if (!this.settings.get("edit.streamingAbort")) return;
1318
- if (event.type !== "message_update") return;
1319
- const assistantEvent = event.assistantMessageEvent;
1320
- if (assistantEvent.type !== "toolcall_start") return;
1321
- if (event.message.role !== "assistant") return;
1322
-
1323
- const contentIndex = assistantEvent.contentIndex;
1400
+ #getStreamingEditToolCall(event: AgentEvent):
1401
+ | {
1402
+ toolCall: ToolCall;
1403
+ path: string;
1404
+ resolvedPath: string;
1405
+ diff?: string;
1406
+ op?: string;
1407
+ rename?: string;
1408
+ }
1409
+ | undefined {
1410
+ if (event.type !== "message_update") return undefined;
1411
+ if (event.message.role !== "assistant") return undefined;
1412
+
1413
+ const contentIndex = event.assistantMessageEvent.contentIndex ?? 0;
1324
1414
  const messageContent = event.message.content;
1325
- if (!Array.isArray(messageContent) || contentIndex >= messageContent.length) return;
1415
+ if (!Array.isArray(messageContent) || contentIndex < 0 || contentIndex >= messageContent.length) {
1416
+ return undefined;
1417
+ }
1418
+
1326
1419
  const toolCall = messageContent[contentIndex] as ToolCall;
1327
- if (toolCall.name !== "edit") return;
1420
+ if (toolCall.name !== "edit") return undefined;
1328
1421
 
1329
1422
  const args = toolCall.arguments;
1330
- if (!args || typeof args !== "object" || Array.isArray(args)) return;
1331
- if ("old_text" in args || "new_text" in args) return;
1423
+ if (!args || typeof args !== "object" || Array.isArray(args)) return undefined;
1424
+ if ("old_text" in args || "new_text" in args) return undefined;
1332
1425
 
1333
1426
  const path = typeof args.path === "string" ? args.path : undefined;
1334
- if (!path) return;
1427
+ if (!path) return undefined;
1335
1428
 
1336
- const resolvedPath = resolveToCwd(path, this.sessionManager.getCwd());
1337
- this.#ensureFileCache(resolvedPath);
1429
+ return {
1430
+ toolCall,
1431
+ path,
1432
+ resolvedPath: resolveToCwd(path, this.sessionManager.getCwd()),
1433
+ diff: typeof args.diff === "string" ? args.diff : undefined,
1434
+ op: typeof args.op === "string" ? args.op : undefined,
1435
+ rename: typeof args.rename === "string" ? args.rename : undefined,
1436
+ };
1437
+ }
1438
+
1439
+ #lastStreamingEditToolCallId: string | undefined;
1440
+ #abortStreamingEditForAutoGeneratedPath(toolCall: ToolCall, path: string, resolvedPath: string): void {
1441
+ if (this.#lastStreamingEditToolCallId === toolCall.id) return;
1442
+ this.#lastStreamingEditToolCallId = toolCall.id;
1443
+ void assertEditableFile(resolvedPath, path).catch(err => {
1444
+ // peekFile and other I/O can reject with ENOENT, etc. Only ToolError means
1445
+ // auto-generated detection; other failures are left for the edit tool.
1446
+ if (!(err instanceof ToolError)) return;
1447
+ if (this.#lastStreamingEditToolCallId !== toolCall.id) return;
1448
+
1449
+ if (!this.#streamingEditAbortTriggered) {
1450
+ this.#streamingEditAbortTriggered = true;
1451
+ logger.warn("Streaming edit aborted due to auto-generated file guard", {
1452
+ toolCallId: toolCall.id,
1453
+ path,
1454
+ });
1455
+ this.agent.abort();
1456
+ }
1457
+ });
1458
+ }
1459
+
1460
+ #preCacheStreamingEditFile(event: AgentEvent): void {
1461
+ if (!this.settings.get("edit.streamingAbort")) return;
1462
+ if (this.#streamingEditAbortTriggered) return;
1463
+ if (event.type !== "message_update") return;
1464
+
1465
+ const assistantEvent = event.assistantMessageEvent;
1466
+ if (
1467
+ assistantEvent.type !== "toolcall_start" &&
1468
+ assistantEvent.type !== "toolcall_delta" &&
1469
+ assistantEvent.type !== "toolcall_end"
1470
+ ) {
1471
+ return;
1472
+ }
1473
+
1474
+ const streamingEdit = this.#getStreamingEditToolCall(event);
1475
+ if (!streamingEdit) return;
1476
+
1477
+ const shouldCheckAutoGenerated =
1478
+ !streamingEdit.toolCall.id || !this.#streamingEditPrecheckedToolCallIds.has(streamingEdit.toolCall.id);
1479
+ if (shouldCheckAutoGenerated) {
1480
+ if (streamingEdit.toolCall.id) {
1481
+ this.#streamingEditPrecheckedToolCallIds.add(streamingEdit.toolCall.id);
1482
+ }
1483
+ this.#abortStreamingEditForAutoGeneratedPath(
1484
+ streamingEdit.toolCall,
1485
+ streamingEdit.path,
1486
+ streamingEdit.resolvedPath,
1487
+ );
1488
+ }
1489
+
1490
+ this.#ensureFileCache(streamingEdit.resolvedPath);
1338
1491
  }
1339
1492
 
1340
1493
  #ensureFileCache(resolvedPath: string): void {
@@ -1359,24 +1512,15 @@ export class AgentSession {
1359
1512
  if (!this.settings.get("edit.streamingAbort")) return;
1360
1513
  if (this.#streamingEditAbortTriggered) return;
1361
1514
  if (event.type !== "message_update") return;
1515
+
1362
1516
  const assistantEvent = event.assistantMessageEvent;
1363
1517
  if (assistantEvent.type !== "toolcall_end" && assistantEvent.type !== "toolcall_delta") return;
1364
- if (event.message.role !== "assistant") return;
1365
-
1366
- const contentIndex = assistantEvent.contentIndex;
1367
- const messageContent = event.message.content;
1368
- if (!Array.isArray(messageContent) || contentIndex >= messageContent.length) return;
1369
- const toolCall = messageContent[contentIndex] as ToolCall;
1370
- if (toolCall.name !== "edit" || !toolCall.id) return;
1371
1518
 
1372
- const args = toolCall.arguments;
1373
- if (!args || typeof args !== "object" || Array.isArray(args)) return;
1374
- if ("old_text" in args || "new_text" in args) return;
1519
+ const streamingEdit = this.#getStreamingEditToolCall(event);
1520
+ if (!streamingEdit?.toolCall.id) return;
1375
1521
 
1376
- const path = typeof args.path === "string" ? args.path : undefined;
1377
- const diff = typeof args.diff === "string" ? args.diff : undefined;
1378
- const op = typeof args.op === "string" ? args.op : undefined;
1379
- if (!path || !diff) return;
1522
+ const { toolCall, path, resolvedPath, diff, op, rename } = streamingEdit;
1523
+ if (!diff) return;
1380
1524
  if (op && op !== "update") return;
1381
1525
 
1382
1526
  if (!diff.includes("\n")) return;
@@ -1399,13 +1543,10 @@ export class AgentSession {
1399
1543
  if (lastChecked !== undefined && lineCount <= lastChecked) return;
1400
1544
  this.#streamingEditCheckedLineCounts.set(toolCall.id, lineCount);
1401
1545
 
1402
- const rename = typeof args.rename === "string" ? args.rename : undefined;
1403
-
1404
1546
  const removedLines = lines
1405
1547
  .filter(line => line.startsWith("-") && !line.startsWith("--- "))
1406
1548
  .map(line => line.slice(1));
1407
1549
  if (removedLines.length > 0) {
1408
- const resolvedPath = resolveToCwd(path, this.sessionManager.getCwd());
1409
1550
  let cachedContent = this.#streamingEditFileCache.get(resolvedPath);
1410
1551
  if (cachedContent === undefined) {
1411
1552
  this.#ensureFileCache(resolvedPath);
@@ -1495,7 +1636,6 @@ export class AgentSession {
1495
1636
  if (!this.#extensionRunner) return;
1496
1637
  if (event.type === "agent_start") {
1497
1638
  this.#turnIndex = 0;
1498
- this.#nextToolChoiceOverride = undefined;
1499
1639
  await this.#extensionRunner.emit({ type: "agent_start" });
1500
1640
  } else if (event.type === "agent_end") {
1501
1641
  await this.#extensionRunner.emit({ type: "agent_end", messages: event.messages });
@@ -1661,10 +1801,9 @@ export class AgentSession {
1661
1801
  if (drained === false && deliveryState) {
1662
1802
  logger.warn("Async job completion deliveries still pending during dispose", { ...deliveryState });
1663
1803
  }
1804
+ this.#stopPowerAssertion();
1664
1805
  await this.sessionManager.close();
1665
1806
  this.#closeAllProviderSessions("dispose");
1666
- this.#unsubscribePendingActionPush?.();
1667
- this.#unsubscribePendingActionPush = undefined;
1668
1807
  this.#disconnectFromAgent();
1669
1808
  this.#eventListeners = [];
1670
1809
  }
@@ -1877,6 +2016,14 @@ export class AgentSession {
1877
2016
  validToolNames.push(name);
1878
2017
  }
1879
2018
  }
2019
+ // Auto-QA tool must survive any runtime tool-set mutation.
2020
+ if (isAutoQaEnabled(this.settings) && !validToolNames.includes("report_tool_issue")) {
2021
+ const qaTool = this.#toolRegistry.get("report_tool_issue");
2022
+ if (qaTool) {
2023
+ tools.push(qaTool);
2024
+ validToolNames.push("report_tool_issue");
2025
+ }
2026
+ }
1880
2027
  if (this.#mcpDiscoveryEnabled) {
1881
2028
  this.#selectedMCPToolNames = new Set(
1882
2029
  validToolNames.filter(
@@ -1968,7 +2115,7 @@ export class AgentSession {
1968
2115
 
1969
2116
  this.#setDiscoverableMCPTools(this.#collectDiscoverableMCPToolsFromRegistry());
1970
2117
  this.#pruneSelectedMCPToolNames();
1971
- if (!this.sessionManager.buildSessionContext().hasPersistedMCPToolSelection) {
2118
+ if (!this.buildDisplaySessionContext().hasPersistedMCPToolSelection) {
1972
2119
  this.#selectedMCPToolNames = new Set([
1973
2120
  ...this.#selectedMCPToolNames,
1974
2121
  ...this.#getConfiguredDefaultSelectedMCPToolNames(),
@@ -1983,6 +2130,49 @@ export class AgentSession {
1983
2130
  await this.#applyActiveToolsByName(nextActive, { previousSelectedMCPToolNames });
1984
2131
  }
1985
2132
 
2133
+ /**
2134
+ * Replace RPC host-owned tools and refresh the active tool set before the next model call.
2135
+ */
2136
+ async refreshRpcHostTools(rpcTools: AgentTool[]): Promise<void> {
2137
+ const nextToolNames = rpcTools.map(tool => tool.name);
2138
+ const uniqueToolNames = new Set(nextToolNames);
2139
+ if (uniqueToolNames.size !== nextToolNames.length) {
2140
+ throw new Error("RPC host tool names must be unique");
2141
+ }
2142
+
2143
+ for (const name of uniqueToolNames) {
2144
+ if (this.#toolRegistry.has(name) && !this.#rpcHostToolNames.has(name)) {
2145
+ throw new Error(`RPC host tool "${name}" conflicts with an existing tool`);
2146
+ }
2147
+ }
2148
+
2149
+ const previousRpcHostToolNames = new Set(this.#rpcHostToolNames);
2150
+ const previousActiveToolNames = this.getActiveToolNames();
2151
+ for (const name of previousRpcHostToolNames) {
2152
+ this.#toolRegistry.delete(name);
2153
+ }
2154
+ this.#rpcHostToolNames.clear();
2155
+
2156
+ for (const tool of rpcTools) {
2157
+ const finalTool = (
2158
+ this.#extensionRunner ? new ExtensionToolWrapper(tool, this.#extensionRunner) : tool
2159
+ ) as AgentTool;
2160
+ this.#toolRegistry.set(finalTool.name, finalTool);
2161
+ this.#rpcHostToolNames.add(finalTool.name);
2162
+ }
2163
+
2164
+ const activeNonRpcToolNames = previousActiveToolNames.filter(name => !previousRpcHostToolNames.has(name));
2165
+ const preservedRpcToolNames = previousActiveToolNames.filter(
2166
+ name => previousRpcHostToolNames.has(name) && this.#rpcHostToolNames.has(name),
2167
+ );
2168
+ const autoActivatedRpcToolNames = rpcTools
2169
+ .filter(tool => !tool.hidden && !previousRpcHostToolNames.has(tool.name))
2170
+ .map(tool => tool.name);
2171
+ await this.#applyActiveToolsByName(
2172
+ Array.from(new Set([...activeNonRpcToolNames, ...preservedRpcToolNames, ...autoActivatedRpcToolNames])),
2173
+ );
2174
+ }
2175
+
1986
2176
  /** Whether auto-compaction is currently running */
1987
2177
  get isCompacting(): boolean {
1988
2178
  return this.#autoCompactionAbortController !== undefined || this.#compactionAbortController !== undefined;
@@ -1993,6 +2183,10 @@ export class AgentSession {
1993
2183
  return this.agent.state.messages;
1994
2184
  }
1995
2185
 
2186
+ buildDisplaySessionContext(): SessionContext {
2187
+ return deobfuscateSessionContext(this.sessionManager.buildSessionContext(), this.#obfuscator);
2188
+ }
2189
+
1996
2190
  /** Convert session messages using the same pre-LLM pipeline as the active session. */
1997
2191
  async convertMessagesToLlm(messages: AgentMessage[], signal?: AbortSignal): Promise<Message[]> {
1998
2192
  const transformedMessages = await this.#transformContext(messages, signal);
@@ -2103,7 +2297,16 @@ export class AgentSession {
2103
2297
  }
2104
2298
 
2105
2299
  resolveRoleModel(role: string): Model | undefined {
2106
- return this.#resolveRoleModel(role, this.#modelRegistry.getAvailable(), this.model);
2300
+ return this.#resolveRoleModelFull(role, this.#modelRegistry.getAvailable(), this.model).model;
2301
+ }
2302
+
2303
+ /**
2304
+ * Resolve a role to its model AND thinking level.
2305
+ * Unlike resolveRoleModel(), this preserves the thinking level suffix
2306
+ * from role configuration (e.g., "anthropic/claude-sonnet-4-5:xhigh").
2307
+ */
2308
+ resolveRoleModelWithThinking(role: string): ResolvedModelRoleValue {
2309
+ return this.#resolveRoleModelFull(role, this.#modelRegistry.getAvailable(), this.model);
2107
2310
  }
2108
2311
 
2109
2312
  get promptTemplates(): ReadonlyArray<PromptTemplate> {
@@ -2154,7 +2357,7 @@ export class AgentSession {
2154
2357
  throw error;
2155
2358
  }
2156
2359
 
2157
- const content = renderPromptTemplate(planModeReferencePrompt, {
2360
+ const content = prompt.render(planModeReferencePrompt, {
2158
2361
  planFilePath,
2159
2362
  planContent,
2160
2363
  });
@@ -2191,7 +2394,7 @@ export class AgentSession {
2191
2394
  : sessionPlanUrl;
2192
2395
 
2193
2396
  const planExists = fs.existsSync(resolvedPlanPath);
2194
- const content = renderPromptTemplate(planModeActivePrompt, {
2397
+ const content = prompt.render(planModeActivePrompt, {
2195
2398
  planFilePath: displayPlanPath,
2196
2399
  planExists,
2197
2400
  askToolName: "ask",
@@ -2263,7 +2466,10 @@ export class AgentSession {
2263
2466
  return;
2264
2467
  }
2265
2468
 
2266
- const eagerTodoPrelude = !options?.synthetic ? this.#createEagerTodoPrelude() : undefined;
2469
+ // Skip eager todo prelude when the user has already queued a directive
2470
+ const hasPendingUserDirective = this.#toolChoiceQueue.inspect().includes("user-force");
2471
+ const eagerTodoPrelude =
2472
+ !options?.synthetic && !hasPendingUserDirective ? this.#createEagerTodoPrelude(expandedText) : undefined;
2267
2473
 
2268
2474
  const userContent: (TextContent | ImageContent)[] = [{ type: "text", text: expandedText }];
2269
2475
  if (options?.images) {
@@ -2276,7 +2482,9 @@ export class AgentSession {
2276
2482
  : { role: "user" as const, content: userContent, attribution: promptAttribution, timestamp: Date.now() };
2277
2483
 
2278
2484
  if (eagerTodoPrelude) {
2279
- this.#nextToolChoiceOverride = eagerTodoPrelude.toolChoice;
2485
+ this.#toolChoiceQueue.pushOnce(eagerTodoPrelude.toolChoice, {
2486
+ label: "eager-todo",
2487
+ });
2280
2488
  }
2281
2489
 
2282
2490
  try {
@@ -2285,9 +2493,9 @@ export class AgentSession {
2285
2493
  prependMessages: eagerTodoPrelude ? [eagerTodoPrelude.message] : undefined,
2286
2494
  });
2287
2495
  } finally {
2288
- if (eagerTodoPrelude) {
2289
- this.#nextToolChoiceOverride = undefined;
2290
- }
2496
+ // Clean up residual eager-todo directive if the prompt never consumed it
2497
+ // (e.g., compaction aborted, validation failed).
2498
+ this.#toolChoiceQueue.removeByLabel("eager-todo");
2291
2499
  }
2292
2500
  if (!options?.synthetic) {
2293
2501
  await this.#enforcePlanModeToolDecision();
@@ -2651,6 +2859,10 @@ export class AgentSession {
2651
2859
  });
2652
2860
  }
2653
2861
 
2862
+ queueDeferredMessage(message: CustomMessage): void {
2863
+ this.#queueHiddenNextTurnMessage(message, true);
2864
+ }
2865
+
2654
2866
  #queueHiddenNextTurnMessage(message: CustomMessage, triggerTurn: boolean): void {
2655
2867
  this.#pendingNextTurnMessages.push(message);
2656
2868
  if (!triggerTurn) return;
@@ -3007,6 +3219,13 @@ export class AgentSession {
3007
3219
  // block runs, but nested prompt setup/finalizers may still be unwinding. Without this,
3008
3220
  // a subsequent prompt() can incorrectly observe the session as busy after an abort.
3009
3221
  this.#promptInFlightCount = 0;
3222
+ // Safety net: if the agent loop aborted without producing an assistant
3223
+ // message (e.g. failed before the first stream), the in-flight yield was
3224
+ // never resolved or rejected by the normal message_end path. Reject it now
3225
+ // so any requeue callback still fires and the queue stays consistent.
3226
+ if (this.#toolChoiceQueue.hasInFlight) {
3227
+ this.#toolChoiceQueue.reject("aborted");
3228
+ }
3010
3229
  }
3011
3230
 
3012
3231
  /**
@@ -3182,7 +3401,7 @@ export class AgentSession {
3182
3401
  * Validates API key, saves to session log but NOT to settings.
3183
3402
  * @throws Error if no API key available for the model
3184
3403
  */
3185
- async setModelTemporary(model: Model): Promise<void> {
3404
+ async setModelTemporary(model: Model, thinkingLevel?: ThinkingLevel): Promise<void> {
3186
3405
  const apiKey = await this.#modelRegistry.getApiKey(model, this.sessionId);
3187
3406
  if (!apiKey) {
3188
3407
  throw new Error(`No API key for ${model.provider}/${model.id}`);
@@ -3193,8 +3412,8 @@ export class AgentSession {
3193
3412
  this.sessionManager.appendModelChange(`${model.provider}/${model.id}`, "temporary");
3194
3413
  this.settings.getStorage()?.recordModelUsage(`${model.provider}/${model.id}`);
3195
3414
 
3196
- // Re-apply the current thinking level for the newly selected model
3197
- this.setThinkingLevel(this.thinkingLevel);
3415
+ // Apply explicit thinking level, or re-clamp current level to new model's capabilities
3416
+ this.setThinkingLevel(thinkingLevel ?? this.thinkingLevel);
3198
3417
  }
3199
3418
 
3200
3419
  /**
@@ -3267,13 +3486,12 @@ export class AgentSession {
3267
3486
  const next = roleModels[nextIndex];
3268
3487
 
3269
3488
  if (options?.temporary) {
3270
- await this.setModelTemporary(next.model);
3489
+ await this.setModelTemporary(next.model, next.explicitThinkingLevel ? next.thinkingLevel : undefined);
3271
3490
  } else {
3272
3491
  await this.setModel(next.model, next.role);
3273
- }
3274
-
3275
- if (next.explicitThinkingLevel && next.thinkingLevel !== undefined) {
3276
- this.setThinkingLevel(next.thinkingLevel);
3492
+ if (next.explicitThinkingLevel && next.thinkingLevel !== undefined) {
3493
+ this.setThinkingLevel(next.thinkingLevel);
3494
+ }
3277
3495
  }
3278
3496
 
3279
3497
  return { model: next.model, thinkingLevel: this.thinkingLevel, role: next.role };
@@ -3473,7 +3691,7 @@ export class AgentSession {
3473
3691
  }
3474
3692
 
3475
3693
  await this.sessionManager.rewriteEntries();
3476
- const sessionContext = this.sessionManager.buildSessionContext();
3694
+ const sessionContext = this.buildDisplaySessionContext();
3477
3695
  this.agent.replaceMessages(sessionContext.messages);
3478
3696
  this.#syncTodoPhasesFromBranch();
3479
3697
  this.#closeCodexProviderSessionsForHistoryRewrite();
@@ -3599,7 +3817,7 @@ export class AgentSession {
3599
3817
  preserveData,
3600
3818
  );
3601
3819
  const newEntries = this.sessionManager.getEntries();
3602
- const sessionContext = this.sessionManager.buildSessionContext();
3820
+ const sessionContext = this.buildDisplaySessionContext();
3603
3821
  this.agent.replaceMessages(sessionContext.messages);
3604
3822
  this.#syncTodoPhasesFromBranch();
3605
3823
  this.#closeCodexProviderSessionsForHistoryRewrite();
@@ -3646,6 +3864,12 @@ export class AgentSession {
3646
3864
  this.#handoffAbortController?.abort();
3647
3865
  }
3648
3866
 
3867
+ /** Trigger idle compaction through the auto-compaction flow (with UI events). */
3868
+ async runIdleCompaction(): Promise<void> {
3869
+ if (this.isStreaming || this.isCompacting) return;
3870
+ await this.#runAutoCompaction("idle", false, true);
3871
+ }
3872
+
3649
3873
  /**
3650
3874
  * Cancel in-progress branch summarization.
3651
3875
  */
@@ -3708,7 +3932,7 @@ export class AgentSession {
3708
3932
  }
3709
3933
 
3710
3934
  // Build the handoff prompt
3711
- const handoffPrompt = renderPromptTemplate(handoffDocumentPrompt, {
3935
+ const handoffPrompt = prompt.render(handoffDocumentPrompt, {
3712
3936
  additionalFocus: customInstructions,
3713
3937
  });
3714
3938
 
@@ -3814,7 +4038,7 @@ export class AgentSession {
3814
4038
  }
3815
4039
 
3816
4040
  // Rebuild agent messages from session
3817
- const sessionContext = this.sessionManager.buildSessionContext();
4041
+ const sessionContext = this.buildDisplaySessionContext();
3818
4042
  this.agent.replaceMessages(sessionContext.messages);
3819
4043
  this.#syncTodoPhasesFromBranch();
3820
4044
 
@@ -3976,7 +4200,7 @@ export class AgentSession {
3976
4200
  return;
3977
4201
  }
3978
4202
 
3979
- const reminder = renderPromptTemplate(planModeToolDecisionReminderPrompt, {
4203
+ const reminder = prompt.render(planModeToolDecisionReminderPrompt, {
3980
4204
  askToolName: "ask",
3981
4205
  exitToolName: "exit_plan_mode",
3982
4206
  });
@@ -3988,7 +4212,7 @@ export class AgentSession {
3988
4212
  });
3989
4213
  }
3990
4214
 
3991
- #createEagerTodoPrelude(): { message: AgentMessage; toolChoice: ToolChoice } | undefined {
4215
+ #createEagerTodoPrelude(promptText: string): { message: AgentMessage; toolChoice: ToolChoice } | undefined {
3992
4216
  const eagerTodosEnabled = this.settings.get("todo.eager");
3993
4217
  const todosEnabled = this.settings.get("todo.enabled");
3994
4218
  if (!eagerTodosEnabled || !todosEnabled) {
@@ -4002,6 +4226,11 @@ export class AgentSession {
4002
4226
  return undefined;
4003
4227
  }
4004
4228
 
4229
+ const trimmedPromptText = promptText.trimEnd();
4230
+ if (trimmedPromptText.endsWith("?") || trimmedPromptText.endsWith("!")) {
4231
+ return undefined;
4232
+ }
4233
+
4005
4234
  if (!this.#toolRegistry.has("todo_write")) {
4006
4235
  logger.warn("Eager todo enforcement skipped because todo_write is unavailable", {
4007
4236
  activeToolNames: this.agent.state.tools.map(tool => tool.name),
@@ -4018,7 +4247,7 @@ export class AgentSession {
4018
4247
  return undefined;
4019
4248
  }
4020
4249
 
4021
- const eagerTodoReminder = renderPromptTemplate(eagerTodoPrompt);
4250
+ const eagerTodoReminder = prompt.render(eagerTodoPrompt);
4022
4251
 
4023
4252
  return {
4024
4253
  message: {
@@ -4036,6 +4265,13 @@ export class AgentSession {
4036
4265
  * Check if agent stopped with incomplete todos and prompt to continue.
4037
4266
  */
4038
4267
  async #checkTodoCompletion(): Promise<void> {
4268
+ // Skip todo reminders when the most recent turn was driven by an explicit user force —
4269
+ // the user wanted exactly that tool, not a follow-up nag about incomplete todos.
4270
+ const lastServedLabel = this.#toolChoiceQueue.consumeLastServedLabel();
4271
+ if (lastServedLabel === "user-force") {
4272
+ return;
4273
+ }
4274
+
4039
4275
  const remindersEnabled = this.settings.get("todo.reminders");
4040
4276
  const todosEnabled = this.settings.get("todo.enabled");
4041
4277
  if (!remindersEnabled || !todosEnabled) {
@@ -4364,19 +4600,25 @@ export class AgentSession {
4364
4600
  return availableModels.find(m => m.provider === currentModel.provider && m.id === configuredTarget);
4365
4601
  }
4366
4602
 
4367
- #resolveRoleModel(role: string, availableModels: Model[], currentModel: Model | undefined): Model | undefined {
4603
+ #resolveRoleModelFull(
4604
+ role: string,
4605
+ availableModels: Model[],
4606
+ currentModel: Model | undefined,
4607
+ ): ResolvedModelRoleValue {
4368
4608
  const roleModelStr =
4369
4609
  role === "default"
4370
4610
  ? (this.settings.getModelRole("default") ??
4371
4611
  (currentModel ? `${currentModel.provider}/${currentModel.id}` : undefined))
4372
4612
  : this.settings.getModelRole(role);
4373
4613
 
4374
- if (!roleModelStr) return undefined;
4614
+ if (!roleModelStr) {
4615
+ return { model: undefined, thinkingLevel: undefined, explicitThinkingLevel: false, warning: undefined };
4616
+ }
4375
4617
 
4376
4618
  return resolveModelRoleValue(roleModelStr, availableModels, {
4377
4619
  settings: this.settings,
4378
4620
  matchPreferences: { usageOrder: this.settings.getStorage()?.getModelUsageOrder() },
4379
- }).model;
4621
+ });
4380
4622
  }
4381
4623
 
4382
4624
  #getCompactionModelCandidates(availableModels: Model[]): Model[] {
@@ -4393,7 +4635,7 @@ export class AgentSession {
4393
4635
 
4394
4636
  const currentModel = this.model;
4395
4637
  for (const role of MODEL_ROLE_IDS) {
4396
- addCandidate(this.#resolveRoleModel(role, availableModels, currentModel));
4638
+ addCandidate(this.#resolveRoleModelFull(role, availableModels, currentModel).model);
4397
4639
  }
4398
4640
 
4399
4641
  const sortedByContext = [...availableModels].sort((a, b) => b.contextWindow - a.contextWindow);
@@ -4410,11 +4652,16 @@ export class AgentSession {
4410
4652
  /**
4411
4653
  * Internal: Run auto-compaction with events.
4412
4654
  */
4413
- async #runAutoCompaction(reason: "overflow" | "threshold", willRetry: boolean, deferred = false): Promise<void> {
4655
+ async #runAutoCompaction(
4656
+ reason: "overflow" | "threshold" | "idle",
4657
+ willRetry: boolean,
4658
+ deferred = false,
4659
+ ): Promise<void> {
4414
4660
  const compactionSettings = this.settings.getGroup("compaction");
4415
- if (!compactionSettings.enabled || compactionSettings.strategy === "off") return;
4661
+ if (compactionSettings.strategy === "off") return;
4662
+ if (reason !== "idle" && !compactionSettings.enabled) return;
4416
4663
  const generation = this.#promptGeneration;
4417
- if (!deferred && reason !== "overflow" && compactionSettings.strategy === "handoff") {
4664
+ if (!deferred && reason !== "overflow" && reason !== "idle" && compactionSettings.strategy === "handoff") {
4418
4665
  this.#schedulePostPromptTask(
4419
4666
  async signal => {
4420
4667
  await Promise.resolve();
@@ -4689,7 +4936,7 @@ export class AgentSession {
4689
4936
  preserveData,
4690
4937
  );
4691
4938
  const newEntries = this.sessionManager.getEntries();
4692
- const sessionContext = this.sessionManager.buildSessionContext();
4939
+ const sessionContext = this.buildDisplaySessionContext();
4693
4940
  this.agent.replaceMessages(sessionContext.messages);
4694
4941
  this.#syncTodoPhasesFromBranch();
4695
4942
  this.#closeCodexProviderSessionsForHistoryRewrite();
@@ -4717,7 +4964,7 @@ export class AgentSession {
4717
4964
  };
4718
4965
  await this.#emitSessionEvent({ type: "auto_compaction_end", action, result, aborted: false, willRetry });
4719
4966
 
4720
- if (!willRetry && compactionSettings.autoContinue !== false) {
4967
+ if (!willRetry && reason !== "idle" && compactionSettings.autoContinue !== false) {
4721
4968
  const continuePrompt = async () => {
4722
4969
  await this.#promptWithMessage(
4723
4970
  {
@@ -4823,6 +5070,17 @@ export class AgentSession {
4823
5070
  }
4824
5071
 
4825
5072
  #isTransientErrorMessage(errorMessage: string): boolean {
5073
+ return (
5074
+ this.#isTransientEnvelopeErrorMessage(errorMessage) || this.#isTransientTransportErrorMessage(errorMessage)
5075
+ );
5076
+ }
5077
+
5078
+ #isTransientEnvelopeErrorMessage(errorMessage: string): boolean {
5079
+ // Match Anthropic stream-envelope failures that indicate a broken stream before any content starts.
5080
+ return /anthropic stream envelope error:/i.test(errorMessage) && /before message_start/i.test(errorMessage);
5081
+ }
5082
+
5083
+ #isTransientTransportErrorMessage(errorMessage: string): boolean {
4826
5084
  // Match: overloaded_error, provider returned error, rate limit, 429, 500, 502, 503, 504,
4827
5085
  // service unavailable, network/connection errors, fetch failed, terminated, retry delay exceeded
4828
5086
  return /overloaded|provider.?returned.?error|rate.?limit|too many requests|429|500|502|503|504|service.?unavailable|server.?error|internal.?error|network.?error|connection.?error|connection.?refused|other side closed|fetch failed|upstream.?connect|reset before headers|socket hang up|timed? out|timeout|terminated|retry delay|stream stall/i.test(
@@ -5516,7 +5774,7 @@ export class AgentSession {
5516
5774
  // Flush pending writes before switching so restore snapshots reflect committed state.
5517
5775
  await this.sessionManager.flush();
5518
5776
  const previousSessionState = this.sessionManager.captureState();
5519
- const previousSessionContext = this.sessionManager.buildSessionContext();
5777
+ const previousSessionContext = this.buildDisplaySessionContext();
5520
5778
  // switchSession replaces these arrays wholesale during load/rollback, so retaining
5521
5779
  // the existing message objects is sufficient and avoids structured-clone failures for
5522
5780
  // extension/custom metadata that is valid to persist but not cloneable.
@@ -5545,7 +5803,7 @@ export class AgentSession {
5545
5803
  await this.sessionManager.setSessionFile(sessionPath);
5546
5804
  this.agent.sessionId = this.sessionManager.getSessionId();
5547
5805
 
5548
- const sessionContext = this.sessionManager.buildSessionContext();
5806
+ const sessionContext = this.buildDisplaySessionContext();
5549
5807
  const didReloadConversationChange =
5550
5808
  !switchingToDifferentSession &&
5551
5809
  this.#didSessionMessagesChange(previousSessionContext.messages, sessionContext.messages);
@@ -5711,7 +5969,7 @@ export class AgentSession {
5711
5969
  this.agent.sessionId = this.sessionManager.getSessionId();
5712
5970
 
5713
5971
  // Reload messages from entries (works for both file and in-memory mode)
5714
- const sessionContext = this.sessionManager.buildSessionContext();
5972
+ const sessionContext = this.buildDisplaySessionContext();
5715
5973
 
5716
5974
  await this.#restoreMCPSelectionsForSessionContext(sessionContext);
5717
5975
 
@@ -5882,7 +6140,7 @@ export class AgentSession {
5882
6140
  }
5883
6141
 
5884
6142
  // Update agent state
5885
- const sessionContext = this.sessionManager.buildSessionContext();
6143
+ const sessionContext = this.buildDisplaySessionContext();
5886
6144
  await this.#restoreMCPSelectionsForSessionContext(sessionContext);
5887
6145
  this.agent.replaceMessages(sessionContext.messages);
5888
6146
  this.#syncTodoPhasesFromBranch();
@@ -6156,176 +6414,13 @@ export class AgentSession {
6156
6414
  * Includes user messages, assistant text, thinking blocks, tool calls, and tool results.
6157
6415
  */
6158
6416
  formatSessionAsText(): string {
6159
- const lines: string[] = [];
6160
-
6161
- /** Serialize an object as XML parameter elements, one per key. */
6162
- function formatArgsAsXml(args: Record<string, unknown>, indent = "\t"): string {
6163
- const parts: string[] = [];
6164
- for (const [key, value] of Object.entries(args)) {
6165
- if (key === INTENT_FIELD) continue;
6166
- const text = typeof value === "string" ? value : JSON.stringify(value);
6167
- parts.push(`${indent}<parameter name="${key}">${text}</parameter>`);
6168
- }
6169
- return parts.join("\n");
6170
- }
6171
-
6172
- // Include system prompt at the beginning
6173
- const systemPrompt = this.agent.state.systemPrompt;
6174
- if (systemPrompt) {
6175
- lines.push("## System Prompt\n");
6176
- lines.push(systemPrompt);
6177
- lines.push("\n");
6178
- }
6179
-
6180
- // Include model and thinking level
6181
- const model = this.agent.state.model;
6182
- const thinkingLevel = this.#thinkingLevel;
6183
- lines.push("## Configuration\n");
6184
- lines.push(`Model: ${model ? `${model.provider}/${model.id}` : "(not selected)"}`);
6185
- lines.push(`Thinking Level: ${thinkingLevel}`);
6186
- lines.push("\n");
6187
-
6188
- // Include available tools
6189
- const tools = this.agent.state.tools;
6190
-
6191
- // Recursively strip all fields starting with 'TypeBox.' from an object
6192
- function stripTypeBoxFields(obj: any): any {
6193
- if (Array.isArray(obj)) {
6194
- return obj.map(stripTypeBoxFields);
6195
- }
6196
- if (obj && typeof obj === "object") {
6197
- const result: Record<string, any> = {};
6198
- for (const [k, v] of Object.entries(obj)) {
6199
- if (!k.startsWith("TypeBox.")) {
6200
- result[k] = stripTypeBoxFields(v);
6201
- }
6202
- }
6203
- return result;
6204
- }
6205
- return obj;
6206
- }
6207
-
6208
- if (tools.length > 0) {
6209
- lines.push("## Available Tools\n");
6210
- for (const tool of tools) {
6211
- lines.push(`<tool name="${tool.name}">`);
6212
- lines.push(tool.description);
6213
- const parametersClean = stripTypeBoxFields(tool.parameters);
6214
- lines.push(`\nParameters:\n${formatArgsAsXml(parametersClean as Record<string, unknown>)}`);
6215
- lines.push("<" + "/tool>\n");
6216
- }
6217
- lines.push("\n");
6218
- }
6219
-
6220
- for (const msg of this.messages) {
6221
- if (msg.role === "user" || msg.role === "developer") {
6222
- lines.push(msg.role === "developer" ? "## Developer\n" : "## User\n");
6223
- if (typeof msg.content === "string") {
6224
- lines.push(msg.content);
6225
- } else {
6226
- for (const c of msg.content) {
6227
- if (c.type === "text") {
6228
- lines.push(c.text);
6229
- } else if (c.type === "image") {
6230
- lines.push("[Image]");
6231
- }
6232
- }
6233
- }
6234
- lines.push("\n");
6235
- } else if (msg.role === "assistant") {
6236
- const assistantMsg = msg as AssistantMessage;
6237
- lines.push("## Assistant\n");
6238
-
6239
- for (const c of assistantMsg.content) {
6240
- if (c.type === "text") {
6241
- lines.push(c.text);
6242
- } else if (c.type === "thinking") {
6243
- lines.push("<thinking>");
6244
- lines.push(c.thinking);
6245
- lines.push("</thinking>\n");
6246
- } else if (c.type === "toolCall") {
6247
- lines.push(`<invoke name="${c.name}">`);
6248
- if (c.arguments && typeof c.arguments === "object") {
6249
- lines.push(formatArgsAsXml(c.arguments as Record<string, unknown>));
6250
- }
6251
- lines.push("<" + "/invoke>\n");
6252
- }
6253
- }
6254
- lines.push("");
6255
- } else if (msg.role === "toolResult") {
6256
- lines.push(`### Tool Result: ${msg.toolName}`);
6257
- if (msg.isError) {
6258
- lines.push("(error)");
6259
- }
6260
- for (const c of msg.content) {
6261
- if (c.type === "text") {
6262
- lines.push("```");
6263
- lines.push(c.text);
6264
- lines.push("```");
6265
- } else if (c.type === "image") {
6266
- lines.push("[Image output]");
6267
- }
6268
- }
6269
- lines.push("");
6270
- } else if (msg.role === "bashExecution") {
6271
- const bashMsg = msg as BashExecutionMessage;
6272
- if (!bashMsg.excludeFromContext) {
6273
- lines.push("## Bash Execution\n");
6274
- lines.push(bashExecutionToText(bashMsg));
6275
- lines.push("\n");
6276
- }
6277
- } else if (msg.role === "pythonExecution") {
6278
- const pythonMsg = msg as PythonExecutionMessage;
6279
- if (!pythonMsg.excludeFromContext) {
6280
- lines.push("## Python Execution\n");
6281
- lines.push(pythonExecutionToText(pythonMsg));
6282
- lines.push("\n");
6283
- }
6284
- } else if (msg.role === "custom" || msg.role === "hookMessage") {
6285
- const customMsg = msg as CustomMessage | HookMessage;
6286
- lines.push(`## ${customMsg.customType}\n`);
6287
- if (typeof customMsg.content === "string") {
6288
- lines.push(customMsg.content);
6289
- } else {
6290
- for (const c of customMsg.content) {
6291
- if (c.type === "text") {
6292
- lines.push(c.text);
6293
- } else if (c.type === "image") {
6294
- lines.push("[Image]");
6295
- }
6296
- }
6297
- }
6298
- lines.push("\n");
6299
- } else if (msg.role === "branchSummary") {
6300
- const branchMsg = msg as BranchSummaryMessage;
6301
- lines.push("## Branch Summary\n");
6302
- lines.push(`(from branch: ${branchMsg.fromId})\n`);
6303
- lines.push(branchMsg.summary);
6304
- lines.push("\n");
6305
- } else if (msg.role === "compactionSummary") {
6306
- const compactMsg = msg as CompactionSummaryMessage;
6307
- lines.push("## Compaction Summary\n");
6308
- lines.push(`(${compactMsg.tokensBefore} tokens before compaction)\n`);
6309
- lines.push(compactMsg.summary);
6310
- lines.push("\n");
6311
- } else if (msg.role === "fileMention") {
6312
- const fileMsg = msg as FileMentionMessage;
6313
- lines.push("## File Mention\n");
6314
- for (const file of fileMsg.files) {
6315
- lines.push(`<file path="${file.path}">`);
6316
- if (file.content) {
6317
- lines.push(file.content);
6318
- }
6319
- if (file.image) {
6320
- lines.push("[Image attached]");
6321
- }
6322
- lines.push("</file>\n");
6323
- }
6324
- lines.push("\n");
6325
- }
6326
- }
6327
-
6328
- return lines.join("\n").trim();
6417
+ return formatSessionDumpText({
6418
+ messages: this.messages,
6419
+ systemPrompt: this.agent.state.systemPrompt,
6420
+ model: this.agent.state.model,
6421
+ thinkingLevel: this.#thinkingLevel,
6422
+ tools: this.agent.state.tools,
6423
+ });
6329
6424
  }
6330
6425
 
6331
6426
  /**