@oh-my-pi/pi-coding-agent 13.19.0 → 14.0.3

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 (205) hide show
  1. package/CHANGELOG.md +277 -2
  2. package/package.json +86 -20
  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 +91 -0
  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 +83 -125
  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 -5
  28. package/src/commit/agentic/index.ts +3 -4
  29. package/src/commit/agentic/tools/analyze-file.ts +3 -3
  30. package/src/commit/agentic/validation.ts +1 -1
  31. package/src/commit/analysis/conventional.ts +4 -4
  32. package/src/commit/analysis/summary.ts +3 -3
  33. package/src/commit/changelog/generate.ts +4 -4
  34. package/src/commit/map-reduce/map-phase.ts +4 -4
  35. package/src/commit/map-reduce/reduce-phase.ts +4 -4
  36. package/src/commit/pipeline.ts +3 -4
  37. package/src/config/model-registry.ts +17 -3
  38. package/src/config/prompt-templates.ts +44 -226
  39. package/src/config/resolve-config-value.ts +4 -2
  40. package/src/config/settings-schema.ts +54 -2
  41. package/src/config/settings.ts +25 -26
  42. package/src/dap/client.ts +674 -0
  43. package/src/dap/config.ts +150 -0
  44. package/src/dap/defaults.json +211 -0
  45. package/src/dap/index.ts +4 -0
  46. package/src/dap/session.ts +1255 -0
  47. package/src/dap/types.ts +600 -0
  48. package/src/debug/log-viewer.ts +3 -2
  49. package/src/discovery/builtin.ts +1 -2
  50. package/src/discovery/codex.ts +2 -2
  51. package/src/discovery/github.ts +2 -1
  52. package/src/discovery/helpers.ts +2 -2
  53. package/src/discovery/opencode.ts +2 -2
  54. package/src/edit/diff.ts +818 -0
  55. package/src/edit/index.ts +309 -0
  56. package/src/edit/line-hash.ts +67 -0
  57. package/src/edit/modes/chunk.ts +454 -0
  58. package/src/{patch → edit/modes}/hashline.ts +741 -361
  59. package/src/{patch/applicator.ts → edit/modes/patch.ts} +420 -117
  60. package/src/{patch/fuzzy.ts → edit/modes/replace.ts} +519 -197
  61. package/src/{patch → edit}/normalize.ts +97 -76
  62. package/src/{patch/shared.ts → edit/renderer.ts} +181 -108
  63. package/src/exec/bash-executor.ts +4 -2
  64. package/src/exec/idle-timeout-watchdog.ts +126 -0
  65. package/src/exec/non-interactive-env.ts +5 -0
  66. package/src/extensibility/custom-commands/bundled/ci-green/index.ts +2 -2
  67. package/src/extensibility/custom-commands/bundled/review/index.ts +36 -15
  68. package/src/extensibility/custom-commands/loader.ts +1 -2
  69. package/src/extensibility/custom-tools/loader.ts +34 -11
  70. package/src/extensibility/extensions/loader.ts +9 -4
  71. package/src/extensibility/extensions/runner.ts +24 -1
  72. package/src/extensibility/extensions/types.ts +1 -1
  73. package/src/extensibility/hooks/loader.ts +5 -6
  74. package/src/extensibility/hooks/types.ts +1 -1
  75. package/src/extensibility/plugins/doctor.ts +2 -1
  76. package/src/extensibility/slash-commands.ts +3 -7
  77. package/src/index.ts +2 -1
  78. package/src/internal-urls/docs-index.generated.ts +11 -11
  79. package/src/ipy/executor.ts +58 -17
  80. package/src/ipy/gateway-coordinator.ts +6 -4
  81. package/src/ipy/kernel.ts +45 -22
  82. package/src/ipy/runtime.ts +2 -2
  83. package/src/lsp/client.ts +7 -4
  84. package/src/lsp/clients/lsp-linter-client.ts +4 -4
  85. package/src/lsp/config.ts +20 -4
  86. package/src/lsp/defaults.json +688 -154
  87. package/src/lsp/index.ts +234 -45
  88. package/src/lsp/lspmux.ts +2 -2
  89. package/src/lsp/startup-events.ts +13 -0
  90. package/src/lsp/types.ts +12 -1
  91. package/src/lsp/utils.ts +8 -1
  92. package/src/main.ts +102 -46
  93. package/src/memories/index.ts +4 -5
  94. package/src/modes/acp/acp-agent.ts +563 -163
  95. package/src/modes/acp/acp-event-mapper.ts +9 -1
  96. package/src/modes/acp/acp-mode.ts +4 -2
  97. package/src/modes/components/agent-dashboard.ts +3 -4
  98. package/src/modes/components/diff.ts +6 -7
  99. package/src/modes/components/read-tool-group.ts +6 -12
  100. package/src/modes/components/session-observer-overlay.ts +21 -12
  101. package/src/modes/components/settings-defs.ts +5 -0
  102. package/src/modes/components/tool-execution.ts +1 -1
  103. package/src/modes/components/welcome.ts +1 -1
  104. package/src/modes/controllers/btw-controller.ts +2 -2
  105. package/src/modes/controllers/command-controller.ts +3 -2
  106. package/src/modes/controllers/input-controller.ts +12 -8
  107. package/src/modes/index.ts +20 -2
  108. package/src/modes/interactive-mode.ts +94 -37
  109. package/src/modes/rpc/host-tools.ts +186 -0
  110. package/src/modes/rpc/rpc-client.ts +178 -13
  111. package/src/modes/rpc/rpc-mode.ts +73 -3
  112. package/src/modes/rpc/rpc-types.ts +53 -1
  113. package/src/modes/theme/theme.ts +80 -8
  114. package/src/modes/types.ts +2 -2
  115. package/src/prompts/review-request.md +6 -0
  116. package/src/prompts/system/system-prompt.md +2 -1
  117. package/src/prompts/tools/chunk-edit.md +223 -0
  118. package/src/prompts/tools/debug.md +43 -0
  119. package/src/prompts/tools/grep.md +3 -0
  120. package/src/prompts/tools/lsp.md +5 -5
  121. package/src/prompts/tools/read-chunk.md +17 -0
  122. package/src/prompts/tools/read.md +19 -5
  123. package/src/sdk.ts +190 -154
  124. package/src/secrets/obfuscator.ts +1 -1
  125. package/src/session/agent-session.ts +306 -256
  126. package/src/session/agent-storage.ts +12 -12
  127. package/src/session/compaction/branch-summarization.ts +3 -3
  128. package/src/session/compaction/compaction.ts +5 -6
  129. package/src/session/compaction/utils.ts +3 -3
  130. package/src/session/history-storage.ts +62 -19
  131. package/src/session/messages.ts +3 -3
  132. package/src/session/session-dump-format.ts +203 -0
  133. package/src/session/session-storage.ts +4 -2
  134. package/src/session/streaming-output.ts +1 -1
  135. package/src/session/tool-choice-queue.ts +213 -0
  136. package/src/slash-commands/builtin-registry.ts +56 -8
  137. package/src/ssh/connection-manager.ts +2 -2
  138. package/src/ssh/sshfs-mount.ts +5 -5
  139. package/src/stt/downloader.ts +4 -4
  140. package/src/stt/recorder.ts +4 -4
  141. package/src/stt/transcriber.ts +2 -2
  142. package/src/system-prompt.ts +21 -13
  143. package/src/task/agents.ts +5 -6
  144. package/src/task/commands.ts +2 -5
  145. package/src/task/executor.ts +4 -4
  146. package/src/task/index.ts +3 -4
  147. package/src/task/template.ts +2 -2
  148. package/src/task/worktree.ts +4 -4
  149. package/src/tools/ask.ts +2 -3
  150. package/src/tools/ast-edit.ts +7 -7
  151. package/src/tools/ast-grep.ts +7 -7
  152. package/src/tools/auto-generated-guard.ts +36 -41
  153. package/src/tools/await-tool.ts +2 -2
  154. package/src/tools/bash.ts +5 -23
  155. package/src/tools/browser.ts +4 -5
  156. package/src/tools/calculator.ts +2 -3
  157. package/src/tools/cancel-job.ts +2 -2
  158. package/src/tools/checkpoint.ts +3 -3
  159. package/src/tools/debug.ts +1007 -0
  160. package/src/tools/exit-plan-mode.ts +2 -3
  161. package/src/tools/fetch.ts +67 -3
  162. package/src/tools/find.ts +4 -5
  163. package/src/tools/fs-cache-invalidation.ts +5 -0
  164. package/src/tools/gemini-image.ts +13 -5
  165. package/src/tools/gh.ts +10 -11
  166. package/src/tools/grep.ts +57 -9
  167. package/src/tools/index.ts +44 -22
  168. package/src/tools/inspect-image.ts +4 -4
  169. package/src/tools/output-meta.ts +1 -1
  170. package/src/tools/python.ts +19 -6
  171. package/src/tools/read.ts +198 -67
  172. package/src/tools/render-mermaid.ts +2 -3
  173. package/src/tools/render-utils.ts +20 -6
  174. package/src/tools/renderers.ts +3 -1
  175. package/src/tools/report-tool-issue.ts +80 -0
  176. package/src/tools/resolve.ts +70 -39
  177. package/src/tools/search-tool-bm25.ts +2 -2
  178. package/src/tools/ssh.ts +2 -2
  179. package/src/tools/todo-write.ts +2 -2
  180. package/src/tools/tool-timeouts.ts +1 -0
  181. package/src/tools/write.ts +5 -6
  182. package/src/tui/tree-list.ts +3 -1
  183. package/src/utils/clipboard.ts +80 -0
  184. package/src/utils/commit-message-generator.ts +2 -3
  185. package/src/utils/edit-mode.ts +49 -0
  186. package/src/utils/file-display-mode.ts +6 -5
  187. package/src/utils/file-mentions.ts +8 -7
  188. package/src/utils/git.ts +4 -4
  189. package/src/utils/image-loading.ts +98 -0
  190. package/src/utils/title-generator.ts +2 -3
  191. package/src/utils/tools-manager.ts +6 -6
  192. package/src/web/scrapers/choosealicense.ts +1 -1
  193. package/src/web/search/index.ts +3 -3
  194. package/src/autoresearch/command-initialize.md +0 -34
  195. package/src/patch/diff.ts +0 -433
  196. package/src/patch/index.ts +0 -888
  197. package/src/patch/parser.ts +0 -532
  198. package/src/patch/types.ts +0 -292
  199. package/src/prompts/agents/oracle.md +0 -77
  200. package/src/tools/pending-action.ts +0 -49
  201. package/src/utils/child-process.ts +0 -88
  202. package/src/utils/frontmatter.ts +0 -117
  203. package/src/utils/image-input.ts +0 -274
  204. package/src/utils/mime.ts +0 -53
  205. 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";
