@codex-infinity/pi-infinity 0.52.2 → 0.60.1

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 (258) hide show
  1. package/CHANGELOG.md +386 -0
  2. package/README.md +97 -66
  3. package/dist/bun/cli.d.ts +3 -0
  4. package/dist/bun/cli.d.ts.map +1 -0
  5. package/dist/bun/cli.js +6 -0
  6. package/dist/bun/cli.js.map +1 -0
  7. package/dist/bun/register-bedrock.d.ts +2 -0
  8. package/dist/bun/register-bedrock.d.ts.map +1 -0
  9. package/dist/bun/register-bedrock.js +4 -0
  10. package/dist/bun/register-bedrock.js.map +1 -0
  11. package/dist/cli/args.d.ts +2 -0
  12. package/dist/cli/args.d.ts.map +1 -1
  13. package/dist/cli/args.js +17 -6
  14. package/dist/cli/args.js.map +1 -1
  15. package/dist/cli/initial-message.d.ts +18 -0
  16. package/dist/cli/initial-message.d.ts.map +1 -0
  17. package/dist/cli/initial-message.js +22 -0
  18. package/dist/cli/initial-message.js.map +1 -0
  19. package/dist/cli.d.ts.map +1 -1
  20. package/dist/cli.js +2 -0
  21. package/dist/cli.js.map +1 -1
  22. package/dist/core/agent-session.d.ts +28 -6
  23. package/dist/core/agent-session.d.ts.map +1 -1
  24. package/dist/core/agent-session.js +289 -69
  25. package/dist/core/agent-session.js.map +1 -1
  26. package/dist/core/auth-storage.d.ts +1 -0
  27. package/dist/core/auth-storage.d.ts.map +1 -1
  28. package/dist/core/auth-storage.js +27 -2
  29. package/dist/core/auth-storage.js.map +1 -1
  30. package/dist/core/bash-executor.d.ts +6 -7
  31. package/dist/core/bash-executor.d.ts.map +1 -1
  32. package/dist/core/bash-executor.js +8 -107
  33. package/dist/core/bash-executor.js.map +1 -1
  34. package/dist/core/compaction/branch-summarization.d.ts.map +1 -1
  35. package/dist/core/compaction/branch-summarization.js +1 -0
  36. package/dist/core/compaction/branch-summarization.js.map +1 -1
  37. package/dist/core/compaction/compaction.d.ts.map +1 -1
  38. package/dist/core/compaction/compaction.js +6 -1
  39. package/dist/core/compaction/compaction.js.map +1 -1
  40. package/dist/core/compaction/utils.d.ts +3 -0
  41. package/dist/core/compaction/utils.d.ts.map +1 -1
  42. package/dist/core/compaction/utils.js +16 -1
  43. package/dist/core/compaction/utils.js.map +1 -1
  44. package/dist/core/export-html/index.d.ts +5 -2
  45. package/dist/core/export-html/index.d.ts.map +1 -1
  46. package/dist/core/export-html/index.js +4 -3
  47. package/dist/core/export-html/index.js.map +1 -1
  48. package/dist/core/export-html/template.js +11 -14
  49. package/dist/core/export-html/tool-renderer.d.ts +5 -2
  50. package/dist/core/export-html/tool-renderer.d.ts.map +1 -1
  51. package/dist/core/export-html/tool-renderer.js +17 -4
  52. package/dist/core/export-html/tool-renderer.js.map +1 -1
  53. package/dist/core/extensions/index.d.ts +2 -2
  54. package/dist/core/extensions/index.d.ts.map +1 -1
  55. package/dist/core/extensions/index.js +1 -1
  56. package/dist/core/extensions/index.js.map +1 -1
  57. package/dist/core/extensions/loader.d.ts.map +1 -1
  58. package/dist/core/extensions/loader.js +37 -11
  59. package/dist/core/extensions/loader.js.map +1 -1
  60. package/dist/core/extensions/runner.d.ts +8 -4
  61. package/dist/core/extensions/runner.d.ts.map +1 -1
  62. package/dist/core/extensions/runner.js +77 -8
  63. package/dist/core/extensions/runner.js.map +1 -1
  64. package/dist/core/extensions/types.d.ts +56 -4
  65. package/dist/core/extensions/types.d.ts.map +1 -1
  66. package/dist/core/extensions/types.js.map +1 -1
  67. package/dist/core/extensions/wrapper.d.ts +4 -11
  68. package/dist/core/extensions/wrapper.d.ts.map +1 -1
  69. package/dist/core/extensions/wrapper.js +4 -78
  70. package/dist/core/extensions/wrapper.js.map +1 -1
  71. package/dist/core/footer-data-provider.d.ts +6 -1
  72. package/dist/core/footer-data-provider.d.ts.map +1 -1
  73. package/dist/core/footer-data-provider.js +83 -37
  74. package/dist/core/footer-data-provider.js.map +1 -1
  75. package/dist/core/index.d.ts +1 -1
  76. package/dist/core/index.d.ts.map +1 -1
  77. package/dist/core/index.js +1 -1
  78. package/dist/core/index.js.map +1 -1
  79. package/dist/core/keybindings.d.ts +3 -0
  80. package/dist/core/keybindings.d.ts.map +1 -1
  81. package/dist/core/keybindings.js +22 -12
  82. package/dist/core/keybindings.js.map +1 -1
  83. package/dist/core/model-registry.d.ts +11 -0
  84. package/dist/core/model-registry.d.ts.map +1 -1
  85. package/dist/core/model-registry.js +56 -16
  86. package/dist/core/model-registry.js.map +1 -1
  87. package/dist/core/model-resolver.d.ts +6 -0
  88. package/dist/core/model-resolver.d.ts.map +1 -1
  89. package/dist/core/model-resolver.js +126 -43
  90. package/dist/core/model-resolver.js.map +1 -1
  91. package/dist/core/package-manager.d.ts +19 -1
  92. package/dist/core/package-manager.d.ts.map +1 -1
  93. package/dist/core/package-manager.js +290 -57
  94. package/dist/core/package-manager.js.map +1 -1
  95. package/dist/core/resolve-config-value.d.ts.map +1 -1
  96. package/dist/core/resolve-config-value.js +43 -8
  97. package/dist/core/resolve-config-value.js.map +1 -1
  98. package/dist/core/resource-loader.d.ts.map +1 -1
  99. package/dist/core/resource-loader.js +4 -7
  100. package/dist/core/resource-loader.js.map +1 -1
  101. package/dist/core/sdk.d.ts +1 -1
  102. package/dist/core/sdk.d.ts.map +1 -1
  103. package/dist/core/sdk.js +7 -0
  104. package/dist/core/sdk.js.map +1 -1
  105. package/dist/core/session-manager.d.ts +1 -0
  106. package/dist/core/session-manager.d.ts.map +1 -1
  107. package/dist/core/session-manager.js +21 -15
  108. package/dist/core/session-manager.js.map +1 -1
  109. package/dist/core/settings-manager.d.ts +10 -0
  110. package/dist/core/settings-manager.d.ts.map +1 -1
  111. package/dist/core/settings-manager.js +59 -5
  112. package/dist/core/settings-manager.js.map +1 -1
  113. package/dist/core/skills.d.ts +3 -2
  114. package/dist/core/skills.d.ts.map +1 -1
  115. package/dist/core/skills.js +29 -8
  116. package/dist/core/skills.js.map +1 -1
  117. package/dist/core/slash-commands.d.ts.map +1 -1
  118. package/dist/core/slash-commands.js +1 -1
  119. package/dist/core/slash-commands.js.map +1 -1
  120. package/dist/core/system-prompt.d.ts +4 -0
  121. package/dist/core/system-prompt.d.ts.map +1 -1
  122. package/dist/core/system-prompt.js +43 -29
  123. package/dist/core/system-prompt.js.map +1 -1
  124. package/dist/core/tools/bash.d.ts +8 -0
  125. package/dist/core/tools/bash.d.ts.map +1 -1
  126. package/dist/core/tools/bash.js +75 -69
  127. package/dist/core/tools/bash.js.map +1 -1
  128. package/dist/core/tools/edit-diff.d.ts.map +1 -1
  129. package/dist/core/tools/edit-diff.js +1 -0
  130. package/dist/core/tools/edit-diff.js.map +1 -1
  131. package/dist/core/tools/find.d.ts.map +1 -1
  132. package/dist/core/tools/find.js +6 -3
  133. package/dist/core/tools/find.js.map +1 -1
  134. package/dist/core/tools/index.d.ts +1 -1
  135. package/dist/core/tools/index.d.ts.map +1 -1
  136. package/dist/core/tools/index.js +1 -1
  137. package/dist/core/tools/index.js.map +1 -1
  138. package/dist/index.d.ts +3 -3
  139. package/dist/index.d.ts.map +1 -1
  140. package/dist/index.js +2 -2
  141. package/dist/index.js.map +1 -1
  142. package/dist/main.d.ts.map +1 -1
  143. package/dist/main.js +116 -36
  144. package/dist/main.js.map +1 -1
  145. package/dist/modes/interactive/components/extension-editor.d.ts +5 -2
  146. package/dist/modes/interactive/components/extension-editor.d.ts.map +1 -1
  147. package/dist/modes/interactive/components/extension-editor.js +9 -0
  148. package/dist/modes/interactive/components/extension-editor.js.map +1 -1
  149. package/dist/modes/interactive/components/footer.d.ts.map +1 -1
  150. package/dist/modes/interactive/components/footer.js +8 -23
  151. package/dist/modes/interactive/components/footer.js.map +1 -1
  152. package/dist/modes/interactive/components/login-dialog.d.ts.map +1 -1
  153. package/dist/modes/interactive/components/login-dialog.js +1 -1
  154. package/dist/modes/interactive/components/login-dialog.js.map +1 -1
  155. package/dist/modes/interactive/components/model-selector.d.ts +1 -1
  156. package/dist/modes/interactive/components/model-selector.d.ts.map +1 -1
  157. package/dist/modes/interactive/components/model-selector.js +1 -1
  158. package/dist/modes/interactive/components/model-selector.js.map +1 -1
  159. package/dist/modes/interactive/components/oauth-selector.d.ts.map +1 -1
  160. package/dist/modes/interactive/components/oauth-selector.js +1 -1
  161. package/dist/modes/interactive/components/oauth-selector.js.map +1 -1
  162. package/dist/modes/interactive/components/session-selector.d.ts.map +1 -1
  163. package/dist/modes/interactive/components/session-selector.js +1 -1
  164. package/dist/modes/interactive/components/session-selector.js.map +1 -1
  165. package/dist/modes/interactive/components/settings-selector.d.ts +2 -0
  166. package/dist/modes/interactive/components/settings-selector.d.ts.map +1 -1
  167. package/dist/modes/interactive/components/settings-selector.js +15 -1
  168. package/dist/modes/interactive/components/settings-selector.js.map +1 -1
  169. package/dist/modes/interactive/components/show-images-selector.d.ts.map +1 -1
  170. package/dist/modes/interactive/components/show-images-selector.js +5 -1
  171. package/dist/modes/interactive/components/show-images-selector.js.map +1 -1
  172. package/dist/modes/interactive/components/theme-selector.d.ts.map +1 -1
  173. package/dist/modes/interactive/components/theme-selector.js +5 -1
  174. package/dist/modes/interactive/components/theme-selector.js.map +1 -1
  175. package/dist/modes/interactive/components/thinking-selector.d.ts.map +1 -1
  176. package/dist/modes/interactive/components/thinking-selector.js +5 -1
  177. package/dist/modes/interactive/components/thinking-selector.js.map +1 -1
  178. package/dist/modes/interactive/components/tool-execution.d.ts +7 -0
  179. package/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
  180. package/dist/modes/interactive/components/tool-execution.js +158 -7
  181. package/dist/modes/interactive/components/tool-execution.js.map +1 -1
  182. package/dist/modes/interactive/components/tree-selector.d.ts +21 -2
  183. package/dist/modes/interactive/components/tree-selector.d.ts.map +1 -1
  184. package/dist/modes/interactive/components/tree-selector.js +127 -10
  185. package/dist/modes/interactive/components/tree-selector.js.map +1 -1
  186. package/dist/modes/interactive/components/user-message.d.ts +1 -0
  187. package/dist/modes/interactive/components/user-message.d.ts.map +1 -1
  188. package/dist/modes/interactive/components/user-message.js +12 -0
  189. package/dist/modes/interactive/components/user-message.js.map +1 -1
  190. package/dist/modes/interactive/interactive-mode.d.ts +4 -1
  191. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  192. package/dist/modes/interactive/interactive-mode.js +160 -66
  193. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  194. package/dist/modes/interactive/theme/theme.d.ts.map +1 -1
  195. package/dist/modes/interactive/theme/theme.js +5 -0
  196. package/dist/modes/interactive/theme/theme.js.map +1 -1
  197. package/dist/modes/rpc/jsonl.d.ts +17 -0
  198. package/dist/modes/rpc/jsonl.d.ts.map +1 -0
  199. package/dist/modes/rpc/jsonl.js +49 -0
  200. package/dist/modes/rpc/jsonl.js.map +1 -0
  201. package/dist/modes/rpc/rpc-client.d.ts +1 -1
  202. package/dist/modes/rpc/rpc-client.d.ts.map +1 -1
  203. package/dist/modes/rpc/rpc-client.js +7 -11
  204. package/dist/modes/rpc/rpc-client.js.map +1 -1
  205. package/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
  206. package/dist/modes/rpc/rpc-mode.js +9 -11
  207. package/dist/modes/rpc/rpc-mode.js.map +1 -1
  208. package/dist/utils/clipboard-image.d.ts.map +1 -1
  209. package/dist/utils/clipboard-image.js +94 -11
  210. package/dist/utils/clipboard-image.js.map +1 -1
  211. package/dist/utils/clipboard.d.ts.map +1 -1
  212. package/dist/utils/clipboard.js +16 -15
  213. package/dist/utils/clipboard.js.map +1 -1
  214. package/dist/utils/exif-orientation.d.ts +5 -0
  215. package/dist/utils/exif-orientation.d.ts.map +1 -0
  216. package/dist/utils/exif-orientation.js +158 -0
  217. package/dist/utils/exif-orientation.js.map +1 -0
  218. package/dist/utils/image-convert.d.ts.map +1 -1
  219. package/dist/utils/image-convert.js +5 -1
  220. package/dist/utils/image-convert.js.map +1 -1
  221. package/dist/utils/image-resize.d.ts.map +1 -1
  222. package/dist/utils/image-resize.js +6 -1
  223. package/dist/utils/image-resize.js.map +1 -1
  224. package/dist/utils/tools-manager.d.ts.map +1 -1
  225. package/dist/utils/tools-manager.js +66 -21
  226. package/dist/utils/tools-manager.js.map +1 -1
  227. package/docs/compaction.md +2 -0
  228. package/docs/custom-provider.md +57 -9
  229. package/docs/extensions.md +125 -12
  230. package/docs/keybindings.md +11 -1
  231. package/docs/models.md +44 -2
  232. package/docs/packages.md +9 -0
  233. package/docs/providers.md +10 -1
  234. package/docs/rpc.md +44 -7
  235. package/docs/sdk.md +2 -2
  236. package/docs/settings.md +11 -0
  237. package/docs/terminal-setup.md +39 -3
  238. package/docs/tmux.md +61 -0
  239. package/docs/tree.md +9 -0
  240. package/examples/extensions/README.md +2 -0
  241. package/examples/extensions/antigravity-image-gen.ts +8 -5
  242. package/examples/extensions/built-in-tool-renderer.ts +246 -0
  243. package/examples/extensions/custom-provider-anthropic/package-lock.json +2 -2
  244. package/examples/extensions/custom-provider-anthropic/package.json +1 -1
  245. package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
  246. package/examples/extensions/custom-provider-gitlab-duo/test.ts +2 -2
  247. package/examples/extensions/custom-provider-qwen-cli/package.json +1 -1
  248. package/examples/extensions/dynamic-tools.ts +74 -0
  249. package/examples/extensions/overlay-qa-tests.ts +468 -1
  250. package/examples/extensions/preset.ts +2 -3
  251. package/examples/extensions/provider-payload.ts +14 -0
  252. package/examples/extensions/sandbox/index.ts +2 -3
  253. package/examples/extensions/subagent/agents.ts +2 -3
  254. package/examples/extensions/tool-override.ts +2 -3
  255. package/examples/extensions/with-deps/index.ts +1 -5
  256. package/examples/extensions/with-deps/package-lock.json +2 -2
  257. package/examples/extensions/with-deps/package.json +1 -1
  258. package/package.json +10 -7