@@ -62,8 +61,9 @@ import {
62
61
  type ResolvedModelRoleValue,
63
62
  resolveModelRoleValue,
64
63
  } from "../config/model-resolver";
65
- import { expandPromptTemplate, type PromptTemplate, renderPromptTemplate } from "../config/prompt-templates";
64
+ import { expandPromptTemplate, type PromptTemplate } from "../config/prompt-templates";
66
65
  import type { Settings, SkillsSettings } from "../config/settings";
66
+ import { normalizeDiff, normalizeToLF, ParseError, previewPatch, stripBom } from "../edit";
67
67
  import { type BashResult, executeBash as executeBashCommand } from "../exec/bash-executor";
68
68
  import { exportSessionToHtml } from "../export/html";
69
69
  import type { TtsrManager, TtsrMatchContext } from "../export/ttsr";
@@ -104,7 +104,6 @@ import {
104
104
  selectDiscoverableMCPToolNamesByServer,
105
105
  } from "../mcp/discoverable-tool-metadata";
106
106
  import { getCurrentThemeName, theme } from "../modes/theme/theme";
107
- import { normalizeDiff, normalizeToLF, ParseError, previewPatch, stripBom } from "../patch";
108
107
  import type { PlanModeState } from "../plan-mode/state";
109
108
  import autoHandoffThresholdFocusPrompt from "../prompts/system/auto-handoff-threshold-focus.md" with { type: "text" };
110
109
  import eagerTodoPrompt from "../prompts/system/eager-todo.md" with { type: "text" };
@@ -117,11 +116,13 @@ import planModeToolDecisionReminderPrompt from "../prompts/system/plan-mode-tool
117
116
  import ttsrInterruptTemplate from "../prompts/system/ttsr-interrupt.md" with { type: "text" };
118
117
  import { deobfuscateSessionContext, type SecretObfuscator } from "../secrets/obfuscator";
119
118
  import { resolveThinkingLevelForModel, toReasoningEffort } from "../thinking";
119
+ import { assertEditableFile } from "../tools/auto-generated-guard";
120
120
  import type { CheckpointState } from "../tools/checkpoint";
121
121
  import { outputMeta } from "../tools/output-meta";
122
122
  import { resolveToCwd } from "../tools/path-utils";
123
- import type { PendingActionStore } from "../tools/pending-action";
123
+ import { isAutoQaEnabled } from "../tools/report-tool-issue";
124
124
  import { getLatestTodoPhasesFromEntries, type TodoItem, type TodoPhase } from "../tools/todo-write";
125
+ import { ToolError } from "../tools/tool-errors";
125
126
  import { clampTimeout } from "../tools/tool-timeouts";
126
127
  import { parseCommandArgs } from "../utils/command-args";
127
128
  import { resolveFileDisplayMode } from "../utils/file-display-mode";