@@ -24,7 +24,7 @@ import { calculateContextTokens, collectEntriesForBranchSummary, compact, estima
24
24
  import { DEFAULT_THINKING_LEVEL } from "./defaults.js";
25
25
  import { exportSessionToHtml } from "./export-html/index.js";
26
26
  import { createToolHtmlRenderer } from "./export-html/tool-renderer.js";
27
- import { ExtensionRunner, wrapRegisteredTools, wrapToolsWithExtensions, } from "./extensions/index.js";
27
+ import { ExtensionRunner, wrapRegisteredTools, } from "./extensions/index.js";
28
28
  import { expandPromptTemplate } from "./prompt-templates.js";
29
29
  import { getLatestCompactionEntry } from "./session-manager.js";
30
30
  import { BUILTIN_SLASH_COMMANDS } from "./slash-commands.js";
@@ -63,6 +63,7 @@ export class AgentSession {
63
63
  // Event subscription state
64
64
  _unsubscribeAgent;
65
65
  _eventListeners = [];
66
+ _agentEventQueue = Promise.resolve();
66
67
  /** Tracks pending steering messages for UI display. Removed when delivered. */
67
68
  _steeringMessages = [];
68
69
  /** Tracks pending follow-up messages for UI display. Removed when delivered. */
@@ -72,6 +73,7 @@ export class AgentSession {
72
73
  // Compaction state
73
74
  _compactionAbortController = undefined;
74
75
  _autoCompactionAbortController = undefined;
76
+ _overflowRecoveryAttempted = false;
75
77
  // Branch summarization state
76
78
  _branchSummaryAbortController = undefined;
77
79
  // Retry state
@@ -104,6 +106,8 @@ export class AgentSession {
104
106
  _modelRegistry;
105
107
  // Tool registry for extension getTools/setTools
106
108
  _toolRegistry = new Map();
109
+ _toolPromptSnippets = new Map();
110
+ _toolPromptGuidelines = new Map();
107
111
  // Base system prompt (without extension appends) - used to apply fresh appends each turn
108
112
  _baseSystemPrompt = "";
109
113
  constructor(config) {
@@ -123,6 +127,7 @@ export class AgentSession {
123
127
  // Always subscribe to agent events for internal handling
124
128
  // (session persistence, extensions, auto-compaction, retry logic)
125
129
  this._unsubscribeAgent = this.agent.subscribe(this._handleAgentEvent);
130
+ this._installAgentToolHooks();
126
131
  this._buildRuntime({
127
132
  activeToolNames: this._initialActiveToolNames,
128
133
  includeAllExtensionTools: true,
@@ -132,6 +137,59 @@ export class AgentSession {
132
137
  get modelRegistry() {
133
138
  return this._modelRegistry;
134
139
  }
140
+ /**
141
+ * Install tool hooks once on the Agent instance.
142
+ *
143
+ * The callbacks read `this._extensionRunner` at execution time, so extension reload swaps in the
144
+ * new runner without reinstalling hooks. Extension-specific tool wrappers are still used to adapt
145
+ * registered tool execution to the extension context. Tool call and tool result interception now
146
+ * happens here instead of in wrappers.
147
+ */
148
+ _installAgentToolHooks() {
149
+ this.agent.setBeforeToolCall(async ({ toolCall, args }) => {
150
+ const runner = this._extensionRunner;
151
+ if (!runner?.hasHandlers("tool_call")) {
152
+ return undefined;
153
+ }
154
+ await this._agentEventQueue;
155
+ try {
156
+ return await runner.emitToolCall({
157
+ type: "tool_call",
158
+ toolName: toolCall.name,
159
+ toolCallId: toolCall.id,
160
+ input: args,
161
+ });
162
+ }
163
+ catch (err) {
164
+ if (err instanceof Error) {
165
+ throw err;
166
+ }
167
+ throw new Error(`Extension failed, blocking execution: ${String(err)}`);
168
+ }
169
+ });
170
+ this.agent.setAfterToolCall(async ({ toolCall, args, result, isError }) => {
171
+ const runner = this._extensionRunner;
172
+ if (!runner?.hasHandlers("tool_result")) {
173
+ return undefined;
174
+ }
175
+ const hookResult = await runner.emitToolResult({
176
+ type: "tool_result",
177
+ toolName: toolCall.name,
178
+ toolCallId: toolCall.id,
179
+ input: args,
180
+ content: result.content,
181
+ details: isError ? undefined : result.details,
182
+ isError,
183
+ });
184
+ if (!hookResult || isError) {
185
+ return undefined;
186
+ }
187
+ return {
188
+ content: hookResult.content,
189
+ details: hookResult.details,
190
+ };
191
+ });
192
+ }
135
193
  // =========================================================================
136
194
  // Event Subscription
137
195
  // =========================================================================
@@ -144,10 +202,47 @@ export class AgentSession {
144
202
  // Track last assistant message for auto-compaction check
145
203
  _lastAssistantMessage = undefined;
146
204
  /** Internal handler for agent events - shared by subscribe and reconnect */
147
- _handleAgentEvent = async (event) => {
205
+ _handleAgentEvent = (event) => {
206
+ // Create retry promise synchronously before queueing async processing.
207
+ // Agent.emit() calls this handler synchronously, and prompt() calls waitForRetry()
208
+ // as soon as agent.prompt() resolves. If _retryPromise is created only inside
209
+ // _processAgentEvent, slow earlier queued events can delay agent_end processing
210
+ // and waitForRetry() can miss the in-flight retry.
211
+ this._createRetryPromiseForAgentEnd(event);
212
+ this._agentEventQueue = this._agentEventQueue.then(() => this._processAgentEvent(event), () => this._processAgentEvent(event));
213
+ // Keep queue alive if an event handler fails
214
+ this._agentEventQueue.catch(() => { });
215
+ };
216
+ _createRetryPromiseForAgentEnd(event) {
217
+ if (event.type !== "agent_end" || this._retryPromise) {
218
+ return;
219
+ }
220
+ const settings = this.settingsManager.getRetrySettings();
221
+ if (!settings.enabled) {
222
+ return;
223
+ }
224
+ const lastAssistant = this._findLastAssistantInMessages(event.messages);
225
+ if (!lastAssistant || !this._isRetryableError(lastAssistant)) {
226
+ return;
227
+ }
228
+ this._retryPromise = new Promise((resolve) => {
229
+ this._retryResolve = resolve;
230
+ });
231
+ }
232
+ _findLastAssistantInMessages(messages) {
233
+ for (let i = messages.length - 1; i >= 0; i--) {
234
+ const message = messages[i];
235
+ if (message.role === "assistant") {
236
+ return message;
237
+ }
238
+ }
239
+ return undefined;
240
+ }
241
+ async _processAgentEvent(event) {
148
242
  // When a user message starts, check if it's from either queue and remove it BEFORE emitting
149
243
  // This ensures the UI sees the updated queue state
150
244
  if (event.type === "message_start" && event.message.role === "user") {
245
+ this._overflowRecoveryAttempted = false;
151
246
  const messageText = this._getUserMessageText(event.message);
152
247
  if (messageText) {
153
248
  // Check steering queue first
@@ -185,9 +280,12 @@ export class AgentSession {
185
280
  // Track assistant message for auto-compaction (checked on agent_end)
186
281
  if (event.message.role === "assistant") {
187
282
  this._lastAssistantMessage = event.message;
283
+ const assistantMsg = event.message;
284
+ if (assistantMsg.stopReason !== "error") {
285
+ this._overflowRecoveryAttempted = false;
286
+ }
188
287
  // Reset retry counter immediately on successful assistant response
189
288
  // This prevents accumulation across multiple LLM calls within a turn
190
- const assistantMsg = event.message;
191
289
  if (assistantMsg.stopReason !== "error" && this._retryAttempt > 0) {
192
290
  this._emit({
193
291
  type: "auto_retry_end",
@@ -213,7 +311,7 @@ export class AgentSession {
213
311
  // Queue autonomous follow-up if enabled (after compaction, so it has full context)
214
312
  this._maybeQueueAutonomousFollowUp(msg);
215
313
  }
216
- };
314
+ }
217
315
  /** Resolve the pending retry promise */
218
316
  _resolveRetry() {
219
317
  if (this._retryResolve) {
@@ -432,9 +530,11 @@ export class AgentSession {
432
530
  this._baseSystemPrompt = this._rebuildSystemPrompt(validToolNames);
433
531
  this.agent.setSystemPrompt(this._baseSystemPrompt);
434
532
  }
435
- /** Whether auto-compaction is currently running */
533
+ /** Whether compaction or branch summarization is currently running */
436
534
  get isCompacting() {
437
- return this._autoCompactionAbortController !== undefined || this._compactionAbortController !== undefined;
535
+ return (this._autoCompactionAbortController !== undefined ||
536
+ this._compactionAbortController !== undefined ||
537
+ this._branchSummaryAbortController !== undefined);
438
538
  }
439
539
  /** All messages including custom types like BashExecutionMessage */
440
540
  get messages() {
@@ -472,8 +572,42 @@ export class AgentSession {
472
572
  get promptTemplates() {
473
573
  return this._resourceLoader.getPrompts().prompts;
474
574
  }
575
+ _normalizePromptSnippet(text) {
576
+ if (!text)
577
+ return undefined;
578
+ const oneLine = text
579
+ .replace(/[\r\n]+/g, " ")
580
+ .replace(/\s+/g, " ")
581
+ .trim();
582
+ return oneLine.length > 0 ? oneLine : undefined;
583
+ }
584
+ _normalizePromptGuidelines(guidelines) {
585
+ if (!guidelines || guidelines.length === 0) {
586
+ return [];
587
+ }
588
+ const unique = new Set();
589
+ for (const guideline of guidelines) {
590
+ const normalized = guideline.trim();
591
+ if (normalized.length > 0) {
592
+ unique.add(normalized);
593
+ }
594
+ }
595
+ return Array.from(unique);
596
+ }
475
597
  _rebuildSystemPrompt(toolNames) {
476
- const validToolNames = toolNames.filter((name) => this._baseToolRegistry.has(name));
598
+ const validToolNames = toolNames.filter((name) => this._toolRegistry.has(name));
599
+ const toolSnippets = {};
600
+ const promptGuidelines = [];
601
+ for (const name of validToolNames) {
602
+ const snippet = this._toolPromptSnippets.get(name);
603
+ if (snippet) {
604
+ toolSnippets[name] = snippet;
605
+ }
606
+ const toolGuidelines = this._toolPromptGuidelines.get(name);
607
+ if (toolGuidelines) {
608
+ promptGuidelines.push(...toolGuidelines);
609
+ }
610
+ }
477
611
  const loaderSystemPrompt = this._resourceLoader.getSystemPrompt();
478
612
  const loaderAppendSystemPrompt = this._resourceLoader.getAppendSystemPrompt();
479
613
  const appendSystemPrompt = loaderAppendSystemPrompt.length > 0 ? loaderAppendSystemPrompt.join("\n\n") : undefined;
@@ -486,6 +620,8 @@ export class AgentSession {
486
620
  customPrompt: loaderSystemPrompt,
487
621
  appendSystemPrompt,
488
622
  selectedTools: validToolNames,
623
+ toolSnippets,
624
+ promptGuidelines,
489
625
  });
490
626
  }
491
627
  // =========================================================================
@@ -673,8 +809,9 @@ export class AgentSession {
673
809
  }
674
810
  }
675
811
  /**
676
- * Queue a steering message to interrupt the agent mid-run.
677
- * Delivered after current tool execution, skips remaining tools.
812
+ * Queue a steering message while the agent is running.
813
+ * Delivered after the current assistant turn finishes executing its tool calls,
814
+ * before the next LLM call.
678
815
  * Expands skill commands and prompt templates. Errors on extension commands.
679
816
  * @param images Optional image attachments to include with the message
680
817
  * @throws Error if text is an extension command
@@ -938,11 +1075,12 @@ export class AgentSession {
938
1075
  throw new Error(`No API key for ${model.provider}/${model.id}`);
939
1076
  }
940
1077
  const previousModel = this.model;
1078
+ const thinkingLevel = this._getThinkingLevelForModelSwitch();
941
1079
  this.agent.setModel(model);
942
1080
  this.sessionManager.appendModelChange(model.provider, model.id);
943
1081
  this.settingsManager.setDefaultModelAndProvider(model.provider, model.id);
944
1082
  // Re-clamp thinking level for new model's capabilities
945
- this.setThinkingLevel(this.thinkingLevel);
1083
+ this.setThinkingLevel(thinkingLevel);
946
1084
  await this._emitModelSelect(model, previousModel, "set");
947
1085
  }
948
1086
  /**
@@ -987,12 +1125,16 @@ export class AgentSession {
987
1125
  const len = scopedModels.length;
988
1126
  const nextIndex = direction === "forward" ? (currentIndex + 1) % len : (currentIndex - 1 + len) % len;
989
1127
  const next = scopedModels[nextIndex];
1128
+ const thinkingLevel = this._getThinkingLevelForModelSwitch(next.thinkingLevel);
990
1129
  // Apply model
991
1130
  this.agent.setModel(next.model);
992
1131
  this.sessionManager.appendModelChange(next.model.provider, next.model.id);
993
1132
  this.settingsManager.setDefaultModelAndProvider(next.model.provider, next.model.id);
994
- // Apply thinking level (setThinkingLevel clamps to model capabilities)
995
- this.setThinkingLevel(next.thinkingLevel);
1133
+ // Apply thinking level.
1134
+ // - Explicit scoped model thinking level overrides current session level
1135
+ // - Undefined scoped model thinking level inherits the current session preference
1136
+ // setThinkingLevel clamps to model capabilities.
1137
+ this.setThinkingLevel(thinkingLevel);
996
1138
  await this._emitModelSelect(next.model, currentModel, "cycle");
997
1139
  return { model: next.model, thinkingLevel: this.thinkingLevel, isScoped: true };
998
1140
  }
@@ -1011,11 +1153,12 @@ export class AgentSession {
1011
1153
  if (!apiKey) {
1012
1154
  throw new Error(`No API key for ${nextModel.provider}/${nextModel.id}`);
1013
1155
  }
1156
+ const thinkingLevel = this._getThinkingLevelForModelSwitch();
1014
1157
  this.agent.setModel(nextModel);
1015
1158
  this.sessionManager.appendModelChange(nextModel.provider, nextModel.id);
1016
1159
  this.settingsManager.setDefaultModelAndProvider(nextModel.provider, nextModel.id);
1017
1160
  // Re-clamp thinking level for new model's capabilities
1018
- this.setThinkingLevel(this.thinkingLevel);
1161
+ this.setThinkingLevel(thinkingLevel);
1019
1162
  await this._emitModelSelect(nextModel, currentModel, "cycle");
1020
1163
  return { model: nextModel, thinkingLevel: this.thinkingLevel, isScoped: false };
1021
1164
  }
@@ -1035,7 +1178,9 @@ export class AgentSession {
1035
1178
  this.agent.setThinkingLevel(effectiveLevel);
1036
1179
  if (isChanging) {
1037
1180
  this.sessionManager.appendThinkingLevelChange(effectiveLevel);
1038
- this.settingsManager.setDefaultThinkingLevel(effectiveLevel);
1181
+ if (this.supportsThinking() || effectiveLevel !== "off") {
1182
+ this.settingsManager.setDefaultThinkingLevel(effectiveLevel);
1183
+ }
1039
1184
  }
1040
1185
  }
1041
1186
  /**
@@ -1073,6 +1218,15 @@ export class AgentSession {
1073
1218
  supportsThinking() {
1074
1219
  return !!this.model?.reasoning;
1075
1220
  }
1221
+ _getThinkingLevelForModelSwitch(explicitLevel) {
1222
+ if (explicitLevel !== undefined) {
1223
+ return explicitLevel;
1224
+ }
1225
+ if (!this.supportsThinking()) {
1226
+ return this.settingsManager.getDefaultThinkingLevel() ?? DEFAULT_THINKING_LEVEL;
1227
+ }
1228
+ return this.thinkingLevel;
1229
+ }
1076
1230
  _clampThinkingLevel(level, availableLevels) {
1077
1231
  const ordered = THINKING_LEVELS_WITH_XHIGH;
1078
1232
  const available = new Set(availableLevels);
@@ -1244,15 +1398,27 @@ export class AgentSession {
1244
1398
  // to a larger-context model (e.g. codex) - the overflow error from the old model
1245
1399
  // shouldn't trigger compaction for the new model.
1246
1400
  const sameModel = this.model && assistantMessage.provider === this.model.provider && assistantMessage.model === this.model.id;
1247
- // Skip overflow check if the error is from before a compaction in the current path.
1248
- // This handles the case where an error was kept after compaction (in the "kept" region).
1249
- // The error shouldn't trigger another compaction since we already compacted.
1250
- // Example: opus fails → switch to codex → compact → switch back to opus → opus error
1251
- // is still in context but shouldn't trigger compaction again.
1401
+ // Skip compaction checks if this assistant message is older than the latest
1402
+ // compaction boundary. This prevents a stale pre-compaction usage/error
1403
+ // from retriggering compaction on the first prompt after compaction.
1252
1404
  const compactionEntry = getLatestCompactionEntry(this.sessionManager.getBranch());
1253
- const errorIsFromBeforeCompaction = compactionEntry !== null && assistantMessage.timestamp < new Date(compactionEntry.timestamp).getTime();
1405
+ const assistantIsFromBeforeCompaction = compactionEntry !== null && assistantMessage.timestamp <= new Date(compactionEntry.timestamp).getTime();
1406
+ if (assistantIsFromBeforeCompaction) {
1407
+ return;
1408
+ }
1254
1409
  // Case 1: Overflow - LLM returned context overflow error
1255
- if (sameModel && !errorIsFromBeforeCompaction && isContextOverflow(assistantMessage, contextWindow)) {
1410
+ if (sameModel && isContextOverflow(assistantMessage, contextWindow)) {
1411
+ if (this._overflowRecoveryAttempted) {
1412
+ this._emit({
1413
+ type: "auto_compaction_end",
1414
+ result: undefined,
1415
+ aborted: false,
1416
+ willRetry: false,
1417
+ errorMessage: "Context overflow recovery failed after one compact-and-retry attempt. Try reducing context or switching to a larger-context model.",
1418
+ });
1419
+ return;
1420
+ }
1421
+ this._overflowRecoveryAttempted = true;
1256
1422
  // Remove the error message from agent state (it IS saved to session for history,
1257
1423
  // but we don't want it in context for the retry)
1258
1424
  const messages = this.agent.state.messages;
@@ -1262,11 +1428,29 @@ export class AgentSession {
1262
1428
  await this._runAutoCompaction("overflow", true);
1263
1429
  return;
1264
1430
  }
1265
- // Case 2: Threshold - turn succeeded but context is getting large
1266
- // Skip if this was an error (non-overflow errors don't have usage data)
1267
- if (assistantMessage.stopReason === "error")
1268
- return;
1269
- const contextTokens = calculateContextTokens(assistantMessage.usage);
1431
+ // Case 2: Threshold - context is getting large
1432
+ // For error messages (no usage data), estimate from last successful response.
1433
+ // This ensures sessions that hit persistent API errors (e.g. 529) can still compact.
1434
+ let contextTokens;
1435
+ if (assistantMessage.stopReason === "error") {
1436
+ const messages = this.agent.state.messages;
1437
+ const estimate = estimateContextTokens(messages);
1438
+ if (estimate.lastUsageIndex === null)
1439
+ return; // No usage data at all
1440
+ // Verify the usage source is post-compaction. Kept pre-compaction messages
1441
+ // have stale usage reflecting the old (larger) context and would falsely
1442
+ // trigger compaction right after one just finished.
1443
+ const usageMsg = messages[estimate.lastUsageIndex];
1444
+ if (compactionEntry &&
1445
+ usageMsg.role === "assistant" &&
1446
+ usageMsg.timestamp <= new Date(compactionEntry.timestamp).getTime()) {
1447
+ return;
1448
+ }
1449
+ contextTokens = estimate.tokens;
1450
+ }
1451
+ else {
1452
+ contextTokens = calculateContextTokens(assistantMessage.usage);
1453
+ }
1270
1454
  if (shouldCompact(contextTokens, contextWindow, settings)) {
1271
1455
  await this._runAutoCompaction("threshold", false);
1272
1456
  }
@@ -1467,6 +1651,17 @@ export class AgentSession {
1467
1651
  ? runner.onError(this._extensionErrorListener)
1468
1652
  : undefined;
1469
1653
  }
1654
+ _refreshCurrentModelFromRegistry() {
1655
+ const currentModel = this.model;
1656
+ if (!currentModel) {
1657
+ return;
1658
+ }
1659
+ const refreshedModel = this._modelRegistry.find(currentModel.provider, currentModel.id);
1660
+ if (!refreshedModel || refreshedModel === currentModel) {
1661
+ return;
1662
+ }
1663
+ this.agent.setModel(refreshedModel);
1664
+ }
1470
1665
  _bindExtensionCore(runner) {
1471
1666
  const normalizeLocation = (source) => {
1472
1667
  if (source === "user" || source === "project" || source === "path") {
@@ -1535,6 +1730,7 @@ export class AgentSession {
1535
1730
  getActiveTools: () => this.getActiveToolNames(),
1536
1731
  getAllTools: () => this.getAllTools(),
1537
1732
  setActiveTools: (toolNames) => this.setActiveToolsByName(toolNames),
1733
+ refreshTools: () => this._refreshToolRegistry(),
1538
1734
  getCommands,
1539
1735
  setModel: async (model) => {
1540
1736
  const key = await this.modelRegistry.getApiKey(model);
@@ -1567,8 +1763,62 @@ export class AgentSession {
1567
1763
  })();
1568
1764
  },
1569
1765
  getSystemPrompt: () => this.systemPrompt,
1766
+ }, {
1767
+ registerProvider: (name, config) => {
1768
+ this._modelRegistry.registerProvider(name, config);
1769
+ this._refreshCurrentModelFromRegistry();
1770
+ },
1771
+ unregisterProvider: (name) => {
1772
+ this._modelRegistry.unregisterProvider(name);
1773
+ this._refreshCurrentModelFromRegistry();
1774
+ },
1570
1775
  });
1571
1776
  }
1777
+ _refreshToolRegistry(options) {
1778
+ const previousRegistryNames = new Set(this._toolRegistry.keys());
1779
+ const previousActiveToolNames = this.getActiveToolNames();
1780
+ const registeredTools = this._extensionRunner?.getAllRegisteredTools() ?? [];
1781
+ const allCustomTools = [
1782
+ ...registeredTools,
1783
+ ...this._customTools.map((def) => ({ definition: def, extensionPath: "<sdk>" })),
1784
+ ];
1785
+ this._toolPromptSnippets = new Map(allCustomTools
1786
+ .map((registeredTool) => {
1787
+ const snippet = this._normalizePromptSnippet(registeredTool.definition.promptSnippet);
1788
+ return snippet ? [registeredTool.definition.name, snippet] : undefined;
1789
+ })
1790
+ .filter((entry) => entry !== undefined));
1791
+ this._toolPromptGuidelines = new Map(allCustomTools
1792
+ .map((registeredTool) => {
1793
+ const guidelines = this._normalizePromptGuidelines(registeredTool.definition.promptGuidelines);
1794
+ return guidelines.length > 0 ? [registeredTool.definition.name, guidelines] : undefined;
1795
+ })
1796
+ .filter((entry) => entry !== undefined));
1797
+ const wrappedExtensionTools = this._extensionRunner
1798
+ ? wrapRegisteredTools(allCustomTools, this._extensionRunner)
1799
+ : [];
1800
+ const toolRegistry = new Map(this._baseToolRegistry);
1801
+ for (const tool of wrappedExtensionTools) {
1802
+ toolRegistry.set(tool.name, tool);
1803
+ }
1804
+ this._toolRegistry = toolRegistry;
1805
+ const nextActiveToolNames = options?.activeToolNames
1806
+ ? [...options.activeToolNames]
1807
+ : [...previousActiveToolNames];
1808
+ if (options?.includeAllExtensionTools) {
1809
+ for (const tool of wrappedExtensionTools) {
1810
+ nextActiveToolNames.push(tool.name);
1811
+ }
1812
+ }
1813
+ else if (!options?.activeToolNames) {
1814
+ for (const toolName of this._toolRegistry.keys()) {
1815
+ if (!previousRegistryNames.has(toolName)) {
1816
+ nextActiveToolNames.push(toolName);
1817
+ }
1818
+ }
1819
+ }
1820
+ this.setActiveToolsByName([...new Set(nextActiveToolNames)]);
1821
+ }
1572
1822
  _buildRuntime(options) {
1573
1823
  const autoResizeImages = this.settingsManager.getImageAutoResize();
1574
1824
  const shellCommandPrefix = this.settingsManager.getShellCommandPrefix();
@@ -1598,47 +1848,14 @@ export class AgentSession {
1598
1848
  this._bindExtensionCore(this._extensionRunner);
1599
1849
  this._applyExtensionBindings(this._extensionRunner);
1600
1850
  }
1601
- const registeredTools = this._extensionRunner?.getAllRegisteredTools() ?? [];
1602
- const allCustomTools = [
1603
- ...registeredTools,
1604
- ...this._customTools.map((def) => ({ definition: def, extensionPath: "<sdk>" })),
1605
- ];
1606
- const wrappedExtensionTools = this._extensionRunner
1607
- ? wrapRegisteredTools(allCustomTools, this._extensionRunner)
1608
- : [];
1609
- const toolRegistry = new Map(this._baseToolRegistry);
1610
- for (const tool of wrappedExtensionTools) {
1611
- toolRegistry.set(tool.name, tool);
1612
- }
1613
1851
  const defaultActiveToolNames = this._baseToolsOverride
1614
1852
  ? Object.keys(this._baseToolsOverride)
1615
1853
  : ["read", "bash", "edit", "write"];
1616
1854
  const baseActiveToolNames = options.activeToolNames ?? defaultActiveToolNames;
1617
- const activeToolNameSet = new Set(baseActiveToolNames);
1618
- if (options.includeAllExtensionTools) {
1619
- for (const tool of wrappedExtensionTools) {
1620
- activeToolNameSet.add(tool.name);
1621
- }
1622
- }
1623
- const extensionToolNames = new Set(wrappedExtensionTools.map((tool) => tool.name));
1624
- const activeBaseTools = Array.from(activeToolNameSet)
1625
- .filter((name) => this._baseToolRegistry.has(name) && !extensionToolNames.has(name))
1626
- .map((name) => this._baseToolRegistry.get(name));
1627
- const activeExtensionTools = wrappedExtensionTools.filter((tool) => activeToolNameSet.has(tool.name));
1628
- const activeToolsArray = [...activeBaseTools, ...activeExtensionTools];
1629
- if (this._extensionRunner) {
1630
- const wrappedActiveTools = wrapToolsWithExtensions(activeToolsArray, this._extensionRunner);
1631
- this.agent.setTools(wrappedActiveTools);
1632
- const wrappedAllTools = wrapToolsWithExtensions(Array.from(toolRegistry.values()), this._extensionRunner);
1633
- this._toolRegistry = new Map(wrappedAllTools.map((tool) => [tool.name, tool]));
1634
- }
1635
- else {
1636
- this.agent.setTools(activeToolsArray);
1637
- this._toolRegistry = toolRegistry;
1638
- }
1639
- const systemPromptToolNames = Array.from(activeToolNameSet).filter((name) => this._baseToolRegistry.has(name));
1640
- this._baseSystemPrompt = this._rebuildSystemPrompt(systemPromptToolNames);
1641
- this.agent.setSystemPrompt(this._baseSystemPrompt);
1855
+ this._refreshToolRegistry({
1856
+ activeToolNames: baseActiveToolNames,
1857
+ includeAllExtensionTools: options.includeAllExtensionTools,
1858
+ });
1642
1859
  }
1643
1860
  async reload() {
1644
1861
  const previousFlagValues = this._extensionRunner?.getFlagValues();
@@ -1675,8 +1892,8 @@ export class AgentSession {
1675
1892
  if (isContextOverflow(message, contextWindow))
1676
1893
  return false;
1677
1894
  const err = message.errorMessage;
1678
- // Match: overloaded_error, rate limit, 429, 500, 502, 503, 504, service unavailable, connection errors, fetch failed, terminated, retry delay exceeded
1679
- return /overloaded|rate.?limit|too many requests|429|500|502|503|504|service.?unavailable|server error|internal error|connection.?error|connection.?refused|other side closed|fetch failed|upstream.?connect|reset before headers|terminated|retry delay/i.test(err);
1895
+ // Match: overloaded_error, provider returned error, rate limit, 429, 500, 502, 503, 504, service unavailable, network/connection errors, fetch failed, terminated, retry delay exceeded
1896
+ 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/i.test(err);
1680
1897
  }
1681
1898
  /**
1682
1899
  * Check if autonomous mode is enabled and queue follow-up prompts.
@@ -1722,15 +1939,18 @@ export class AgentSession {
1722
1939
  */
1723
1940
  async _handleRetryableError(message) {
1724
1941
  const settings = this.settingsManager.getRetrySettings();
1725
- if (!settings.enabled)
1942
+ if (!settings.enabled) {
1943
+ this._resolveRetry();
1726
1944
  return false;
1727
- this._retryAttempt++;
1728
- // Create retry promise on first attempt so waitForRetry() can await it
1729
- if (this._retryAttempt === 1 && !this._retryPromise) {
1945
+ }
1946
+ // Retry promise is created synchronously in _handleAgentEvent for agent_end.
1947
+ // Keep a defensive fallback here in case a future refactor bypasses that path.
1948
+ if (!this._retryPromise) {
1730
1949
  this._retryPromise = new Promise((resolve) => {
1731
1950
  this._retryResolve = resolve;
1732
1951
  });
1733
1952
  }
1953
+ this._retryAttempt++;
1734
1954
  if (this._retryAttempt > settings.maxRetries) {
1735
1955
  // Max retries exceeded, emit final failure and reset
1736
1956
  this._emit({