@@ -141,16 +142,13 @@ import {
141
142
  import { DEFAULT_PRUNE_CONFIG, pruneToolOutputs } from "./compaction/pruning";
142
143
  import {
143
144
  type BashExecutionMessage,
144
- type BranchSummaryMessage,
145
- bashExecutionToText,
146
145
  type CompactionSummaryMessage,
147
146
  type CustomMessage,
148
147
  convertToLlm,
149
148
  type FileMentionMessage,
150
- type HookMessage,
151
149
  type PythonExecutionMessage,
152
- pythonExecutionToText,
153
150
  } from "./messages";
151
+ import { formatSessionDumpText } from "./session-dump-format";
154
152
  import type {
155
153
  BranchSummaryEntry,
156
154
  CompactionEntry,
@@ -159,6 +157,7 @@ import type {
159
157
  SessionManager,
160
158
  } from "./session-manager";
161
159
  import { getLatestCompactionEntry } from "./session-manager";
160
+ import { ToolChoiceQueue } from "./tool-choice-queue";
162
161
 
163
162
  /** Session-specific events that extend the core AgentEvent */
164
163
  export type AgentSessionEvent =
@@ -244,8 +243,6 @@ export interface AgentSessionConfig {
244
243
  ttsrManager?: TtsrManager;
245
244
  /** Secret obfuscator for deobfuscating streaming edit content */
246
245
  obfuscator?: SecretObfuscator;
247
- /** Pending action store for preview/apply workflows */
248
- pendingActionStore?: PendingActionStore;
249
246
  /** Shared native search DB for grep/glob/fuzzyFind-backed workflows. */
250
247
  searchDb?: SearchDb;
251
248
  }
@@ -321,7 +318,7 @@ interface HandoffOptions {
321
318
 
322
319
  /** Standard thinking levels */
323
320
 
324
- const AUTO_HANDOFF_THRESHOLD_FOCUS = renderPromptTemplate(autoHandoffThresholdFocusPrompt);
321
+ const AUTO_HANDOFF_THRESHOLD_FOCUS = prompt.render(autoHandoffThresholdFocusPrompt);
325
322
 
326
323
  type RetryFallbackChains = Record<string, string[]>;
327
324
 
@@ -400,6 +397,9 @@ export class AgentSession {
400
397
  readonly sessionManager: SessionManager;
401
398
  readonly settings: Settings;
402
399
  readonly searchDb: SearchDb | undefined;
400
+
401
+ #powerAssertion: MacOSPowerAssertion | undefined;
402
+
403
403
  readonly configWarnings: string[] = [];
404
404
 
405
405
  #asyncJobManager: AsyncJobManager | undefined = undefined;
@@ -410,7 +410,6 @@ export class AgentSession {
410
410
 
411
411
  // Event subscription state
412
412
  #unsubscribeAgent?: () => void;
413
- #unsubscribePendingActionPush?: () => void;
414
413
  #eventListeners: AgentSessionEventListener[] = [];
415
414
 
416
415
  /** Tracks pending steering messages for UI display. Removed when delivered. */
@@ -445,7 +444,7 @@ export class AgentSession {
445
444
  #todoReminderCount = 0;
446
445
  #todoPhases: TodoPhase[] = [];
447
446
  #todoClearTimers = new Map<string, Timer>();
448
- #nextToolChoiceOverride: ToolChoice | undefined = undefined;
447
+ #toolChoiceQueue = new ToolChoiceQueue();
449
448
 
450
449
  // Bash execution state
451
450
  #bashAbortController: AbortController | undefined = undefined;
@@ -483,6 +482,7 @@ export class AgentSession {
483
482
  #discoverableMCPTools = new Map<string, DiscoverableMCPTool>();
484
483
  #discoverableMCPSearchIndex: DiscoverableMCPSearchIndex | null = null;
485
484
  #selectedMCPToolNames = new Set<string>();
485
+ #rpcHostToolNames = new Set<string>();
486
486
  #defaultSelectedMCPServerNames = new Set<string>();
487
487
  #defaultSelectedMCPToolNames = new Set<string>();
488
488
  #sessionDefaultSelectedMCPToolNames = new Map<string, string[]>();
@@ -502,20 +502,49 @@ export class AgentSession {
502
502
 
503
503
  #streamingEditAbortTriggered = false;
504
504
  #streamingEditCheckedLineCounts = new Map<string, number>();
505
+
506
+ #streamingEditPrecheckedToolCallIds = new Set<string>();
507
+
505
508
  #streamingEditFileCache = new Map<string, string>();
506
509
  #promptInFlightCount = 0;
507
510
  #obfuscator: SecretObfuscator | undefined;
508
- #pendingActionStore: PendingActionStore | undefined;
509
511
  #checkpointState: CheckpointState | undefined = undefined;
510
512
  #pendingRewindReport: string | undefined = undefined;
511
513
  #promptGeneration = 0;
512
514
  #providerSessionState = new Map<string, ProviderSessionState>();
513
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
+
514
540
  constructor(config: AgentSessionConfig) {
541
+ setNativeKillTree(killTree);
542
+
515
543
  this.agent = config.agent;
516
544
  this.sessionManager = config.sessionManager;
517
545
  this.settings = config.settings;
518
546
  this.searchDb = config.searchDb;
547
+ this.#startPowerAssertion();
519
548
  this.#asyncJobManager = config.asyncJobManager;
520
549
  this.#scopedModels = config.scopedModels ?? [];
521
550
  this.#thinkingLevel = config.thinkingLevel;
@@ -557,24 +586,16 @@ export class AgentSession {
557
586
  );
558
587
  this.#ttsrManager = config.ttsrManager;
559
588
  this.#obfuscator = config.obfuscator;
560
- this.agent.providerSessionState = this.#providerSessionState;
561
- this.#pendingActionStore = config.pendingActionStore;
562
- this.#unsubscribePendingActionPush = this.#pendingActionStore?.subscribePush(action => {
563
- const reminderText = [
564
- "<system-reminder>",
565
- "This is a preview. Call the `resolve` tool to apply or discard these changes.",
566
- "</system-reminder>",
567
- ].join("\n");
568
- this.agent.steer({
569
- role: "custom",
570
- customType: "resolve-reminder",
571
- content: reminderText,
572
- display: false,
573
- details: { toolName: action.sourceToolName },
574
- attribution: "agent",
575
- timestamp: Date.now(),
576
- });
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);
577
597
  });
598
+ this.agent.providerSessionState = this.#providerSessionState;
578
599
  this.#syncTodoPhasesFromBranch();
579
600
 
580
601
  // Always subscribe to agent events for internal handling
@@ -587,10 +608,40 @@ export class AgentSession {
587
608
  return this.#modelRegistry;
588
609
  }
589
610
 
590
- consumeNextToolChoiceOverride(): ToolChoice | undefined {
591
- const toolChoice = this.#nextToolChoiceOverride;
592
- this.#nextToolChoiceOverride = undefined;
593
- 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();
594
645
  }
595
646
 
596
647
  /** Provider-scoped mutable state store for transport/session caches. */
@@ -696,6 +747,17 @@ export class AgentSession {
696
747
  if (event.type === "turn_end" && this.#ttsrManager) {
697
748
  this.#ttsrManager.incrementMessageCount();
698
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
+ }
699
761
  if (event.type === "turn_end" && this.#pendingRewindReport) {
700
762
  const report = this.#pendingRewindReport;
701
763
  this.#pendingRewindReport = undefined;
@@ -794,8 +856,13 @@ export class AgentSession {
794
856
  }
795
857
  }
796
858
 
797
- if (event.type === "message_update" && event.assistantMessageEvent.type === "toolcall_start") {
798
- 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);
799
866
  }
800
867
 
801
868
  if (
@@ -1113,7 +1180,7 @@ export class AgentSession {
1113
1180
  if (this.#pendingTtsrInjections.length === 0) return undefined;
1114
1181
  const rules = this.#pendingTtsrInjections;
1115
1182
  const content = rules
1116
- .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 }))
1117
1184
  .join("\n\n");
1118
1185
  this.#pendingTtsrInjections = [];
1119
1186
  return { content, rules };
@@ -1326,31 +1393,101 @@ export class AgentSession {
1326
1393
  #resetStreamingEditState(): void {
1327
1394
  this.#streamingEditAbortTriggered = false;
1328
1395
  this.#streamingEditCheckedLineCounts.clear();
1396
+ this.#streamingEditPrecheckedToolCallIds.clear();
1329
1397
  this.#streamingEditFileCache.clear();
1330
1398
  }
1331
1399
 
1332
- async #preCacheStreamingEditFile(event: AgentEvent): Promise<void> {
1333
- if (!this.settings.get("edit.streamingAbort")) return;
1334
- if (event.type !== "message_update") return;
1335
- const assistantEvent = event.assistantMessageEvent;
1336
- if (assistantEvent.type !== "toolcall_start") return;
1337
- if (event.message.role !== "assistant") return;
1338
-
1339
- 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;
1340
1414
  const messageContent = event.message.content;
1341
- if (!Array.isArray(messageContent) || contentIndex >= messageContent.length) return;
1415
+ if (!Array.isArray(messageContent) || contentIndex < 0 || contentIndex >= messageContent.length) {
1416
+ return undefined;
1417
+ }
1418
+
1342
1419
  const toolCall = messageContent[contentIndex] as ToolCall;
1343
- if (toolCall.name !== "edit") return;
1420
+ if (toolCall.name !== "edit") return undefined;
1344
1421
 
1345
1422
  const args = toolCall.arguments;
1346
- if (!args || typeof args !== "object" || Array.isArray(args)) return;
1347
- 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;
1348
1425
 
1349
1426
  const path = typeof args.path === "string" ? args.path : undefined;
1350
- if (!path) return;
1427
+ if (!path) return undefined;
1351
1428
 
1352
- const resolvedPath = resolveToCwd(path, this.sessionManager.getCwd());
1353
- 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);
1354
1491
  }
1355
1492
 
1356
1493
  #ensureFileCache(resolvedPath: string): void {
@@ -1375,24 +1512,15 @@ export class AgentSession {
1375
1512
  if (!this.settings.get("edit.streamingAbort")) return;
1376
1513
  if (this.#streamingEditAbortTriggered) return;
1377
1514
  if (event.type !== "message_update") return;
1515
+
1378
1516
  const assistantEvent = event.assistantMessageEvent;
1379
1517
  if (assistantEvent.type !== "toolcall_end" && assistantEvent.type !== "toolcall_delta") return;
1380
- if (event.message.role !== "assistant") return;
1381
1518
 
1382
- const contentIndex = assistantEvent.contentIndex;
1383
- const messageContent = event.message.content;
1384
- if (!Array.isArray(messageContent) || contentIndex >= messageContent.length) return;
1385
- const toolCall = messageContent[contentIndex] as ToolCall;
1386
- if (toolCall.name !== "edit" || !toolCall.id) return;
1387
-
1388
- const args = toolCall.arguments;
1389
- if (!args || typeof args !== "object" || Array.isArray(args)) return;
1390
- if ("old_text" in args || "new_text" in args) return;
1519
+ const streamingEdit = this.#getStreamingEditToolCall(event);
1520
+ if (!streamingEdit?.toolCall.id) return;
1391
1521
 
1392
- const path = typeof args.path === "string" ? args.path : undefined;
1393
- const diff = typeof args.diff === "string" ? args.diff : undefined;
1394
- const op = typeof args.op === "string" ? args.op : undefined;
1395
- if (!path || !diff) return;
1522
+ const { toolCall, path, resolvedPath, diff, op, rename } = streamingEdit;
1523
+ if (!diff) return;
1396
1524
  if (op && op !== "update") return;
1397
1525
 
1398
1526
  if (!diff.includes("\n")) return;
@@ -1415,13 +1543,10 @@ export class AgentSession {
1415
1543
  if (lastChecked !== undefined && lineCount <= lastChecked) return;
1416
1544
  this.#streamingEditCheckedLineCounts.set(toolCall.id, lineCount);
1417
1545
 
1418
- const rename = typeof args.rename === "string" ? args.rename : undefined;
1419
-
1420
1546
  const removedLines = lines
1421
1547
  .filter(line => line.startsWith("-") && !line.startsWith("--- "))
1422
1548
  .map(line => line.slice(1));
1423
1549
  if (removedLines.length > 0) {
1424
- const resolvedPath = resolveToCwd(path, this.sessionManager.getCwd());
1425
1550
  let cachedContent = this.#streamingEditFileCache.get(resolvedPath);
1426
1551
  if (cachedContent === undefined) {
1427
1552
  this.#ensureFileCache(resolvedPath);
@@ -1511,7 +1636,6 @@ export class AgentSession {
1511
1636
  if (!this.#extensionRunner) return;
1512
1637
  if (event.type === "agent_start") {
1513
1638
  this.#turnIndex = 0;
1514
- this.#nextToolChoiceOverride = undefined;
1515
1639
  await this.#extensionRunner.emit({ type: "agent_start" });
1516
1640
  } else if (event.type === "agent_end") {
1517
1641
  await this.#extensionRunner.emit({ type: "agent_end", messages: event.messages });
@@ -1677,10 +1801,9 @@ export class AgentSession {
1677
1801
  if (drained === false && deliveryState) {
1678
1802
  logger.warn("Async job completion deliveries still pending during dispose", { ...deliveryState });
1679
1803
  }
1804
+ this.#stopPowerAssertion();
1680
1805
  await this.sessionManager.close();
1681
1806
  this.#closeAllProviderSessions("dispose");
1682
- this.#unsubscribePendingActionPush?.();
1683
- this.#unsubscribePendingActionPush = undefined;
1684
1807
  this.#disconnectFromAgent();
1685
1808
  this.#eventListeners = [];
1686
1809
  }
@@ -1893,6 +2016,14 @@ export class AgentSession {
1893
2016
  validToolNames.push(name);
1894
2017
  }
1895
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
+ }
1896
2027
  if (this.#mcpDiscoveryEnabled) {
1897
2028
  this.#selectedMCPToolNames = new Set(
1898
2029
  validToolNames.filter(
@@ -1999,6 +2130,49 @@ export class AgentSession {
1999
2130
  await this.#applyActiveToolsByName(nextActive, { previousSelectedMCPToolNames });
2000
2131
  }
2001
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
+
2002
2176
  /** Whether auto-compaction is currently running */
2003
2177
  get isCompacting(): boolean {
2004
2178
  return this.#autoCompactionAbortController !== undefined || this.#compactionAbortController !== undefined;
@@ -2183,7 +2357,7 @@ export class AgentSession {
2183
2357
  throw error;
2184
2358
  }
2185
2359
 
2186
- const content = renderPromptTemplate(planModeReferencePrompt, {
2360
+ const content = prompt.render(planModeReferencePrompt, {
2187
2361
  planFilePath,
2188
2362
  planContent,
2189
2363
  });
@@ -2220,7 +2394,7 @@ export class AgentSession {
2220
2394
  : sessionPlanUrl;
2221
2395
 
2222
2396
  const planExists = fs.existsSync(resolvedPlanPath);
2223
- const content = renderPromptTemplate(planModeActivePrompt, {
2397
+ const content = prompt.render(planModeActivePrompt, {
2224
2398
  planFilePath: displayPlanPath,
2225
2399
  planExists,
2226
2400
  askToolName: "ask",
@@ -2292,7 +2466,10 @@ export class AgentSession {
2292
2466
  return;
2293
2467
  }
2294
2468
 
2295
- 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;
2296
2473
 
2297
2474
  const userContent: (TextContent | ImageContent)[] = [{ type: "text", text: expandedText }];
2298
2475
  if (options?.images) {
@@ -2305,7 +2482,9 @@ export class AgentSession {
2305
2482
  : { role: "user" as const, content: userContent, attribution: promptAttribution, timestamp: Date.now() };
2306
2483
 
2307
2484
  if (eagerTodoPrelude) {
2308
- this.#nextToolChoiceOverride = eagerTodoPrelude.toolChoice;
2485
+ this.#toolChoiceQueue.pushOnce(eagerTodoPrelude.toolChoice, {
2486
+ label: "eager-todo",
2487
+ });
2309
2488
  }
2310
2489
 
2311
2490
  try {
@@ -2314,9 +2493,9 @@ export class AgentSession {
2314
2493
  prependMessages: eagerTodoPrelude ? [eagerTodoPrelude.message] : undefined,
2315
2494
  });
2316
2495
  } finally {
2317
- if (eagerTodoPrelude) {
2318
- this.#nextToolChoiceOverride = undefined;
2319
- }
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");
2320
2499
  }
2321
2500
  if (!options?.synthetic) {
2322
2501
  await this.#enforcePlanModeToolDecision();
@@ -2680,6 +2859,10 @@ export class AgentSession {
2680
2859
  });
2681
2860
  }
2682
2861
 
2862
+ queueDeferredMessage(message: CustomMessage): void {
2863
+ this.#queueHiddenNextTurnMessage(message, true);
2864
+ }
2865
+
2683
2866
  #queueHiddenNextTurnMessage(message: CustomMessage, triggerTurn: boolean): void {
2684
2867
  this.#pendingNextTurnMessages.push(message);
2685
2868
  if (!triggerTurn) return;
@@ -3036,6 +3219,13 @@ export class AgentSession {
3036
3219
  // block runs, but nested prompt setup/finalizers may still be unwinding. Without this,
3037
3220
  // a subsequent prompt() can incorrectly observe the session as busy after an abort.
3038
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
+ }
3039
3229
  }
3040
3230
 
3041
3231
  /**
@@ -3742,7 +3932,7 @@ export class AgentSession {
3742
3932
  }
3743
3933
 
3744
3934
  // Build the handoff prompt
3745
- const handoffPrompt = renderPromptTemplate(handoffDocumentPrompt, {
3935
+ const handoffPrompt = prompt.render(handoffDocumentPrompt, {
3746
3936
  additionalFocus: customInstructions,
3747
3937
  });
3748
3938
 
@@ -4010,7 +4200,7 @@ export class AgentSession {
4010
4200
  return;
4011
4201
  }
4012
4202
 
4013
- const reminder = renderPromptTemplate(planModeToolDecisionReminderPrompt, {
4203
+ const reminder = prompt.render(planModeToolDecisionReminderPrompt, {
4014
4204
  askToolName: "ask",
4015
4205
  exitToolName: "exit_plan_mode",
4016
4206
  });
@@ -4022,7 +4212,7 @@ export class AgentSession {
4022
4212
  });
4023
4213
  }
4024
4214
 
4025
- #createEagerTodoPrelude(): { message: AgentMessage; toolChoice: ToolChoice } | undefined {
4215
+ #createEagerTodoPrelude(promptText: string): { message: AgentMessage; toolChoice: ToolChoice } | undefined {
4026
4216
  const eagerTodosEnabled = this.settings.get("todo.eager");
4027
4217
  const todosEnabled = this.settings.get("todo.enabled");
4028
4218
  if (!eagerTodosEnabled || !todosEnabled) {
@@ -4036,6 +4226,11 @@ export class AgentSession {
4036
4226
  return undefined;
4037
4227
  }
4038
4228
 
4229
+ const trimmedPromptText = promptText.trimEnd();
4230
+ if (trimmedPromptText.endsWith("?") || trimmedPromptText.endsWith("!")) {
4231
+ return undefined;
4232
+ }
4233
+
4039
4234
  if (!this.#toolRegistry.has("todo_write")) {
4040
4235
  logger.warn("Eager todo enforcement skipped because todo_write is unavailable", {
4041
4236
  activeToolNames: this.agent.state.tools.map(tool => tool.name),
@@ -4052,7 +4247,7 @@ export class AgentSession {
4052
4247
  return undefined;
4053
4248
  }
4054
4249
 
4055
- const eagerTodoReminder = renderPromptTemplate(eagerTodoPrompt);
4250
+ const eagerTodoReminder = prompt.render(eagerTodoPrompt);
4056
4251
 
4057
4252
  return {
4058
4253
  message: {
@@ -4070,6 +4265,13 @@ export class AgentSession {
4070
4265
  * Check if agent stopped with incomplete todos and prompt to continue.
4071
4266
  */
4072
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
+
4073
4275
  const remindersEnabled = this.settings.get("todo.reminders");
4074
4276
  const todosEnabled = this.settings.get("todo.enabled");
4075
4277
  if (!remindersEnabled || !todosEnabled) {
@@ -4868,6 +5070,17 @@ export class AgentSession {
4868
5070
  }
4869
5071
 
4870
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 {
4871
5084
  // Match: overloaded_error, provider returned error, rate limit, 429, 500, 502, 503, 504,
4872
5085
  // service unavailable, network/connection errors, fetch failed, terminated, retry delay exceeded
4873
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(
@@ -6201,176 +6414,13 @@ export class AgentSession {
6201
6414
  * Includes user messages, assistant text, thinking blocks, tool calls, and tool results.
6202
6415
  */
6203
6416
  formatSessionAsText(): string {
6204
- const lines: string[] = [];
6205
-
6206
- /** Serialize an object as XML parameter elements, one per key. */
6207
- function formatArgsAsXml(args: Record<string, unknown>, indent = "\t"): string {
6208
- const parts: string[] = [];
6209
- for (const [key, value] of Object.entries(args)) {
6210
- if (key === INTENT_FIELD) continue;
6211
- const text = typeof value === "string" ? value : JSON.stringify(value);
6212
- parts.push(`${indent}<parameter name="${key}">${text}</parameter>`);
6213
- }
6214
- return parts.join("\n");
6215
- }
6216
-
6217
- // Include system prompt at the beginning
6218
- const systemPrompt = this.agent.state.systemPrompt;
6219
- if (systemPrompt) {
6220
- lines.push("## System Prompt\n");
6221
- lines.push(systemPrompt);
6222
- lines.push("\n");
6223
- }
6224
-
6225
- // Include model and thinking level
6226
- const model = this.agent.state.model;
6227
- const thinkingLevel = this.#thinkingLevel;
6228
- lines.push("## Configuration\n");
6229
- lines.push(`Model: ${model ? `${model.provider}/${model.id}` : "(not selected)"}`);
6230
- lines.push(`Thinking Level: ${thinkingLevel}`);
6231
- lines.push("\n");
6232
-
6233
- // Include available tools
6234
- const tools = this.agent.state.tools;
6235
-
6236
- // Recursively strip all fields starting with 'TypeBox.' from an object
6237
- function stripTypeBoxFields(obj: any): any {
6238
- if (Array.isArray(obj)) {
6239
- return obj.map(stripTypeBoxFields);
6240
- }
6241
- if (obj && typeof obj === "object") {
6242
- const result: Record<string, any> = {};
6243
- for (const [k, v] of Object.entries(obj)) {
6244
- if (!k.startsWith("TypeBox.")) {
6245
- result[k] = stripTypeBoxFields(v);
6246
- }
6247
- }
6248
- return result;
6249
- }
6250
- return obj;
6251
- }
6252
-
6253
- if (tools.length > 0) {
6254
- lines.push("## Available Tools\n");
6255
- for (const tool of tools) {
6256
- lines.push(`<tool name="${tool.name}">`);
6257
- lines.push(tool.description);
6258
- const parametersClean = stripTypeBoxFields(tool.parameters);
6259
- lines.push(`\nParameters:\n${formatArgsAsXml(parametersClean as Record<string, unknown>)}`);
6260
- lines.push("<" + "/tool>\n");
6261
- }
6262
- lines.push("\n");
6263
- }
6264
-
6265
- for (const msg of this.messages) {
6266
- if (msg.role === "user" || msg.role === "developer") {
6267
- lines.push(msg.role === "developer" ? "## Developer\n" : "## User\n");
6268
- if (typeof msg.content === "string") {
6269
- lines.push(msg.content);
6270
- } else {
6271
- for (const c of msg.content) {
6272
- if (c.type === "text") {
6273
- lines.push(c.text);
6274
- } else if (c.type === "image") {
6275
- lines.push("[Image]");
6276
- }
6277
- }
6278
- }
6279
- lines.push("\n");
6280
- } else if (msg.role === "assistant") {
6281
- const assistantMsg = msg as AssistantMessage;
6282
- lines.push("## Assistant\n");
6283
-
6284
- for (const c of assistantMsg.content) {
6285
- if (c.type === "text") {
6286
- lines.push(c.text);
6287
- } else if (c.type === "thinking") {
6288
- lines.push("<thinking>");
6289
- lines.push(c.thinking);
6290
- lines.push("</thinking>\n");
6291
- } else if (c.type === "toolCall") {
6292
- lines.push(`<invoke name="${c.name}">`);
6293
- if (c.arguments && typeof c.arguments === "object") {
6294
- lines.push(formatArgsAsXml(c.arguments as Record<string, unknown>));
6295
- }
6296
- lines.push("<" + "/invoke>\n");
6297
- }
6298
- }
6299
- lines.push("");
6300
- } else if (msg.role === "toolResult") {
6301
- lines.push(`### Tool Result: ${msg.toolName}`);
6302
- if (msg.isError) {
6303
- lines.push("(error)");
6304
- }
6305
- for (const c of msg.content) {
6306
- if (c.type === "text") {
6307
- lines.push("```");
6308
- lines.push(c.text);
6309
- lines.push("```");
6310
- } else if (c.type === "image") {
6311
- lines.push("[Image output]");
6312
- }
6313
- }
6314
- lines.push("");
6315
- } else if (msg.role === "bashExecution") {
6316
- const bashMsg = msg as BashExecutionMessage;
6317
- if (!bashMsg.excludeFromContext) {
6318
- lines.push("## Bash Execution\n");
6319
- lines.push(bashExecutionToText(bashMsg));
6320
- lines.push("\n");
6321
- }
6322
- } else if (msg.role === "pythonExecution") {
6323
- const pythonMsg = msg as PythonExecutionMessage;
6324
- if (!pythonMsg.excludeFromContext) {
6325
- lines.push("## Python Execution\n");
6326
- lines.push(pythonExecutionToText(pythonMsg));
6327
- lines.push("\n");
6328
- }
6329
- } else if (msg.role === "custom" || msg.role === "hookMessage") {
6330
- const customMsg = msg as CustomMessage | HookMessage;
6331
- lines.push(`## ${customMsg.customType}\n`);
6332
- if (typeof customMsg.content === "string") {
6333
- lines.push(customMsg.content);
6334
- } else {
6335
- for (const c of customMsg.content) {
6336
- if (c.type === "text") {
6337
- lines.push(c.text);
6338
- } else if (c.type === "image") {
6339
- lines.push("[Image]");
6340
- }
6341
- }
6342
- }
6343
- lines.push("\n");
6344
- } else if (msg.role === "branchSummary") {
6345
- const branchMsg = msg as BranchSummaryMessage;
6346
- lines.push("## Branch Summary\n");
6347
- lines.push(`(from branch: ${branchMsg.fromId})\n`);
6348
- lines.push(branchMsg.summary);
6349
- lines.push("\n");
6350
- } else if (msg.role === "compactionSummary") {
6351
- const compactMsg = msg as CompactionSummaryMessage;
6352
- lines.push("## Compaction Summary\n");
6353
- lines.push(`(${compactMsg.tokensBefore} tokens before compaction)\n`);
6354
- lines.push(compactMsg.summary);
6355
- lines.push("\n");
6356
- } else if (msg.role === "fileMention") {
6357
- const fileMsg = msg as FileMentionMessage;
6358
- lines.push("## File Mention\n");
6359
- for (const file of fileMsg.files) {
6360
- lines.push(`<file path="${file.path}">`);
6361
- if (file.content) {
6362
- lines.push(file.content);
6363
- }
6364
- if (file.image) {
6365
- lines.push("[Image attached]");
6366
- }
6367
- lines.push("</file>\n");
6368
- }
6369
- lines.push("\n");
6370
- }
6371
- }
6372
-
6373
- 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
+ });
6374
6424
  }
6375
6425
 
6376
6426
  /**