@trenchwork/coder 1.3.0

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 (560) hide show
  1. package/LICENSE +16 -0
  2. package/README.md +173 -0
  3. package/agents/trenchwork-code.rules.json +199 -0
  4. package/dist/bin/deepseek.d.ts +3 -0
  5. package/dist/bin/deepseek.d.ts.map +1 -0
  6. package/dist/bin/deepseek.js +23 -0
  7. package/dist/bin/deepseek.js.map +1 -0
  8. package/dist/capabilities/baseCapability.d.ts +72 -0
  9. package/dist/capabilities/baseCapability.d.ts.map +1 -0
  10. package/dist/capabilities/baseCapability.js +183 -0
  11. package/dist/capabilities/baseCapability.js.map +1 -0
  12. package/dist/capabilities/bashCapability.d.ts +13 -0
  13. package/dist/capabilities/bashCapability.d.ts.map +1 -0
  14. package/dist/capabilities/bashCapability.js +24 -0
  15. package/dist/capabilities/bashCapability.js.map +1 -0
  16. package/dist/capabilities/editCapability.d.ts +17 -0
  17. package/dist/capabilities/editCapability.d.ts.map +1 -0
  18. package/dist/capabilities/editCapability.js +27 -0
  19. package/dist/capabilities/editCapability.js.map +1 -0
  20. package/dist/capabilities/enhancedGitCapability.d.ts +7 -0
  21. package/dist/capabilities/enhancedGitCapability.d.ts.map +1 -0
  22. package/dist/capabilities/enhancedGitCapability.js +220 -0
  23. package/dist/capabilities/enhancedGitCapability.js.map +1 -0
  24. package/dist/capabilities/filesystemCapability.d.ts +13 -0
  25. package/dist/capabilities/filesystemCapability.d.ts.map +1 -0
  26. package/dist/capabilities/filesystemCapability.js +24 -0
  27. package/dist/capabilities/filesystemCapability.js.map +1 -0
  28. package/dist/capabilities/gitHistoryCapability.d.ts +6 -0
  29. package/dist/capabilities/gitHistoryCapability.d.ts.map +1 -0
  30. package/dist/capabilities/gitHistoryCapability.js +184 -0
  31. package/dist/capabilities/gitHistoryCapability.js.map +1 -0
  32. package/dist/capabilities/hitlCapability.d.ts +18 -0
  33. package/dist/capabilities/hitlCapability.d.ts.map +1 -0
  34. package/dist/capabilities/hitlCapability.js +29 -0
  35. package/dist/capabilities/hitlCapability.js.map +1 -0
  36. package/dist/capabilities/index.d.ts +11 -0
  37. package/dist/capabilities/index.d.ts.map +1 -0
  38. package/dist/capabilities/index.js +16 -0
  39. package/dist/capabilities/index.js.map +1 -0
  40. package/dist/capabilities/memoryCapability.d.ts +10 -0
  41. package/dist/capabilities/memoryCapability.d.ts.map +1 -0
  42. package/dist/capabilities/memoryCapability.js +22 -0
  43. package/dist/capabilities/memoryCapability.js.map +1 -0
  44. package/dist/capabilities/notebookCapability.d.ts +6 -0
  45. package/dist/capabilities/notebookCapability.d.ts.map +1 -0
  46. package/dist/capabilities/notebookCapability.js +17 -0
  47. package/dist/capabilities/notebookCapability.js.map +1 -0
  48. package/dist/capabilities/searchCapability.d.ts +19 -0
  49. package/dist/capabilities/searchCapability.d.ts.map +1 -0
  50. package/dist/capabilities/searchCapability.js +29 -0
  51. package/dist/capabilities/searchCapability.js.map +1 -0
  52. package/dist/capabilities/skillCapability.d.ts +6 -0
  53. package/dist/capabilities/skillCapability.d.ts.map +1 -0
  54. package/dist/capabilities/skillCapability.js +17 -0
  55. package/dist/capabilities/skillCapability.js.map +1 -0
  56. package/dist/capabilities/todoCapability.d.ts +11 -0
  57. package/dist/capabilities/todoCapability.d.ts.map +1 -0
  58. package/dist/capabilities/todoCapability.js +22 -0
  59. package/dist/capabilities/todoCapability.js.map +1 -0
  60. package/dist/capabilities/toolManifest.d.ts +3 -0
  61. package/dist/capabilities/toolManifest.d.ts.map +1 -0
  62. package/dist/capabilities/toolManifest.js +163 -0
  63. package/dist/capabilities/toolManifest.js.map +1 -0
  64. package/dist/capabilities/toolRegistry.d.ts +25 -0
  65. package/dist/capabilities/toolRegistry.d.ts.map +1 -0
  66. package/dist/capabilities/toolRegistry.js +150 -0
  67. package/dist/capabilities/toolRegistry.js.map +1 -0
  68. package/dist/capabilities/unifiedCodingCapability.d.ts +62 -0
  69. package/dist/capabilities/unifiedCodingCapability.d.ts.map +1 -0
  70. package/dist/capabilities/unifiedCodingCapability.js +790 -0
  71. package/dist/capabilities/unifiedCodingCapability.js.map +1 -0
  72. package/dist/capabilities/webCapability.d.ts +23 -0
  73. package/dist/capabilities/webCapability.d.ts.map +1 -0
  74. package/dist/capabilities/webCapability.js +33 -0
  75. package/dist/capabilities/webCapability.js.map +1 -0
  76. package/dist/config.d.ts +25 -0
  77. package/dist/config.d.ts.map +1 -0
  78. package/dist/config.js +181 -0
  79. package/dist/config.js.map +1 -0
  80. package/dist/contracts/agent-profiles.schema.json +25 -0
  81. package/dist/contracts/agent-schemas.json +158 -0
  82. package/dist/contracts/models.schema.json +9 -0
  83. package/dist/contracts/module-schema.json +367 -0
  84. package/dist/contracts/schemas/agent-profile.schema.json +157 -0
  85. package/dist/contracts/schemas/agent-rules.schema.json +238 -0
  86. package/dist/contracts/schemas/agent-schemas.schema.json +528 -0
  87. package/dist/contracts/schemas/agent.schema.json +90 -0
  88. package/dist/contracts/schemas/tool-selection.schema.json +174 -0
  89. package/dist/contracts/tools.schema.json +42 -0
  90. package/dist/contracts/unified-schema.json +40 -0
  91. package/dist/contracts/v1/agent.d.ts +225 -0
  92. package/dist/contracts/v1/agent.d.ts.map +1 -0
  93. package/dist/contracts/v1/agent.js +8 -0
  94. package/dist/contracts/v1/agent.js.map +1 -0
  95. package/dist/contracts/v1/agentProfileManifest.d.ts +60 -0
  96. package/dist/contracts/v1/agentProfileManifest.d.ts.map +1 -0
  97. package/dist/contracts/v1/agentProfileManifest.js +9 -0
  98. package/dist/contracts/v1/agentProfileManifest.js.map +1 -0
  99. package/dist/contracts/v1/agentRules.d.ts +60 -0
  100. package/dist/contracts/v1/agentRules.d.ts.map +1 -0
  101. package/dist/contracts/v1/agentRules.js +10 -0
  102. package/dist/contracts/v1/agentRules.js.map +1 -0
  103. package/dist/contracts/v1/provider.d.ts +149 -0
  104. package/dist/contracts/v1/provider.d.ts.map +1 -0
  105. package/dist/contracts/v1/provider.js +7 -0
  106. package/dist/contracts/v1/provider.js.map +1 -0
  107. package/dist/contracts/v1/tool.d.ts +136 -0
  108. package/dist/contracts/v1/tool.d.ts.map +1 -0
  109. package/dist/contracts/v1/tool.js +7 -0
  110. package/dist/contracts/v1/tool.js.map +1 -0
  111. package/dist/contracts/v1/toolAccess.d.ts +43 -0
  112. package/dist/contracts/v1/toolAccess.d.ts.map +1 -0
  113. package/dist/contracts/v1/toolAccess.js +9 -0
  114. package/dist/contracts/v1/toolAccess.js.map +1 -0
  115. package/dist/core/adversarial.d.ts +38 -0
  116. package/dist/core/adversarial.d.ts.map +1 -0
  117. package/dist/core/adversarial.js +106 -0
  118. package/dist/core/adversarial.js.map +1 -0
  119. package/dist/core/adversarialCorrection.d.ts +22 -0
  120. package/dist/core/adversarialCorrection.d.ts.map +1 -0
  121. package/dist/core/adversarialCorrection.js +25 -0
  122. package/dist/core/adversarialCorrection.js.map +1 -0
  123. package/dist/core/agent.d.ts +331 -0
  124. package/dist/core/agent.d.ts.map +1 -0
  125. package/dist/core/agent.js +1637 -0
  126. package/dist/core/agent.js.map +1 -0
  127. package/dist/core/agentProfileManifest.d.ts +3 -0
  128. package/dist/core/agentProfileManifest.d.ts.map +1 -0
  129. package/dist/core/agentProfileManifest.js +188 -0
  130. package/dist/core/agentProfileManifest.js.map +1 -0
  131. package/dist/core/agentProfiles.d.ts +22 -0
  132. package/dist/core/agentProfiles.d.ts.map +1 -0
  133. package/dist/core/agentProfiles.js +35 -0
  134. package/dist/core/agentProfiles.js.map +1 -0
  135. package/dist/core/agentRegistry.d.ts +111 -0
  136. package/dist/core/agentRegistry.d.ts.map +1 -0
  137. package/dist/core/agentRegistry.js +229 -0
  138. package/dist/core/agentRegistry.js.map +1 -0
  139. package/dist/core/agentRulebook.d.ts +11 -0
  140. package/dist/core/agentRulebook.d.ts.map +1 -0
  141. package/dist/core/agentRulebook.js +136 -0
  142. package/dist/core/agentRulebook.js.map +1 -0
  143. package/dist/core/agentSchemaLoader.d.ts +131 -0
  144. package/dist/core/agentSchemaLoader.d.ts.map +1 -0
  145. package/dist/core/agentSchemaLoader.js +235 -0
  146. package/dist/core/agentSchemaLoader.js.map +1 -0
  147. package/dist/core/aiErrorFixer.d.ts +57 -0
  148. package/dist/core/aiErrorFixer.d.ts.map +1 -0
  149. package/dist/core/aiErrorFixer.js +214 -0
  150. package/dist/core/aiErrorFixer.js.map +1 -0
  151. package/dist/core/bashCommandGuidance.d.ts +16 -0
  152. package/dist/core/bashCommandGuidance.d.ts.map +1 -0
  153. package/dist/core/bashCommandGuidance.js +40 -0
  154. package/dist/core/bashCommandGuidance.js.map +1 -0
  155. package/dist/core/compactionNote.d.ts +13 -0
  156. package/dist/core/compactionNote.d.ts.map +1 -0
  157. package/dist/core/compactionNote.js +13 -0
  158. package/dist/core/compactionNote.js.map +1 -0
  159. package/dist/core/constants.d.ts +31 -0
  160. package/dist/core/constants.d.ts.map +1 -0
  161. package/dist/core/constants.js +62 -0
  162. package/dist/core/constants.js.map +1 -0
  163. package/dist/core/contextManager.d.ts +271 -0
  164. package/dist/core/contextManager.d.ts.map +1 -0
  165. package/dist/core/contextManager.js +1076 -0
  166. package/dist/core/contextManager.js.map +1 -0
  167. package/dist/core/contextUsage.d.ts +28 -0
  168. package/dist/core/contextUsage.d.ts.map +1 -0
  169. package/dist/core/contextUsage.js +62 -0
  170. package/dist/core/contextUsage.js.map +1 -0
  171. package/dist/core/contextWindow.d.ts +42 -0
  172. package/dist/core/contextWindow.d.ts.map +1 -0
  173. package/dist/core/contextWindow.js +127 -0
  174. package/dist/core/contextWindow.js.map +1 -0
  175. package/dist/core/customCommands.d.ts +19 -0
  176. package/dist/core/customCommands.d.ts.map +1 -0
  177. package/dist/core/customCommands.js +85 -0
  178. package/dist/core/customCommands.js.map +1 -0
  179. package/dist/core/diffPanel.d.ts +30 -0
  180. package/dist/core/diffPanel.d.ts.map +1 -0
  181. package/dist/core/diffPanel.js +48 -0
  182. package/dist/core/diffPanel.js.map +1 -0
  183. package/dist/core/errorClassification.d.ts +44 -0
  184. package/dist/core/errorClassification.d.ts.map +1 -0
  185. package/dist/core/errorClassification.js +333 -0
  186. package/dist/core/errorClassification.js.map +1 -0
  187. package/dist/core/errors/apiKeyErrors.d.ts +11 -0
  188. package/dist/core/errors/apiKeyErrors.d.ts.map +1 -0
  189. package/dist/core/errors/apiKeyErrors.js +159 -0
  190. package/dist/core/errors/apiKeyErrors.js.map +1 -0
  191. package/dist/core/errors/errorTypes.d.ts +111 -0
  192. package/dist/core/errors/errorTypes.d.ts.map +1 -0
  193. package/dist/core/errors/errorTypes.js +345 -0
  194. package/dist/core/errors/errorTypes.js.map +1 -0
  195. package/dist/core/errors/index.d.ts +50 -0
  196. package/dist/core/errors/index.d.ts.map +1 -0
  197. package/dist/core/errors/index.js +156 -0
  198. package/dist/core/errors/index.js.map +1 -0
  199. package/dist/core/errors/networkErrors.d.ts +14 -0
  200. package/dist/core/errors/networkErrors.d.ts.map +1 -0
  201. package/dist/core/errors/networkErrors.js +53 -0
  202. package/dist/core/errors/networkErrors.js.map +1 -0
  203. package/dist/core/errors/safetyValidator.d.ts +109 -0
  204. package/dist/core/errors/safetyValidator.d.ts.map +1 -0
  205. package/dist/core/errors/safetyValidator.js +272 -0
  206. package/dist/core/errors/safetyValidator.js.map +1 -0
  207. package/dist/core/errors.d.ts +4 -0
  208. package/dist/core/errors.d.ts.map +1 -0
  209. package/dist/core/errors.js +33 -0
  210. package/dist/core/errors.js.map +1 -0
  211. package/dist/core/failureRegistry.d.ts +30 -0
  212. package/dist/core/failureRegistry.d.ts.map +1 -0
  213. package/dist/core/failureRegistry.js +74 -0
  214. package/dist/core/failureRegistry.js.map +1 -0
  215. package/dist/core/fileMentions.d.ts +40 -0
  216. package/dist/core/fileMentions.d.ts.map +1 -0
  217. package/dist/core/fileMentions.js +136 -0
  218. package/dist/core/fileMentions.js.map +1 -0
  219. package/dist/core/finalResponseFormatter.d.ts +10 -0
  220. package/dist/core/finalResponseFormatter.d.ts.map +1 -0
  221. package/dist/core/finalResponseFormatter.js +14 -0
  222. package/dist/core/finalResponseFormatter.js.map +1 -0
  223. package/dist/core/guardrails.d.ts +146 -0
  224. package/dist/core/guardrails.d.ts.map +1 -0
  225. package/dist/core/guardrails.js +361 -0
  226. package/dist/core/guardrails.js.map +1 -0
  227. package/dist/core/hitl.d.ts +119 -0
  228. package/dist/core/hitl.d.ts.map +1 -0
  229. package/dist/core/hitl.js +396 -0
  230. package/dist/core/hitl.js.map +1 -0
  231. package/dist/core/hooks.d.ts +95 -0
  232. package/dist/core/hooks.d.ts.map +1 -0
  233. package/dist/core/hooks.js +236 -0
  234. package/dist/core/hooks.js.map +1 -0
  235. package/dist/core/hostedAuth.d.ts +88 -0
  236. package/dist/core/hostedAuth.d.ts.map +1 -0
  237. package/dist/core/hostedAuth.js +219 -0
  238. package/dist/core/hostedAuth.js.map +1 -0
  239. package/dist/core/inputProtection.d.ts +122 -0
  240. package/dist/core/inputProtection.d.ts.map +1 -0
  241. package/dist/core/inputProtection.js +422 -0
  242. package/dist/core/inputProtection.js.map +1 -0
  243. package/dist/core/modelDiscovery.d.ts +102 -0
  244. package/dist/core/modelDiscovery.d.ts.map +1 -0
  245. package/dist/core/modelDiscovery.js +416 -0
  246. package/dist/core/modelDiscovery.js.map +1 -0
  247. package/dist/core/multilinePasteHandler.d.ts +35 -0
  248. package/dist/core/multilinePasteHandler.d.ts.map +1 -0
  249. package/dist/core/multilinePasteHandler.js +81 -0
  250. package/dist/core/multilinePasteHandler.js.map +1 -0
  251. package/dist/core/permissionMode.d.ts +40 -0
  252. package/dist/core/permissionMode.d.ts.map +1 -0
  253. package/dist/core/permissionMode.js +86 -0
  254. package/dist/core/permissionMode.js.map +1 -0
  255. package/dist/core/postWriteDiagnostics.d.ts +32 -0
  256. package/dist/core/postWriteDiagnostics.d.ts.map +1 -0
  257. package/dist/core/postWriteDiagnostics.js +127 -0
  258. package/dist/core/postWriteDiagnostics.js.map +1 -0
  259. package/dist/core/preferences.d.ts +66 -0
  260. package/dist/core/preferences.d.ts.map +1 -0
  261. package/dist/core/preferences.js +310 -0
  262. package/dist/core/preferences.js.map +1 -0
  263. package/dist/core/quota.d.ts +61 -0
  264. package/dist/core/quota.d.ts.map +1 -0
  265. package/dist/core/quota.js +104 -0
  266. package/dist/core/quota.js.map +1 -0
  267. package/dist/core/quotaErrors.d.ts +42 -0
  268. package/dist/core/quotaErrors.d.ts.map +1 -0
  269. package/dist/core/quotaErrors.js +86 -0
  270. package/dist/core/quotaErrors.js.map +1 -0
  271. package/dist/core/refusalDetection.d.ts +2 -0
  272. package/dist/core/refusalDetection.d.ts.map +1 -0
  273. package/dist/core/refusalDetection.js +51 -0
  274. package/dist/core/refusalDetection.js.map +1 -0
  275. package/dist/core/relativeTime.d.ts +8 -0
  276. package/dist/core/relativeTime.d.ts.map +1 -0
  277. package/dist/core/relativeTime.js +29 -0
  278. package/dist/core/relativeTime.js.map +1 -0
  279. package/dist/core/resultVerification.d.ts +48 -0
  280. package/dist/core/resultVerification.d.ts.map +1 -0
  281. package/dist/core/resultVerification.js +127 -0
  282. package/dist/core/resultVerification.js.map +1 -0
  283. package/dist/core/rewind.d.ts +14 -0
  284. package/dist/core/rewind.d.ts.map +1 -0
  285. package/dist/core/rewind.js +25 -0
  286. package/dist/core/rewind.js.map +1 -0
  287. package/dist/core/schemaValidator.d.ts +49 -0
  288. package/dist/core/schemaValidator.d.ts.map +1 -0
  289. package/dist/core/schemaValidator.js +234 -0
  290. package/dist/core/schemaValidator.js.map +1 -0
  291. package/dist/core/secretStore.d.ts +59 -0
  292. package/dist/core/secretStore.d.ts.map +1 -0
  293. package/dist/core/secretStore.js +278 -0
  294. package/dist/core/secretStore.js.map +1 -0
  295. package/dist/core/sessionStorage.d.ts +10 -0
  296. package/dist/core/sessionStorage.d.ts.map +1 -0
  297. package/dist/core/sessionStorage.js +46 -0
  298. package/dist/core/sessionStorage.js.map +1 -0
  299. package/dist/core/sessionStore.d.ts +35 -0
  300. package/dist/core/sessionStore.d.ts.map +1 -0
  301. package/dist/core/sessionStore.js +190 -0
  302. package/dist/core/sessionStore.js.map +1 -0
  303. package/dist/core/shutdown.d.ts +34 -0
  304. package/dist/core/shutdown.d.ts.map +1 -0
  305. package/dist/core/shutdown.js +186 -0
  306. package/dist/core/shutdown.js.map +1 -0
  307. package/dist/core/slashCommands.d.ts +38 -0
  308. package/dist/core/slashCommands.d.ts.map +1 -0
  309. package/dist/core/slashCommands.js +72 -0
  310. package/dist/core/slashCommands.js.map +1 -0
  311. package/dist/core/subAgentNote.d.ts +15 -0
  312. package/dist/core/subAgentNote.d.ts.map +1 -0
  313. package/dist/core/subAgentNote.js +16 -0
  314. package/dist/core/subAgentNote.js.map +1 -0
  315. package/dist/core/sudoPasswordManager.d.ts +52 -0
  316. package/dist/core/sudoPasswordManager.d.ts.map +1 -0
  317. package/dist/core/sudoPasswordManager.js +115 -0
  318. package/dist/core/sudoPasswordManager.js.map +1 -0
  319. package/dist/core/taskCompletionDetector.d.ts +117 -0
  320. package/dist/core/taskCompletionDetector.d.ts.map +1 -0
  321. package/dist/core/taskCompletionDetector.js +532 -0
  322. package/dist/core/taskCompletionDetector.js.map +1 -0
  323. package/dist/core/testFailureMonitor.d.ts +67 -0
  324. package/dist/core/testFailureMonitor.d.ts.map +1 -0
  325. package/dist/core/testFailureMonitor.js +262 -0
  326. package/dist/core/testFailureMonitor.js.map +1 -0
  327. package/dist/core/thinkingVerbs.d.ts +31 -0
  328. package/dist/core/thinkingVerbs.d.ts.map +1 -0
  329. package/dist/core/thinkingVerbs.js +58 -0
  330. package/dist/core/thinkingVerbs.js.map +1 -0
  331. package/dist/core/toolPreconditions.d.ts +34 -0
  332. package/dist/core/toolPreconditions.d.ts.map +1 -0
  333. package/dist/core/toolPreconditions.js +242 -0
  334. package/dist/core/toolPreconditions.js.map +1 -0
  335. package/dist/core/toolRuntime.d.ts +193 -0
  336. package/dist/core/toolRuntime.d.ts.map +1 -0
  337. package/dist/core/toolRuntime.js +526 -0
  338. package/dist/core/toolRuntime.js.map +1 -0
  339. package/dist/core/turnGovernor.d.ts +63 -0
  340. package/dist/core/turnGovernor.d.ts.map +1 -0
  341. package/dist/core/turnGovernor.js +94 -0
  342. package/dist/core/turnGovernor.js.map +1 -0
  343. package/dist/core/types/utilityTypes.d.ts +183 -0
  344. package/dist/core/types/utilityTypes.d.ts.map +1 -0
  345. package/dist/core/types/utilityTypes.js +273 -0
  346. package/dist/core/types/utilityTypes.js.map +1 -0
  347. package/dist/core/types.d.ts +334 -0
  348. package/dist/core/types.d.ts.map +1 -0
  349. package/dist/core/types.js +76 -0
  350. package/dist/core/types.js.map +1 -0
  351. package/dist/core/updateChecker.d.ts +148 -0
  352. package/dist/core/updateChecker.d.ts.map +1 -0
  353. package/dist/core/updateChecker.js +605 -0
  354. package/dist/core/updateChecker.js.map +1 -0
  355. package/dist/core/usage.d.ts +28 -0
  356. package/dist/core/usage.d.ts.map +1 -0
  357. package/dist/core/usage.js +77 -0
  358. package/dist/core/usage.js.map +1 -0
  359. package/dist/headless/interactiveShell.d.ts +47 -0
  360. package/dist/headless/interactiveShell.d.ts.map +1 -0
  361. package/dist/headless/interactiveShell.js +2495 -0
  362. package/dist/headless/interactiveShell.js.map +1 -0
  363. package/dist/leanAgent.d.ts +73 -0
  364. package/dist/leanAgent.d.ts.map +1 -0
  365. package/dist/leanAgent.js +177 -0
  366. package/dist/leanAgent.js.map +1 -0
  367. package/dist/plugins/providers/deepseek/index.d.ts +12 -0
  368. package/dist/plugins/providers/deepseek/index.d.ts.map +1 -0
  369. package/dist/plugins/providers/deepseek/index.js +123 -0
  370. package/dist/plugins/providers/deepseek/index.js.map +1 -0
  371. package/dist/plugins/providers/index.d.ts +2 -0
  372. package/dist/plugins/providers/index.d.ts.map +1 -0
  373. package/dist/plugins/providers/index.js +10 -0
  374. package/dist/plugins/providers/index.js.map +1 -0
  375. package/dist/providers/baseProvider.d.ts +140 -0
  376. package/dist/providers/baseProvider.d.ts.map +1 -0
  377. package/dist/providers/baseProvider.js +230 -0
  378. package/dist/providers/baseProvider.js.map +1 -0
  379. package/dist/providers/openaiChatCompletionsProvider.d.ts +70 -0
  380. package/dist/providers/openaiChatCompletionsProvider.d.ts.map +1 -0
  381. package/dist/providers/openaiChatCompletionsProvider.js +971 -0
  382. package/dist/providers/openaiChatCompletionsProvider.js.map +1 -0
  383. package/dist/providers/providerFactory.d.ts +22 -0
  384. package/dist/providers/providerFactory.d.ts.map +1 -0
  385. package/dist/providers/providerFactory.js +25 -0
  386. package/dist/providers/providerFactory.js.map +1 -0
  387. package/dist/providers/resilientProvider.d.ts +96 -0
  388. package/dist/providers/resilientProvider.d.ts.map +1 -0
  389. package/dist/providers/resilientProvider.js +251 -0
  390. package/dist/providers/resilientProvider.js.map +1 -0
  391. package/dist/runtime/agentController.d.ts +137 -0
  392. package/dist/runtime/agentController.d.ts.map +1 -0
  393. package/dist/runtime/agentController.js +784 -0
  394. package/dist/runtime/agentController.js.map +1 -0
  395. package/dist/runtime/agentHost.d.ts +61 -0
  396. package/dist/runtime/agentHost.d.ts.map +1 -0
  397. package/dist/runtime/agentHost.js +158 -0
  398. package/dist/runtime/agentHost.js.map +1 -0
  399. package/dist/runtime/agentSession.d.ts +49 -0
  400. package/dist/runtime/agentSession.d.ts.map +1 -0
  401. package/dist/runtime/agentSession.js +218 -0
  402. package/dist/runtime/agentSession.js.map +1 -0
  403. package/dist/runtime/agentSpawningWiring.d.ts +32 -0
  404. package/dist/runtime/agentSpawningWiring.d.ts.map +1 -0
  405. package/dist/runtime/agentSpawningWiring.js +114 -0
  406. package/dist/runtime/agentSpawningWiring.js.map +1 -0
  407. package/dist/runtime/node.d.ts +7 -0
  408. package/dist/runtime/node.d.ts.map +1 -0
  409. package/dist/runtime/node.js +50 -0
  410. package/dist/runtime/node.js.map +1 -0
  411. package/dist/runtime/universal.d.ts +18 -0
  412. package/dist/runtime/universal.d.ts.map +1 -0
  413. package/dist/runtime/universal.js +21 -0
  414. package/dist/runtime/universal.js.map +1 -0
  415. package/dist/shell/liveStatus.d.ts +27 -0
  416. package/dist/shell/liveStatus.d.ts.map +1 -0
  417. package/dist/shell/liveStatus.js +53 -0
  418. package/dist/shell/liveStatus.js.map +1 -0
  419. package/dist/shell/systemPrompt.d.ts +12 -0
  420. package/dist/shell/systemPrompt.d.ts.map +1 -0
  421. package/dist/shell/systemPrompt.js +16 -0
  422. package/dist/shell/systemPrompt.js.map +1 -0
  423. package/dist/shell/toolPresentation.d.ts +54 -0
  424. package/dist/shell/toolPresentation.d.ts.map +1 -0
  425. package/dist/shell/toolPresentation.js +334 -0
  426. package/dist/shell/toolPresentation.js.map +1 -0
  427. package/dist/tools/bashTools.d.ts +11 -0
  428. package/dist/tools/bashTools.d.ts.map +1 -0
  429. package/dist/tools/bashTools.js +785 -0
  430. package/dist/tools/bashTools.js.map +1 -0
  431. package/dist/tools/diffUtils.d.ts +43 -0
  432. package/dist/tools/diffUtils.d.ts.map +1 -0
  433. package/dist/tools/diffUtils.js +607 -0
  434. package/dist/tools/diffUtils.js.map +1 -0
  435. package/dist/tools/editTools.d.ts +29 -0
  436. package/dist/tools/editTools.d.ts.map +1 -0
  437. package/dist/tools/editTools.js +792 -0
  438. package/dist/tools/editTools.js.map +1 -0
  439. package/dist/tools/fileChangeTracker.d.ts +47 -0
  440. package/dist/tools/fileChangeTracker.d.ts.map +1 -0
  441. package/dist/tools/fileChangeTracker.js +154 -0
  442. package/dist/tools/fileChangeTracker.js.map +1 -0
  443. package/dist/tools/fileReadTracker.d.ts +69 -0
  444. package/dist/tools/fileReadTracker.d.ts.map +1 -0
  445. package/dist/tools/fileReadTracker.js +213 -0
  446. package/dist/tools/fileReadTracker.js.map +1 -0
  447. package/dist/tools/fileTools.d.ts +3 -0
  448. package/dist/tools/fileTools.d.ts.map +1 -0
  449. package/dist/tools/fileTools.js +389 -0
  450. package/dist/tools/fileTools.js.map +1 -0
  451. package/dist/tools/grepTools.d.ts +3 -0
  452. package/dist/tools/grepTools.d.ts.map +1 -0
  453. package/dist/tools/grepTools.js +137 -0
  454. package/dist/tools/grepTools.js.map +1 -0
  455. package/dist/tools/hitlTools.d.ts +7 -0
  456. package/dist/tools/hitlTools.d.ts.map +1 -0
  457. package/dist/tools/hitlTools.js +185 -0
  458. package/dist/tools/hitlTools.js.map +1 -0
  459. package/dist/tools/memoryTools.d.ts +27 -0
  460. package/dist/tools/memoryTools.d.ts.map +1 -0
  461. package/dist/tools/memoryTools.js +197 -0
  462. package/dist/tools/memoryTools.js.map +1 -0
  463. package/dist/tools/notebookTools.d.ts +20 -0
  464. package/dist/tools/notebookTools.d.ts.map +1 -0
  465. package/dist/tools/notebookTools.js +140 -0
  466. package/dist/tools/notebookTools.js.map +1 -0
  467. package/dist/tools/searchTools.d.ts +12 -0
  468. package/dist/tools/searchTools.d.ts.map +1 -0
  469. package/dist/tools/searchTools.js +414 -0
  470. package/dist/tools/searchTools.js.map +1 -0
  471. package/dist/tools/skillTools.d.ts +24 -0
  472. package/dist/tools/skillTools.d.ts.map +1 -0
  473. package/dist/tools/skillTools.js +140 -0
  474. package/dist/tools/skillTools.js.map +1 -0
  475. package/dist/tools/todoTools.d.ts +23 -0
  476. package/dist/tools/todoTools.d.ts.map +1 -0
  477. package/dist/tools/todoTools.js +120 -0
  478. package/dist/tools/todoTools.js.map +1 -0
  479. package/dist/tools/webTools.d.ts +26 -0
  480. package/dist/tools/webTools.d.ts.map +1 -0
  481. package/dist/tools/webTools.js +467 -0
  482. package/dist/tools/webTools.js.map +1 -0
  483. package/dist/ui/ink/App.d.ts +53 -0
  484. package/dist/ui/ink/App.d.ts.map +1 -0
  485. package/dist/ui/ink/App.js +13 -0
  486. package/dist/ui/ink/App.js.map +1 -0
  487. package/dist/ui/ink/ChatStatic.d.ts +30 -0
  488. package/dist/ui/ink/ChatStatic.d.ts.map +1 -0
  489. package/dist/ui/ink/ChatStatic.js +83 -0
  490. package/dist/ui/ink/ChatStatic.js.map +1 -0
  491. package/dist/ui/ink/InkPromptController.d.ts +321 -0
  492. package/dist/ui/ink/InkPromptController.d.ts.map +1 -0
  493. package/dist/ui/ink/InkPromptController.js +667 -0
  494. package/dist/ui/ink/InkPromptController.js.map +1 -0
  495. package/dist/ui/ink/Menu.d.ts +21 -0
  496. package/dist/ui/ink/Menu.d.ts.map +1 -0
  497. package/dist/ui/ink/Menu.js +61 -0
  498. package/dist/ui/ink/Menu.js.map +1 -0
  499. package/dist/ui/ink/Prompt.d.ts +47 -0
  500. package/dist/ui/ink/Prompt.d.ts.map +1 -0
  501. package/dist/ui/ink/Prompt.js +571 -0
  502. package/dist/ui/ink/Prompt.js.map +1 -0
  503. package/dist/ui/ink/StatusLine.d.ts +35 -0
  504. package/dist/ui/ink/StatusLine.d.ts.map +1 -0
  505. package/dist/ui/ink/StatusLine.js +66 -0
  506. package/dist/ui/ink/StatusLine.js.map +1 -0
  507. package/dist/ui/ink/pasteBuffer.d.ts +44 -0
  508. package/dist/ui/ink/pasteBuffer.d.ts.map +1 -0
  509. package/dist/ui/ink/pasteBuffer.js +73 -0
  510. package/dist/ui/ink/pasteBuffer.js.map +1 -0
  511. package/dist/ui/theme.d.ts +351 -0
  512. package/dist/ui/theme.d.ts.map +1 -0
  513. package/dist/ui/theme.js +435 -0
  514. package/dist/ui/theme.js.map +1 -0
  515. package/dist/utils/analytics.d.ts +2 -0
  516. package/dist/utils/analytics.d.ts.map +1 -0
  517. package/dist/utils/analytics.js +51 -0
  518. package/dist/utils/analytics.js.map +1 -0
  519. package/dist/utils/asyncUtils.d.ts +95 -0
  520. package/dist/utils/asyncUtils.d.ts.map +1 -0
  521. package/dist/utils/asyncUtils.js +286 -0
  522. package/dist/utils/asyncUtils.js.map +1 -0
  523. package/dist/utils/debugLogger.d.ts +6 -0
  524. package/dist/utils/debugLogger.d.ts.map +1 -0
  525. package/dist/utils/debugLogger.js +39 -0
  526. package/dist/utils/debugLogger.js.map +1 -0
  527. package/dist/utils/errorUtils.d.ts +12 -0
  528. package/dist/utils/errorUtils.d.ts.map +1 -0
  529. package/dist/utils/errorUtils.js +83 -0
  530. package/dist/utils/errorUtils.js.map +1 -0
  531. package/dist/utils/frontmatter.d.ts +10 -0
  532. package/dist/utils/frontmatter.d.ts.map +1 -0
  533. package/dist/utils/frontmatter.js +78 -0
  534. package/dist/utils/frontmatter.js.map +1 -0
  535. package/dist/utils/packageInfo.d.ts +14 -0
  536. package/dist/utils/packageInfo.d.ts.map +1 -0
  537. package/dist/utils/packageInfo.js +45 -0
  538. package/dist/utils/packageInfo.js.map +1 -0
  539. package/dist/utils/planFormatter.d.ts +34 -0
  540. package/dist/utils/planFormatter.d.ts.map +1 -0
  541. package/dist/utils/planFormatter.js +141 -0
  542. package/dist/utils/planFormatter.js.map +1 -0
  543. package/dist/utils/securityUtils.d.ts +132 -0
  544. package/dist/utils/securityUtils.d.ts.map +1 -0
  545. package/dist/utils/securityUtils.js +324 -0
  546. package/dist/utils/securityUtils.js.map +1 -0
  547. package/dist/utils/statusReporter.d.ts +6 -0
  548. package/dist/utils/statusReporter.d.ts.map +1 -0
  549. package/dist/utils/statusReporter.js +26 -0
  550. package/dist/utils/statusReporter.js.map +1 -0
  551. package/dist/workspace.d.ts +8 -0
  552. package/dist/workspace.d.ts.map +1 -0
  553. package/dist/workspace.js +135 -0
  554. package/dist/workspace.js.map +1 -0
  555. package/dist/workspace.validator.d.ts +49 -0
  556. package/dist/workspace.validator.d.ts.map +1 -0
  557. package/dist/workspace.validator.js +215 -0
  558. package/dist/workspace.validator.js.map +1 -0
  559. package/package.json +116 -0
  560. package/scripts/postinstall.cjs +56 -0
@@ -0,0 +1,1637 @@
1
+ import path from 'node:path';
2
+ import { isMultilinePaste, processPaste } from './multilinePasteHandler.js';
3
+ import { safeErrorMessage } from './secretStore.js';
4
+ import { logDebug, debugSnippet } from '../utils/debugLogger.js';
5
+ import { ensureNextSteps } from './finalResponseFormatter.js';
6
+ import { isAdversarialEnabled, reviewDraft } from './adversarial.js';
7
+ // Canonical error classification (audit Rank 5, Phase 2). The agent's transient
8
+ // semantics is distinct (rate-limit + 5xx folded into transient), so it imports
9
+ // isAgentTransientError under its historical local name; logic moved verbatim.
10
+ import { isContextOverflowError, isAgentTransientError as isTransientError } from './errorClassification.js';
11
+ /**
12
+ * Maximum number of context overflow recovery attempts
13
+ */
14
+ const MAX_CONTEXT_RECOVERY_ATTEMPTS = 3;
15
+ // Streaming runs without timeouts - we let the model take as long as it needs
16
+ /**
17
+ * Maximum number of transient error retries
18
+ */
19
+ const MAX_TRANSIENT_RETRIES = 3;
20
+ /**
21
+ * Delay before retry (in ms), with exponential backoff
22
+ */
23
+ function getRetryDelay(attempt) {
24
+ // Base delay of 1 second, doubles each attempt: 1s, 2s, 4s
25
+ return Math.min(1000 * Math.pow(2, attempt - 1), 10000);
26
+ }
27
+ /**
28
+ * Sleep for the specified milliseconds
29
+ */
30
+ function sleep(ms) {
31
+ return new Promise(resolve => setTimeout(resolve, ms));
32
+ }
33
+ export class AgentRuntime {
34
+ messages = [];
35
+ provider;
36
+ toolRuntime;
37
+ callbacks;
38
+ contextManager;
39
+ activeRun = null;
40
+ baseSystemPrompt;
41
+ providerId;
42
+ modelId;
43
+ workingDirectory;
44
+ explainEdits;
45
+ cancellationRequested = false;
46
+ // Loop detection: track last tool calls to detect stuck loops
47
+ lastToolCallSignature = null;
48
+ repeatedToolCallCount = 0;
49
+ static MAX_REPEATED_TOOL_CALLS = 5; // Stop on 5th identical call (4 allowed)
50
+ // Session-level context recovery tracking to prevent endless recovery loops
51
+ totalContextRecoveries = 0;
52
+ static MAX_TOTAL_RECOVERIES = 5; // Max recoveries across entire session
53
+ // Behavioral loop detection: track recent tool calls to catch repetitive patterns
54
+ // e.g., calling "execute_bash" with "git status" 5 times even if output differs slightly
55
+ recentToolCalls = [];
56
+ static TOOL_HISTORY_SIZE = 12;
57
+ static BEHAVIORAL_LOOP_THRESHOLD = 3; // Same tool+cmd 3+ times in last 12 = stuck
58
+ // Consecutive failure detection: track failed commands to prevent stuck loops
59
+ consecutiveFailures = 0;
60
+ static MAX_CONSECUTIVE_FAILURES = 3; // Stop after 3 consecutive failed commands
61
+ static EDIT_CONTEXT_CHAR_LIMIT = 4000;
62
+ // Never cache stateful tools - they must always execute to reflect current system state
63
+ static NON_CACHEABLE_TOOL_NAMES = new Set([
64
+ 'bash',
65
+ 'execute_bash',
66
+ 'execute_command',
67
+ 'run_command',
68
+ 'edit',
69
+ 'edit_file',
70
+ 'write',
71
+ 'write_file',
72
+ 'notebookedit',
73
+ 'read',
74
+ 'read_file',
75
+ 'read_files',
76
+ 'list_files',
77
+ 'list_dir',
78
+ 'glob',
79
+ 'grep',
80
+ 'search',
81
+ 'search_text',
82
+ 'git_status',
83
+ 'git_diff',
84
+ 'git_log',
85
+ 'git_commit',
86
+ 'git_push',
87
+ ]);
88
+ // Skip loop short-circuiting for direct execution tools to avoid blocking user commands
89
+ static LOOP_EXEMPT_TOOL_NAMES = new Set([
90
+ 'bash',
91
+ 'execute_bash',
92
+ 'execute_command',
93
+ 'run_command',
94
+ 'edit',
95
+ 'edit_file',
96
+ 'write',
97
+ 'write_file',
98
+ 'notebookedit',
99
+ // Read/search tools are noise-prone and often repeated legitimately
100
+ 'read',
101
+ 'read_file',
102
+ 'read_files',
103
+ 'list_files',
104
+ 'list_dir',
105
+ 'glob',
106
+ 'glob_search',
107
+ 'grep',
108
+ 'search',
109
+ ]);
110
+ // Tool result cache: prevent duplicate identical tool calls by returning cached results
111
+ // Key: tool signature (name + JSON args), Value: result string
112
+ toolResultCache = new Map();
113
+ static TOOL_CACHE_MAX_SIZE = 50; // Keep last 50 tool results
114
+ // Track tool history position per send() call for accurate progress detection
115
+ toolHistoryCursor = 0;
116
+ // Cached model info from provider API (real context window limits)
117
+ modelInfo = null;
118
+ modelInfoFetched = false;
119
+ constructor(options) {
120
+ this.provider = options.provider;
121
+ this.toolRuntime = options.toolRuntime;
122
+ this.callbacks = options.callbacks ?? {};
123
+ this.contextManager = options.contextManager ?? null;
124
+ this.providerId = options.providerId ?? 'unknown';
125
+ this.modelId = options.modelId ?? 'unknown';
126
+ this.workingDirectory = options.workingDirectory ?? process.cwd();
127
+ this.explainEdits = options.explainEdits ?? false;
128
+ const trimmedPrompt = options.systemPrompt.trim();
129
+ this.baseSystemPrompt = trimmedPrompt || null;
130
+ if (trimmedPrompt) {
131
+ this.messages.push({ role: 'system', content: trimmedPrompt });
132
+ }
133
+ }
134
+ /**
135
+ * Request cancellation of the current operation.
136
+ * The agent will stop at the next safe point (after current tool completes).
137
+ */
138
+ requestCancellation() {
139
+ this.cancellationRequested = true;
140
+ }
141
+ /**
142
+ * Check if cancellation has been requested.
143
+ */
144
+ isCancellationRequested() {
145
+ return this.cancellationRequested;
146
+ }
147
+ /**
148
+ * Check if the agent is currently processing a request.
149
+ */
150
+ isRunning() {
151
+ return this.activeRun !== null;
152
+ }
153
+ /**
154
+ * Check if any of the tool calls are edit operations (Edit, Write)
155
+ */
156
+ isEditToolCall(toolName) {
157
+ const name = toolName.toLowerCase();
158
+ return name === 'edit' || name === 'edit_file' || name === 'write' || name === 'write_file';
159
+ }
160
+ /**
161
+ * Extract a display-friendly file path from a tool call (prefers workspace-relative path)
162
+ */
163
+ getEditedFilePath(call) {
164
+ const args = call.arguments;
165
+ const rawPath = typeof args['file_path'] === 'string'
166
+ ? args['file_path']
167
+ : typeof args['path'] === 'string'
168
+ ? args['path']
169
+ : null;
170
+ if (!rawPath) {
171
+ return null;
172
+ }
173
+ const relativePath = path.relative(this.workingDirectory, rawPath);
174
+ if (relativePath && !relativePath.startsWith('..') && relativePath !== '') {
175
+ return relativePath;
176
+ }
177
+ return rawPath;
178
+ }
179
+ /**
180
+ * Get the file paths from edit tool calls for the explanation prompt
181
+ */
182
+ getEditedFiles(toolCalls) {
183
+ const files = [];
184
+ for (const call of toolCalls) {
185
+ if (this.isEditToolCall(call.name)) {
186
+ const filePath = this.getEditedFilePath(call);
187
+ if (filePath) {
188
+ files.push(filePath);
189
+ }
190
+ }
191
+ }
192
+ return files;
193
+ }
194
+ async send(text, useStreaming = false) {
195
+ const prompt = text.trim();
196
+ if (!prompt) {
197
+ return '';
198
+ }
199
+ // Notify UI immediately so it can reflect activity without waiting for generation
200
+ this.callbacks.onRequestReceived?.(prompt.slice(0, 400));
201
+ // Reset cancellation flag and loop tracking at start of new request
202
+ this.cancellationRequested = false;
203
+ this.resetBehavioralLoopTracking();
204
+ // Track tool history position for this run
205
+ this.toolHistoryCursor = this.toolRuntime.getToolHistory().length;
206
+ // Handle multi-line paste: show actual content to user, send full content to AI
207
+ if (isMultilinePaste(prompt)) {
208
+ const processed = processPaste(prompt);
209
+ // Notify UI about the paste with the actual content, not just summary
210
+ this.callbacks.onMultilinePaste?.(processed.fullContent, processed.metadata);
211
+ // Send the full content to the AI
212
+ this.messages.push({ role: 'user', content: processed.fullContent });
213
+ }
214
+ else {
215
+ // Single-line or short text: send as-is
216
+ this.messages.push({ role: 'user', content: prompt });
217
+ }
218
+ const run = { startedAt: Date.now() };
219
+ this.activeRun = run;
220
+ try {
221
+ // Always use streaming when available - no fallback
222
+ if (useStreaming && this.provider.generateStream) {
223
+ return await this.processConversationStreaming();
224
+ }
225
+ return await this.processConversation();
226
+ }
227
+ finally {
228
+ if (this.activeRun === run) {
229
+ this.activeRun = null;
230
+ }
231
+ // Reset cancellation flag when done
232
+ this.cancellationRequested = false;
233
+ }
234
+ }
235
+ async processConversation() {
236
+ let contextRecoveryAttempts = 0;
237
+ let transientRetryAttempts = 0;
238
+ // eslint-disable-next-line no-constant-condition
239
+ while (true) {
240
+ // Check for cancellation at start of each iteration
241
+ if (this.cancellationRequested) {
242
+ this.callbacks.onCancelled?.();
243
+ return '[Operation cancelled by user]';
244
+ }
245
+ // Prune messages if approaching context limit (BEFORE generation)
246
+ await this.pruneMessagesIfNeeded();
247
+ try {
248
+ const response = await this.provider.generate(this.messages, this.providerTools);
249
+ const usage = response.usage ?? null;
250
+ const contextStats = this.getContextStats();
251
+ // Reset recovery attempts on successful generation
252
+ contextRecoveryAttempts = 0;
253
+ if (response.type === 'tool_calls') {
254
+ // BEHAVIORAL LOOP DETECTION: Check if model is stuck calling same tool repeatedly
255
+ const behavioralLoopResult = this.checkBehavioralLoop(response.toolCalls);
256
+ if (behavioralLoopResult) {
257
+ this.emitAssistantMessage(behavioralLoopResult, { isFinal: true, usage, contextStats });
258
+ this.messages.push({ role: 'assistant', content: behavioralLoopResult });
259
+ return behavioralLoopResult;
260
+ }
261
+ // Loop detection: check if same tool calls are being repeated (exact signature match)
262
+ const signatureCalls = response.toolCalls.filter(call => !this.shouldSkipLoopDetection(call));
263
+ const toolSignature = signatureCalls.length
264
+ ? signatureCalls
265
+ .map((t) => `${t.name}:${JSON.stringify(t.arguments)}`)
266
+ .sort()
267
+ .join('|')
268
+ : null;
269
+ if (toolSignature && toolSignature === this.lastToolCallSignature) {
270
+ this.repeatedToolCallCount++;
271
+ if (this.repeatedToolCallCount >= AgentRuntime.MAX_REPEATED_TOOL_CALLS) {
272
+ // Break out of loop - model is stuck
273
+ const loopMsg = `Tool loop detected: same tools called ${this.repeatedToolCallCount} times. Please try a different approach or provide more specific instructions.`;
274
+ this.emitAssistantMessage(loopMsg, { isFinal: true, usage, contextStats });
275
+ this.messages.push({ role: 'assistant', content: loopMsg });
276
+ this.lastToolCallSignature = null;
277
+ this.repeatedToolCallCount = 0;
278
+ return loopMsg;
279
+ }
280
+ }
281
+ else if (toolSignature) {
282
+ this.lastToolCallSignature = toolSignature;
283
+ this.repeatedToolCallCount = 1;
284
+ }
285
+ else {
286
+ this.lastToolCallSignature = null;
287
+ this.repeatedToolCallCount = 0;
288
+ }
289
+ // Emit narration if present - it shows the AI's thought process before tools
290
+ const narration = response.content?.trim();
291
+ if (narration) {
292
+ this.emitAssistantMessage(narration, {
293
+ isFinal: false,
294
+ usage,
295
+ contextStats,
296
+ });
297
+ }
298
+ this.maybeAckToolCalls(response.toolCalls, Boolean(narration?.length), usage, contextStats);
299
+ const assistantMessage = {
300
+ role: 'assistant',
301
+ content: response.content ?? '',
302
+ };
303
+ if (response.toolCalls?.length) {
304
+ assistantMessage.toolCalls = response.toolCalls;
305
+ }
306
+ this.messages.push(assistantMessage);
307
+ await this.resolveToolCalls(response.toolCalls);
308
+ continue;
309
+ }
310
+ const reply = response.content?.trim() ?? '';
311
+ const { output: ensuredReply } = ensureNextSteps(reply);
312
+ let finalReply = ensuredReply;
313
+ // Reset loop detection when we get a text response (not just tool calls)
314
+ if (finalReply.length >= 10) {
315
+ this.lastToolCallSignature = null;
316
+ this.repeatedToolCallCount = 0;
317
+ }
318
+ finalReply = await this.maybeAdversarialReview(finalReply);
319
+ if (finalReply) {
320
+ this.emitAssistantMessage(finalReply, { isFinal: true, usage, contextStats });
321
+ }
322
+ this.messages.push({ role: 'assistant', content: finalReply });
323
+ // Trigger verification for final responses with verifiable claims
324
+ this.triggerVerificationIfNeeded(finalReply);
325
+ return finalReply;
326
+ }
327
+ catch (error) {
328
+ // Auto-recover from context overflow errors (with session-level limit)
329
+ const canRecover = contextRecoveryAttempts < MAX_CONTEXT_RECOVERY_ATTEMPTS &&
330
+ this.totalContextRecoveries < AgentRuntime.MAX_TOTAL_RECOVERIES;
331
+ if (isContextOverflowError(error) && canRecover) {
332
+ contextRecoveryAttempts++;
333
+ this.totalContextRecoveries++;
334
+ const recovered = await this.recoverFromContextOverflow(contextRecoveryAttempts);
335
+ if (recovered) {
336
+ // Notify UI that we're continuing after recovery
337
+ this.callbacks.onContinueAfterRecovery?.();
338
+ // Retry the generation with reduced context
339
+ continue;
340
+ }
341
+ }
342
+ // Auto-retry transient errors (network issues, rate limits, server errors)
343
+ if (isTransientError(error) && transientRetryAttempts < MAX_TRANSIENT_RETRIES) {
344
+ transientRetryAttempts++;
345
+ const delayMs = getRetryDelay(transientRetryAttempts);
346
+ this.callbacks.onRetrying?.(transientRetryAttempts, MAX_TRANSIENT_RETRIES, error);
347
+ await sleep(delayMs);
348
+ continue;
349
+ }
350
+ // Re-throw if not recoverable or recovery failed
351
+ throw error;
352
+ }
353
+ }
354
+ }
355
+ async processConversationStreaming() {
356
+ if (!this.provider.generateStream) {
357
+ return this.processConversation();
358
+ }
359
+ let contextRecoveryAttempts = 0;
360
+ let transientRetryAttempts = 0;
361
+ const STREAM_HARD_CHAR_LIMIT = 120000; // Hard guardrail to prevent runaway provider output
362
+ let totalCharsReceived = 0;
363
+ let truncatedResponse = false;
364
+ // eslint-disable-next-line no-constant-condition
365
+ while (true) {
366
+ // Check for cancellation at start of each iteration
367
+ if (this.cancellationRequested) {
368
+ this.callbacks.onCancelled?.();
369
+ return '[Operation cancelled by user]';
370
+ }
371
+ // Prune messages if approaching context limit (BEFORE generation)
372
+ await this.pruneMessagesIfNeeded();
373
+ try {
374
+ let fullContent = '';
375
+ let reasoningContent = '';
376
+ const toolCalls = [];
377
+ let usage = null;
378
+ const suppressStreamNarration = this.shouldSuppressToolNarration();
379
+ let bufferedContent = '';
380
+ const stream = this.provider.generateStream(this.messages, this.providerTools);
381
+ const iterator = stream[Symbol.asyncIterator]();
382
+ let streamClosed = false;
383
+ const closeStream = async () => {
384
+ if (streamClosed) {
385
+ return;
386
+ }
387
+ streamClosed = true;
388
+ if (typeof iterator.return === 'function') {
389
+ try {
390
+ await iterator.return();
391
+ }
392
+ catch (closeError) {
393
+ logDebug(`[agent] Failed to close stream cleanly: ${safeErrorMessage(closeError)}`);
394
+ }
395
+ }
396
+ };
397
+ const describeChunk = (chunk) => {
398
+ if (!chunk) {
399
+ return 'unknown chunk';
400
+ }
401
+ switch (chunk.type) {
402
+ case 'content':
403
+ case 'reasoning': {
404
+ const snippet = debugSnippet(chunk.content);
405
+ return snippet ? `${chunk.type} → ${snippet}` : chunk.type;
406
+ }
407
+ case 'tool_call':
408
+ return chunk.toolCall ? `tool_call ${chunk.toolCall.name}` : 'tool_call';
409
+ case 'usage':
410
+ if (chunk.usage?.totalTokens != null) {
411
+ return `usage tokens=${chunk.usage.totalTokens}`;
412
+ }
413
+ return 'usage';
414
+ case 'done':
415
+ return 'done';
416
+ default:
417
+ return chunk.type;
418
+ }
419
+ };
420
+ // Simple streaming loop - no timeouts, let the stream run until done
421
+ try {
422
+ let chunkCount = 0;
423
+ // eslint-disable-next-line no-constant-condition
424
+ while (true) {
425
+ const result = await iterator.next();
426
+ chunkCount++;
427
+ // Only log significant chunks (tool calls, done), not every content/reasoning token
428
+ if (result.done || result.value?.type === 'tool_call') {
429
+ const chunkLabel = result.done ? 'done' : describeChunk(result.value);
430
+ logDebug(`[agent] chunk ${chunkCount}: ${chunkLabel}`);
431
+ }
432
+ // Check for cancellation during streaming
433
+ if (this.cancellationRequested) {
434
+ await closeStream();
435
+ this.callbacks.onCancelled?.();
436
+ const partial = (fullContent || reasoningContent).trim();
437
+ if (partial) {
438
+ this.messages.push({ role: 'assistant', content: `${partial}\n\n[Cancelled by user]` });
439
+ }
440
+ return '[Operation cancelled by user]';
441
+ }
442
+ if (result.done) {
443
+ break;
444
+ }
445
+ const chunk = result.value;
446
+ if (chunk.type === 'reasoning' && chunk.content) {
447
+ // Buffer reasoning content - don't stream token-by-token
448
+ // It will be emitted as a complete block when ready
449
+ const next = reasoningContent + chunk.content;
450
+ totalCharsReceived += chunk.content.length;
451
+ // Hard cap buffered reasoning to protect memory
452
+ if (next.length > 24000) {
453
+ reasoningContent = next.slice(-24000);
454
+ }
455
+ else {
456
+ reasoningContent = next;
457
+ }
458
+ if (totalCharsReceived > STREAM_HARD_CHAR_LIMIT) {
459
+ truncatedResponse = true;
460
+ await closeStream();
461
+ break;
462
+ }
463
+ continue;
464
+ }
465
+ if (chunk.type === 'content' && chunk.content) {
466
+ const nextContent = fullContent + chunk.content;
467
+ totalCharsReceived += chunk.content.length;
468
+ // Cap buffered content to avoid OOM from runaway outputs
469
+ fullContent = nextContent.length > 48000 ? nextContent.slice(-48000) : nextContent;
470
+ if (suppressStreamNarration) {
471
+ const nextBuffered = bufferedContent + chunk.content;
472
+ bufferedContent = nextBuffered.length > 24000 ? nextBuffered.slice(-24000) : nextBuffered;
473
+ }
474
+ else {
475
+ this.callbacks.onStreamChunk?.(chunk.content, 'content');
476
+ }
477
+ if (totalCharsReceived > STREAM_HARD_CHAR_LIMIT) {
478
+ truncatedResponse = true;
479
+ await closeStream();
480
+ break;
481
+ }
482
+ }
483
+ else if (chunk.type === 'tool_call' && chunk.toolCall) {
484
+ // On first tool call, flush any buffered content
485
+ if (toolCalls.length === 0) {
486
+ // Emit complete reasoning block first
487
+ if (reasoningContent.trim()) {
488
+ this.callbacks.onStreamChunk?.(reasoningContent, 'reasoning');
489
+ }
490
+ // Then emit buffered narration content
491
+ if (suppressStreamNarration && bufferedContent) {
492
+ this.callbacks.onStreamChunk?.(bufferedContent, 'content');
493
+ bufferedContent = '';
494
+ }
495
+ }
496
+ toolCalls.push(chunk.toolCall);
497
+ }
498
+ else if (chunk.type === 'usage' && chunk.usage) {
499
+ usage = chunk.usage;
500
+ // Emit real token usage during streaming
501
+ this.callbacks.onUsage?.(chunk.usage);
502
+ }
503
+ }
504
+ }
505
+ finally {
506
+ await closeStream();
507
+ }
508
+ // Reset recovery attempts on successful generation
509
+ contextRecoveryAttempts = 0;
510
+ const contextStats = this.getContextStats();
511
+ // IMPORTANT: Only use fullContent for user-visible output
512
+ // reasoningContent is internal model thinking and should NEVER be shown to users
513
+ // We keep it for conversation history (helps the model) but not for display
514
+ const combinedContent = fullContent;
515
+ if (truncatedResponse) {
516
+ const notice = '\n\n[Response truncated: reached safety limit of 120k characters to prevent OOM.]';
517
+ const updated = combinedContent ? `${combinedContent}${notice}` : notice.trim();
518
+ fullContent = updated;
519
+ reasoningContent = '';
520
+ // Partial tool calls are unsafe when truncated; drop them
521
+ toolCalls.length = 0;
522
+ // Emit the notice via streaming so it reaches the UI — message.complete is suppressed
523
+ // when the buffer is already populated (wasStreamed=true path in interactiveShell.ts)
524
+ this.callbacks.onStreamChunk?.(notice, 'content');
525
+ }
526
+ // If no tool calls were issued, emit reasoning and buffered content as complete blocks
527
+ if (toolCalls.length === 0) {
528
+ // Emit complete reasoning block if we have one
529
+ if (reasoningContent.trim()) {
530
+ this.callbacks.onStreamChunk?.(reasoningContent, 'reasoning');
531
+ }
532
+ // Emit buffered narration content
533
+ if (suppressStreamNarration && bufferedContent) {
534
+ this.callbacks.onStreamChunk?.(bufferedContent, 'content');
535
+ bufferedContent = '';
536
+ }
537
+ }
538
+ // Check if we got tool calls
539
+ if (toolCalls.length > 0) {
540
+ // BEHAVIORAL LOOP DETECTION: Check if model is stuck calling same tool repeatedly
541
+ // This catches patterns like "git status" called 5 times even with slightly different outputs
542
+ const behavioralLoopResult = this.checkBehavioralLoop(toolCalls);
543
+ if (behavioralLoopResult) {
544
+ this.emitAssistantMessage(behavioralLoopResult, { isFinal: true, usage, contextStats, wasStreamed: true });
545
+ this.messages.push({ role: 'assistant', content: behavioralLoopResult });
546
+ return behavioralLoopResult;
547
+ }
548
+ // Loop detection: check if same tool calls are being repeated (exact signature match)
549
+ const signatureCalls = toolCalls.filter(call => !this.shouldSkipLoopDetection(call));
550
+ const toolSignature = signatureCalls.length
551
+ ? signatureCalls
552
+ .map((t) => `${t.name}:${JSON.stringify(t.arguments)}`)
553
+ .sort()
554
+ .join('|')
555
+ : null;
556
+ if (toolSignature && toolSignature === this.lastToolCallSignature) {
557
+ this.repeatedToolCallCount++;
558
+ if (this.repeatedToolCallCount >= AgentRuntime.MAX_REPEATED_TOOL_CALLS) {
559
+ // Break out of loop - model is stuck
560
+ const loopMsg = `Tool loop detected: same tools called ${this.repeatedToolCallCount} times. Please try a different approach or provide more specific instructions.`;
561
+ this.emitAssistantMessage(loopMsg, { isFinal: true, usage, contextStats, wasStreamed: true });
562
+ this.messages.push({ role: 'assistant', content: loopMsg });
563
+ this.lastToolCallSignature = null;
564
+ this.repeatedToolCallCount = 0;
565
+ return loopMsg;
566
+ }
567
+ }
568
+ else if (toolSignature) {
569
+ this.lastToolCallSignature = toolSignature;
570
+ this.repeatedToolCallCount = 1;
571
+ }
572
+ else {
573
+ this.lastToolCallSignature = null;
574
+ this.repeatedToolCallCount = 0;
575
+ }
576
+ // Content was already streamed via onStreamChunk, just record it for context
577
+ // (wasStreamed=true prevents duplicate display)
578
+ // Note: Acknowledgement injection happens during streaming (when first tool_call chunk arrives)
579
+ const narration = combinedContent.trim();
580
+ if (narration) {
581
+ this.emitAssistantMessage(narration, {
582
+ isFinal: false,
583
+ usage,
584
+ contextStats,
585
+ wasStreamed: true,
586
+ });
587
+ }
588
+ this.maybeAckToolCalls(toolCalls, Boolean(narration.length), usage, contextStats);
589
+ const assistantMessage = {
590
+ role: 'assistant',
591
+ content: combinedContent,
592
+ toolCalls,
593
+ };
594
+ this.messages.push(assistantMessage);
595
+ await this.resolveToolCalls(toolCalls);
596
+ continue;
597
+ }
598
+ let reply = combinedContent.trim();
599
+ // For reasoning models: if no content but we have reasoning, use reasoning as the response
600
+ // This handles models like deepseek-v4-pro that put their entire response in reasoning_content
601
+ // The reasoning has already been streamed as 'thought' events showing the AI's thinking
602
+ if (!reply && reasoningContent.trim()) {
603
+ // Use reasoning as the reply - it contains the model's answer
604
+ reply = reasoningContent.trim();
605
+ // Stream the content so it appears as the actual response (not just thoughts)
606
+ this.callbacks.onStreamChunk?.(reply, 'content');
607
+ }
608
+ const { output: ensuredReply, appended } = ensureNextSteps(reply);
609
+ let finalReply = ensuredReply;
610
+ // Reset loop detection when we get a text response (not just tool calls)
611
+ if (finalReply.length >= 10) {
612
+ this.lastToolCallSignature = null;
613
+ this.repeatedToolCallCount = 0;
614
+ }
615
+ // If we appended a required Next steps section, stream just the delta
616
+ if (appended) {
617
+ this.callbacks.onStreamChunk?.(appended, 'content');
618
+ }
619
+ finalReply = await this.maybeAdversarialReview(finalReply);
620
+ // Final message - mark as streamed to avoid double-display in UI
621
+ if (finalReply) {
622
+ this.emitAssistantMessage(finalReply, { isFinal: true, usage, contextStats, wasStreamed: true });
623
+ }
624
+ this.messages.push({ role: 'assistant', content: finalReply });
625
+ // Trigger verification for final responses with verifiable claims
626
+ this.triggerVerificationIfNeeded(finalReply);
627
+ return finalReply;
628
+ }
629
+ catch (error) {
630
+ // Auto-recover from context overflow errors (with session-level limit)
631
+ const canRecover = contextRecoveryAttempts < MAX_CONTEXT_RECOVERY_ATTEMPTS &&
632
+ this.totalContextRecoveries < AgentRuntime.MAX_TOTAL_RECOVERIES;
633
+ if (isContextOverflowError(error) && canRecover) {
634
+ contextRecoveryAttempts++;
635
+ this.totalContextRecoveries++;
636
+ const recovered = await this.recoverFromContextOverflow(contextRecoveryAttempts);
637
+ if (recovered) {
638
+ // Notify UI that we're continuing after recovery
639
+ this.callbacks.onContinueAfterRecovery?.();
640
+ // Retry the generation with reduced context
641
+ continue;
642
+ }
643
+ }
644
+ // Auto-retry transient errors (network issues, rate limits, server errors)
645
+ if (isTransientError(error) && transientRetryAttempts < MAX_TRANSIENT_RETRIES) {
646
+ transientRetryAttempts++;
647
+ const delayMs = getRetryDelay(transientRetryAttempts);
648
+ this.callbacks.onRetrying?.(transientRetryAttempts, MAX_TRANSIENT_RETRIES, error);
649
+ await sleep(delayMs);
650
+ continue;
651
+ }
652
+ // Re-throw if not recoverable or recovery failed
653
+ throw error;
654
+ }
655
+ }
656
+ }
657
+ /**
658
+ * Execute tool calls with optimized concurrency
659
+ *
660
+ * PERF: Uses Promise.all for parallel execution with early result handling.
661
+ * Results are collected in order but execution happens concurrently.
662
+ * For very large batches (>10 tools), uses chunked execution to prevent
663
+ * overwhelming system resources.
664
+ */
665
+ async resolveToolCalls(toolCalls) {
666
+ const numCalls = toolCalls.length;
667
+ const executedEdits = [];
668
+ // Check for cancellation before starting tool execution
669
+ if (this.cancellationRequested) {
670
+ // Add cancellation message for each pending tool call
671
+ for (const call of toolCalls) {
672
+ this.messages.push({
673
+ role: 'tool',
674
+ name: call.name,
675
+ toolCallId: call.id,
676
+ content: '[Tool execution cancelled by user]',
677
+ });
678
+ }
679
+ return;
680
+ }
681
+ // Fast path: single tool call
682
+ if (numCalls === 1) {
683
+ const call = toolCalls[0];
684
+ // Check cache first - prevent duplicate identical tool calls
685
+ const cached = this.getCachedToolResult(call);
686
+ if (cached !== null) {
687
+ // Return cached result with indicator that it was from cache
688
+ this.messages.push({
689
+ role: 'tool',
690
+ name: call.name,
691
+ toolCallId: call.id,
692
+ content: `[Cached result - identical call already executed]\n\n${cached}`,
693
+ });
694
+ return;
695
+ }
696
+ this.callbacks.onToolExecution?.(call.name, true);
697
+ const output = await this.toolRuntime.execute(call);
698
+ this.callbacks.onToolExecution?.(call.name, false);
699
+ // Track consecutive failures
700
+ const failureMsg = this.trackToolResult(output);
701
+ // Cache the result for future identical calls
702
+ this.cacheToolResult(call, output);
703
+ if (this.isEditToolCall(call.name)) {
704
+ executedEdits.push({ call, output, fromCache: false });
705
+ }
706
+ // Add tool result to messages
707
+ const toolContent = failureMsg ? `${output}\n\n[SYSTEM: ${failureMsg}]` : output;
708
+ this.messages.push({
709
+ role: 'tool',
710
+ name: call.name,
711
+ toolCallId: call.id,
712
+ content: toolContent,
713
+ });
714
+ await this.maybeExplainEdits(executedEdits);
715
+ return;
716
+ }
717
+ // PERF: For reasonable batch sizes, execute all in parallel
718
+ // Check cache for each call and only execute non-cached ones
719
+ if (numCalls <= 10) {
720
+ const cachedResults = [];
721
+ const toExecute = [];
722
+ // Separate cached from non-cached calls
723
+ for (const call of toolCalls) {
724
+ const cached = this.getCachedToolResult(call);
725
+ if (cached !== null) {
726
+ cachedResults.push({ call, output: cached, fromCache: true });
727
+ if (this.isEditToolCall(call.name)) {
728
+ executedEdits.push({ call, output: cached, fromCache: true });
729
+ }
730
+ }
731
+ else {
732
+ toExecute.push(call);
733
+ }
734
+ }
735
+ // Execute non-cached calls in parallel
736
+ if (toExecute.length > 0) {
737
+ const toolNames = toExecute.map(c => c.name).join(', ');
738
+ this.callbacks.onToolExecution?.(toolNames, true);
739
+ const executed = await Promise.all(toExecute.map(async (call) => {
740
+ const output = await this.toolRuntime.execute(call);
741
+ this.cacheToolResult(call, output);
742
+ if (this.isEditToolCall(call.name)) {
743
+ executedEdits.push({ call, output, fromCache: false });
744
+ }
745
+ return { call, output, fromCache: false };
746
+ }));
747
+ this.callbacks.onToolExecution?.(toolNames, false);
748
+ cachedResults.push(...executed);
749
+ }
750
+ // Add all results to messages in the original order and track failures
751
+ let failureMsg = null;
752
+ for (const originalCall of toolCalls) {
753
+ const result = cachedResults.find(r => r.call.id === originalCall.id);
754
+ if (result) {
755
+ // Track consecutive failures for non-cached results
756
+ if (!result.fromCache) {
757
+ failureMsg = this.trackToolResult(result.output) ?? failureMsg;
758
+ }
759
+ let content = result.fromCache
760
+ ? `[Cached result - identical call already executed]\n\n${result.output}`
761
+ : result.output;
762
+ // Append failure message to last tool result if detected
763
+ if (failureMsg && originalCall === toolCalls[toolCalls.length - 1]) {
764
+ content = `${content}\n\n[SYSTEM: ${failureMsg}]`;
765
+ }
766
+ this.messages.push({
767
+ role: 'tool',
768
+ name: result.call.name,
769
+ toolCallId: result.call.id,
770
+ content,
771
+ });
772
+ }
773
+ }
774
+ await this.maybeExplainEdits(executedEdits);
775
+ return;
776
+ }
777
+ // PERF: For large batches, use chunked parallel execution with caching
778
+ const CHUNK_SIZE = 8;
779
+ const allResults = [];
780
+ for (let i = 0; i < numCalls; i += CHUNK_SIZE) {
781
+ const chunk = toolCalls.slice(i, i + CHUNK_SIZE);
782
+ const cachedInChunk = [];
783
+ const toExecuteInChunk = [];
784
+ for (const call of chunk) {
785
+ const cached = this.getCachedToolResult(call);
786
+ if (cached !== null) {
787
+ cachedInChunk.push({ call, output: cached, fromCache: true });
788
+ if (this.isEditToolCall(call.name)) {
789
+ executedEdits.push({ call, output: cached, fromCache: true });
790
+ }
791
+ }
792
+ else {
793
+ toExecuteInChunk.push(call);
794
+ }
795
+ }
796
+ if (toExecuteInChunk.length > 0) {
797
+ const chunkNames = toExecuteInChunk.map(c => c.name).join(', ');
798
+ this.callbacks.onToolExecution?.(chunkNames, true);
799
+ const executed = await Promise.all(toExecuteInChunk.map(async (call) => {
800
+ const output = await this.toolRuntime.execute(call);
801
+ this.cacheToolResult(call, output);
802
+ if (this.isEditToolCall(call.name)) {
803
+ executedEdits.push({ call, output, fromCache: false });
804
+ }
805
+ return { call, output, fromCache: false };
806
+ }));
807
+ this.callbacks.onToolExecution?.(chunkNames, false);
808
+ cachedInChunk.push(...executed);
809
+ }
810
+ allResults.push(...cachedInChunk);
811
+ }
812
+ // Add results to messages in original order and track failures
813
+ let failureMsg = null;
814
+ for (const originalCall of toolCalls) {
815
+ const result = allResults.find(r => r.call.id === originalCall.id);
816
+ if (result) {
817
+ // Track consecutive failures for non-cached results
818
+ if (!result.fromCache) {
819
+ failureMsg = this.trackToolResult(result.output) ?? failureMsg;
820
+ }
821
+ let content = result.fromCache
822
+ ? `[Cached result - identical call already executed]\n\n${result.output}`
823
+ : result.output;
824
+ // Append failure message to last tool result if detected
825
+ if (failureMsg && originalCall === toolCalls[toolCalls.length - 1]) {
826
+ content = `${content}\n\n[SYSTEM: ${failureMsg}]`;
827
+ }
828
+ this.messages.push({
829
+ role: 'tool',
830
+ name: result.call.name,
831
+ toolCallId: result.call.id,
832
+ content,
833
+ });
834
+ }
835
+ }
836
+ await this.maybeExplainEdits(executedEdits);
837
+ }
838
+ truncateEditOutput(output) {
839
+ if (!output) {
840
+ return '[no tool output available]';
841
+ }
842
+ const limit = AgentRuntime.EDIT_CONTEXT_CHAR_LIMIT;
843
+ if (output.length <= limit) {
844
+ return output;
845
+ }
846
+ const head = output.slice(0, Math.floor(limit * 0.7));
847
+ const tail = output.slice(-Math.floor(limit * 0.2));
848
+ const omitted = output.length - head.length - tail.length;
849
+ return `${head}\n... [truncated ${omitted} chars] ...\n${tail}`;
850
+ }
851
+ buildEditExplanationPrompt(toolName, files, toolOutput) {
852
+ const fileNames = files.map(f => f.split('/').pop()).join(', ');
853
+ const userContent = [
854
+ `Summarize this ${toolName} operation in 1-2 sentences for the UI status line.`,
855
+ `Files: ${fileNames || 'unknown'}`,
856
+ '',
857
+ 'Output:',
858
+ toolOutput.slice(0, 500), // Limit context to reduce hallucination
859
+ ].join('\n');
860
+ return [
861
+ {
862
+ role: 'system',
863
+ content: 'You write brief UI status messages. Reply with ONLY a 1-2 sentence summary. No analysis, no reasoning, no explanations of your process.',
864
+ },
865
+ { role: 'user', content: userContent },
866
+ // Prefill assistant response to guide format
867
+ { role: 'assistant', content: '' },
868
+ ];
869
+ }
870
+ /**
871
+ * Extract clean explanation from model output that may contain reasoning.
872
+ * Reasoning models like deepseek-v4-pro output chain-of-thought which we need to filter.
873
+ */
874
+ extractCleanExplanation(rawOutput) {
875
+ if (!rawOutput)
876
+ return '';
877
+ // Check for common reasoning patterns and extract final output
878
+ const patterns = [
879
+ // "Final explanation:" or "Final concise explanation:" patterns
880
+ /(?:final\s+(?:concise\s+)?explanation\s*:?\s*["']?)([^"'\n]+(?:["']|$))/i,
881
+ // Quoted final output
882
+ /"([^"]{20,})"(?:\s*\([^)]+\))?$/,
883
+ // Last paragraph after deliberation markers
884
+ /(?:draft|summary|output|result)\s*:?\s*\n?\s*["']?([^"'\n]+)/i,
885
+ ];
886
+ for (const pattern of patterns) {
887
+ const match = rawOutput.match(pattern);
888
+ if (match?.[1]) {
889
+ // Clean up the extracted text
890
+ return match[1].replace(/^["']|["']$/g, '').trim();
891
+ }
892
+ }
893
+ // Check if the output looks like reasoning (contains deliberation markers)
894
+ const reasoningMarkers = [
895
+ /^first,?\s+(the user|i need|let me|looking at)/i,
896
+ /^(from this|based on|analyzing|the tool output shows)/i,
897
+ /^(intent:|impact:|user-visible changes:)/im,
898
+ /^(now,?\s+i (?:need|should|will)|let me (?:craft|think|analyze))/i,
899
+ /\b(draft:|final (?:draft|explanation):)/i,
900
+ ];
901
+ const hasReasoning = reasoningMarkers.some(marker => marker.test(rawOutput));
902
+ if (hasReasoning) {
903
+ // Try to extract the last meaningful sentence/paragraph
904
+ const lines = rawOutput.split('\n').filter(l => l.trim());
905
+ // Look for the last line that looks like a summary (not a reasoning line)
906
+ for (let i = lines.length - 1; i >= 0; i--) {
907
+ const line = lines[i].trim();
908
+ // Skip lines that look like reasoning
909
+ if (reasoningMarkers.some(m => m.test(line)))
910
+ continue;
911
+ // Skip very short lines or lines that are just labels
912
+ if (line.length < 30 || /^[\w\s]+:$/.test(line))
913
+ continue;
914
+ // Found a good candidate
915
+ return line.replace(/^["']|["']$/g, '').replace(/\([^)]+\)$/, '').trim();
916
+ }
917
+ // Fallback: take last 200 chars and try to find a sentence
918
+ const tail = rawOutput.slice(-300);
919
+ const lastSentence = tail.match(/[A-Z][^.!?]*[.!?](?:\s|$)/g);
920
+ if (lastSentence?.length) {
921
+ return lastSentence[lastSentence.length - 1].trim();
922
+ }
923
+ }
924
+ // No reasoning detected, return as-is but truncate if too long
925
+ if (rawOutput.length > 500) {
926
+ // Find a sentence break near the end
927
+ const truncated = rawOutput.slice(0, 500);
928
+ const lastPeriod = truncated.lastIndexOf('.');
929
+ if (lastPeriod > 200) {
930
+ return truncated.slice(0, lastPeriod + 1);
931
+ }
932
+ }
933
+ return rawOutput.trim();
934
+ }
935
+ async maybeExplainEdits(results) {
936
+ if (!this.explainEdits || results.length === 0 || this.cancellationRequested) {
937
+ return;
938
+ }
939
+ for (const result of results) {
940
+ if (result.fromCache || !this.isEditToolCall(result.call.name)) {
941
+ continue;
942
+ }
943
+ const files = this.getEditedFiles([result.call]);
944
+ const truncatedOutput = this.truncateEditOutput(result.output);
945
+ const prompt = this.buildEditExplanationPrompt(result.call.name, files, truncatedOutput);
946
+ try {
947
+ const response = await this.provider.generate(prompt, []);
948
+ if (response.type !== 'message') {
949
+ continue;
950
+ }
951
+ // Extract clean explanation, filtering out any reasoning/deliberation
952
+ const rawExplanation = response.content?.trim() ?? '';
953
+ const explanation = this.extractCleanExplanation(rawExplanation);
954
+ if (explanation) {
955
+ this.callbacks.onEditExplanation?.({
956
+ explanation,
957
+ files,
958
+ toolName: result.call.name,
959
+ toolCallId: result.call.id,
960
+ });
961
+ }
962
+ }
963
+ catch (error) {
964
+ logDebug(`[agent] Failed to generate edit explanation: ${safeErrorMessage(error)}`);
965
+ }
966
+ }
967
+ }
968
+ get providerTools() {
969
+ return this.toolRuntime.listProviderTools();
970
+ }
971
+ /**
972
+ * Whether to suppress tool narration in the content field.
973
+ * Previously suppressed for OpenAI but now we show all thinking/narration.
974
+ */
975
+ shouldSuppressToolNarration() {
976
+ return false; // Always show thinking/narration
977
+ }
978
+ emitAssistantMessage(content, metadata) {
979
+ if (!content || !content.trim()) {
980
+ return;
981
+ }
982
+ const elapsedMs = this.activeRun ? Date.now() - this.activeRun.startedAt : undefined;
983
+ const payload = { ...metadata };
984
+ if (typeof elapsedMs === 'number') {
985
+ payload.elapsedMs = elapsedMs;
986
+ }
987
+ this.callbacks.onAssistantMessage?.(content, payload);
988
+ }
989
+ /**
990
+ * Trigger verification for a final response if callback is registered
991
+ * and response contains verifiable claims (implementation, build success, etc.)
992
+ */
993
+ /**
994
+ * Always-on adversarial review of a finished answer (annotate-only). When
995
+ * the adversarial flag is on and real work happened this turn, a critic
996
+ * pass tries to refute the draft; findings are appended as a visible
997
+ * caveat. Non-destructive (peeks tool history without draining the cursor
998
+ * drainToolExecutions relies on) and fail-open — a critic error returns
999
+ * the answer unchanged.
1000
+ */
1001
+ async maybeAdversarialReview(finalReply) {
1002
+ if (!isAdversarialEnabled() || this.cancellationRequested || !finalReply.trim()) {
1003
+ return finalReply;
1004
+ }
1005
+ try {
1006
+ if (typeof this.toolRuntime.getToolHistory !== 'function')
1007
+ return finalReply;
1008
+ const recent = this.toolRuntime.getToolHistory().slice(this.toolHistoryCursor);
1009
+ if (recent.length === 0)
1010
+ return finalReply; // no real work this turn — skip
1011
+ const actions = recent.map((e) => `${e.toolName}${e.success === false ? ' (failed)' : ''}`).join(', ');
1012
+ let request = '';
1013
+ for (let i = this.messages.length - 1; i >= 0; i--) {
1014
+ const m = this.messages[i];
1015
+ if (m && m.role === 'user') {
1016
+ request = typeof m.content === 'string' ? m.content : '';
1017
+ break;
1018
+ }
1019
+ }
1020
+ const review = await reviewDraft(this.provider, { request, actions, draft: finalReply });
1021
+ if (review.ok || !review.findings)
1022
+ return finalReply;
1023
+ // The critic refuted the draft. Signal the shell so it can re-run the
1024
+ // full tool-executing loop to actually FIX the findings (bounded), rather
1025
+ // than this caveat being the end of it. Still append the visible caveat.
1026
+ this.callbacks.onAdversarialFindings?.(review.findings);
1027
+ return `${finalReply}\n\n---\n⚠ Adversarial review:\n${review.findings}`;
1028
+ }
1029
+ catch {
1030
+ return finalReply;
1031
+ }
1032
+ }
1033
+ triggerVerificationIfNeeded(response) {
1034
+ if (!this.callbacks.onVerificationNeeded) {
1035
+ return;
1036
+ }
1037
+ // Only trigger verification for responses that likely contain verifiable claims
1038
+ // These patterns indicate the model is claiming to have completed work
1039
+ const verifiablePatterns = [
1040
+ /\b(implemented|created|wrote|added|fixed|built|deployed|completed|refactored)\b/i,
1041
+ /\b(tests?\s+(are\s+)?pass(ing)?|build\s+succeed)/i,
1042
+ /\b(file|function|class|module|component)\s+(has been|is now|was)\s+(created|updated|modified)/i,
1043
+ /✅|✓|\[done\]|\[complete\]/i,
1044
+ /\bcommit(ted)?\b.*\b(success|done)\b/i,
1045
+ ];
1046
+ const hasVerifiableClaims = verifiablePatterns.some(pattern => pattern.test(response));
1047
+ if (!hasVerifiableClaims) {
1048
+ return;
1049
+ }
1050
+ // Build conversation history for context (last 5 user/assistant exchanges)
1051
+ const conversationHistory = [];
1052
+ const recentMessages = this.messages.slice(-10);
1053
+ for (const msg of recentMessages) {
1054
+ if (msg.role === 'user' || msg.role === 'assistant') {
1055
+ const content = typeof msg.content === 'string' ? msg.content : '';
1056
+ if (content.length > 0) {
1057
+ conversationHistory.push(`${msg.role}: ${content.slice(0, 500)}`);
1058
+ }
1059
+ }
1060
+ }
1061
+ // Trigger verification callback
1062
+ this.callbacks.onVerificationNeeded(response, {
1063
+ workingDirectory: this.workingDirectory,
1064
+ conversationHistory,
1065
+ provider: this.providerId,
1066
+ model: this.modelId,
1067
+ });
1068
+ }
1069
+ /**
1070
+ * Extract a "command hash" from tool arguments for behavioral loop detection.
1071
+ * For execute_bash, this is the actual command. For other tools, key identifying args.
1072
+ */
1073
+ extractCmdHash(name, args) {
1074
+ // For bash/execute commands, extract the command itself
1075
+ if (name === 'execute_bash' || name === 'Bash') {
1076
+ const cmd = args['command'];
1077
+ if (cmd) {
1078
+ // Normalize: trim, take first 100 chars, remove variable parts like timestamps
1079
+ return cmd.trim().slice(0, 100).replace(/\d{10,}/g, 'N');
1080
+ }
1081
+ }
1082
+ // For file operations, use the path
1083
+ if (name === 'read_file' || name === 'Read' || name === 'read_files') {
1084
+ const path = args['path'] || args['file_path'] || args['paths'];
1085
+ if (path)
1086
+ return `path:${JSON.stringify(path).slice(0, 100)}`;
1087
+ }
1088
+ if (name === 'list_files' || name === 'Glob') {
1089
+ const path = args['path'] || args['pattern'];
1090
+ if (path)
1091
+ return `path:${JSON.stringify(path).slice(0, 100)}`;
1092
+ }
1093
+ // For search, use the query/pattern
1094
+ if (name === 'Grep' || name === 'grep' || name === 'search') {
1095
+ const pattern = args['pattern'] || args['query'];
1096
+ if (pattern)
1097
+ return `search:${String(pattern).slice(0, 100)}`;
1098
+ }
1099
+ // Default: use first significant arg value
1100
+ const firstArg = Object.values(args)[0];
1101
+ if (firstArg) {
1102
+ return String(firstArg).slice(0, 100);
1103
+ }
1104
+ return 'no-args';
1105
+ }
1106
+ /**
1107
+ * Check for behavioral loops - model calling the same tool with similar args repeatedly.
1108
+ * Returns an error message if a loop is detected, null otherwise.
1109
+ *
1110
+ * FUNDAMENTAL PREVENTION: Cached calls are excluded from loop detection since they
1111
+ * don't actually execute (the cache provides the result). This means:
1112
+ * - First call: executes and caches result
1113
+ * - Second identical call: returns cached result, NOT counted toward loop
1114
+ * - Only genuinely NEW (non-cached) repetitive calls trigger loop detection
1115
+ *
1116
+ * Direct execution tools (bash/edit) are also exempt to avoid short-circuiting
1117
+ * legitimate repeated user commands.
1118
+ *
1119
+ * This catches patterns like:
1120
+ * - "git status -sb" called 3 times with DIFFERENT outputs (cache miss each time)
1121
+ * - Repeated file reads where file content changed
1122
+ * - Repeated searches with same pattern but new results
1123
+ */
1124
+ checkBehavioralLoop(toolCalls) {
1125
+ // Skip loop detection for direct execution tools (bash/edit) to avoid false positives
1126
+ const loopEligibleCalls = toolCalls.filter(call => !this.shouldSkipLoopDetection(call));
1127
+ if (loopEligibleCalls.length === 0) {
1128
+ return null;
1129
+ }
1130
+ // Filter out calls that will be served from cache - these don't count toward loops
1131
+ // since they're handled fundamentally by the caching mechanism
1132
+ const nonCachedCalls = loopEligibleCalls.filter(call => this.getCachedToolResult(call) === null);
1133
+ // If all calls are cached, no loop detection needed
1134
+ if (nonCachedCalls.length === 0) {
1135
+ return null;
1136
+ }
1137
+ // Count existing occurrences in recent history
1138
+ const existingCounts = new Map();
1139
+ for (const { name, cmdHash } of this.recentToolCalls) {
1140
+ const key = `${name}:${cmdHash}`;
1141
+ existingCounts.set(key, (existingCounts.get(key) ?? 0) + 1);
1142
+ }
1143
+ // Check if ANY incoming NON-CACHED call would exceed threshold
1144
+ for (const call of nonCachedCalls) {
1145
+ const cmdHash = this.extractCmdHash(call.name, call.arguments ?? {});
1146
+ const key = `${call.name}:${cmdHash}`;
1147
+ const currentCount = existingCounts.get(key) ?? 0;
1148
+ // If adding this call would reach or exceed threshold, block immediately
1149
+ if (currentCount + 1 >= AgentRuntime.BEHAVIORAL_LOOP_THRESHOLD) {
1150
+ // Reset history to prevent immediate re-trigger
1151
+ this.recentToolCalls = [];
1152
+ return `Behavioral loop detected: "${call.name}" called ${currentCount + 1} times with similar arguments. The task appears stuck. Please try a different approach or provide more specific instructions.`;
1153
+ }
1154
+ }
1155
+ // Track only non-cached tool calls (cached ones are handled by caching)
1156
+ for (const call of nonCachedCalls) {
1157
+ const cmdHash = this.extractCmdHash(call.name, call.arguments ?? {});
1158
+ this.recentToolCalls.push({ name: call.name, cmdHash });
1159
+ }
1160
+ // Keep only recent history
1161
+ while (this.recentToolCalls.length > AgentRuntime.TOOL_HISTORY_SIZE) {
1162
+ this.recentToolCalls.shift();
1163
+ }
1164
+ return null;
1165
+ }
1166
+ /**
1167
+ * Provide an acknowledgement before the first tool call when the model
1168
+ * hasn't narrated its plan. This keeps the UI responsive and lets the
1169
+ * user know work is happening even before tool output arrives.
1170
+ */
1171
+ maybeAckToolCalls(toolCalls, hasModelNarration, usage, contextStats) {
1172
+ if (!toolCalls?.length) {
1173
+ return;
1174
+ }
1175
+ const acknowledgement = this.callbacks.onBeforeFirstToolCall?.(toolCalls.map((call) => call.name), hasModelNarration);
1176
+ if (acknowledgement && acknowledgement.trim()) {
1177
+ this.emitAssistantMessage(acknowledgement, {
1178
+ isFinal: false,
1179
+ usage,
1180
+ contextStats,
1181
+ });
1182
+ }
1183
+ }
1184
+ /**
1185
+ * Reset behavioral loop tracking (called when user provides new input or task completes)
1186
+ */
1187
+ resetBehavioralLoopTracking() {
1188
+ this.recentToolCalls = [];
1189
+ this.lastToolCallSignature = null;
1190
+ this.repeatedToolCallCount = 0;
1191
+ this.consecutiveFailures = 0;
1192
+ // Note: we DON'T clear toolResultCache here for cacheable tools; stateful tools bypass caching
1193
+ }
1194
+ /**
1195
+ * Create a stable cache key for a tool call based on name and arguments
1196
+ */
1197
+ getToolCacheKey(call) {
1198
+ const args = call.arguments ?? {};
1199
+ // Sort keys for consistent ordering
1200
+ const sortedArgs = Object.keys(args).sort().reduce((acc, key) => {
1201
+ acc[key] = args[key];
1202
+ return acc;
1203
+ }, {});
1204
+ return `${call.name}:${JSON.stringify(sortedArgs)}`;
1205
+ }
1206
+ /**
1207
+ * Only cache tools that are safe to reuse; stateful commands must always execute.
1208
+ */
1209
+ isCacheableTool(call) {
1210
+ const nameLower = call.name.toLowerCase();
1211
+ return !AgentRuntime.NON_CACHEABLE_TOOL_NAMES.has(nameLower);
1212
+ }
1213
+ /**
1214
+ * Direct execution tools should not trigger behavioral loop short-circuiting.
1215
+ */
1216
+ shouldSkipLoopDetection(call) {
1217
+ const nameLower = call.name.toLowerCase();
1218
+ return AgentRuntime.LOOP_EXEMPT_TOOL_NAMES.has(nameLower);
1219
+ }
1220
+ /**
1221
+ * Check if a tool output indicates failure
1222
+ */
1223
+ isToolOutputFailure(output) {
1224
+ const failurePatterns = [
1225
+ /\bfailed\b/i,
1226
+ /\berror[:\s]/i,
1227
+ /\bexception\b/i,
1228
+ /\bcommand not found\b/i,
1229
+ /\bno such file\b/i,
1230
+ /\bpermission denied\b/i,
1231
+ /\bexit code [1-9]/i,
1232
+ /\btimeout\b/i,
1233
+ /\bcannot\b.*\bfind\b/i,
1234
+ /\bunable to\b/i,
1235
+ /\bsyntax error\b/i,
1236
+ ];
1237
+ return failurePatterns.some(pattern => pattern.test(output));
1238
+ }
1239
+ /**
1240
+ * Track tool execution result and check for consecutive failures.
1241
+ * Returns error message if too many consecutive failures, null otherwise.
1242
+ */
1243
+ trackToolResult(output) {
1244
+ if (this.isToolOutputFailure(output)) {
1245
+ this.consecutiveFailures++;
1246
+ if (this.consecutiveFailures >= AgentRuntime.MAX_CONSECUTIVE_FAILURES) {
1247
+ const msg = `Multiple consecutive command failures detected (${this.consecutiveFailures}). The task appears to be stuck. Please review the errors and try a different approach.`;
1248
+ this.consecutiveFailures = 0; // Reset to allow retry with new approach
1249
+ return msg;
1250
+ }
1251
+ }
1252
+ else {
1253
+ // Successful execution resets the counter
1254
+ this.consecutiveFailures = 0;
1255
+ }
1256
+ return null;
1257
+ }
1258
+ /**
1259
+ * Get cached result for a tool call, or null if not cached
1260
+ */
1261
+ getCachedToolResult(call) {
1262
+ if (!this.isCacheableTool(call)) {
1263
+ return null;
1264
+ }
1265
+ const key = this.getToolCacheKey(call);
1266
+ return this.toolResultCache.get(key) ?? null;
1267
+ }
1268
+ /**
1269
+ * Cache a tool result for future identical calls
1270
+ */
1271
+ cacheToolResult(call, result) {
1272
+ if (!this.isCacheableTool(call)) {
1273
+ return;
1274
+ }
1275
+ const key = this.getToolCacheKey(call);
1276
+ // Evict oldest entries if cache is full
1277
+ if (this.toolResultCache.size >= AgentRuntime.TOOL_CACHE_MAX_SIZE) {
1278
+ const firstKey = this.toolResultCache.keys().next().value;
1279
+ if (firstKey) {
1280
+ this.toolResultCache.delete(firstKey);
1281
+ }
1282
+ }
1283
+ this.toolResultCache.set(key, result);
1284
+ }
1285
+ /**
1286
+ * Drain the list of tools executed during the most recent send() call.
1287
+ * Used by higher-level orchestrators to reason about progress.
1288
+ */
1289
+ drainToolExecutions() {
1290
+ if (typeof this.toolRuntime.getToolHistory !== 'function') {
1291
+ return [];
1292
+ }
1293
+ const history = this.toolRuntime.getToolHistory();
1294
+ const newEntries = history.slice(this.toolHistoryCursor);
1295
+ this.toolHistoryCursor = history.length;
1296
+ return newEntries.map((entry) => ({
1297
+ name: entry.toolName,
1298
+ success: entry.success ?? true,
1299
+ hasOutput: entry.hasOutput ?? true,
1300
+ }));
1301
+ }
1302
+ getHistory() {
1303
+ return this.messages.map(cloneMessage);
1304
+ }
1305
+ loadHistory(history) {
1306
+ this.messages.length = 0;
1307
+ if (history.length === 0) {
1308
+ if (this.baseSystemPrompt) {
1309
+ this.messages.push({ role: 'system', content: this.baseSystemPrompt });
1310
+ }
1311
+ return;
1312
+ }
1313
+ for (const message of history) {
1314
+ this.messages.push(cloneMessage(message));
1315
+ }
1316
+ }
1317
+ clearHistory() {
1318
+ this.messages.length = 0;
1319
+ if (this.baseSystemPrompt) {
1320
+ this.messages.push({ role: 'system', content: this.baseSystemPrompt });
1321
+ }
1322
+ }
1323
+ /**
1324
+ * Prune messages if approaching context limit
1325
+ *
1326
+ * This runs BEFORE each generation to ensure we stay within budget.
1327
+ * If LLM summarization is available, it will create intelligent summaries
1328
+ * instead of just removing old messages.
1329
+ */
1330
+ async pruneMessagesIfNeeded() {
1331
+ if (!this.contextManager) {
1332
+ return;
1333
+ }
1334
+ if (this.contextManager.isApproachingLimit(this.messages)) {
1335
+ // Token count before compaction, so we can report how much was freed.
1336
+ const beforeTokens = this.contextManager.getStats(this.messages).totalTokens;
1337
+ // Try LLM-based summarization first (preserves context better)
1338
+ const result = await this.contextManager.pruneMessagesWithSummary(this.messages);
1339
+ if (result.removed > 0) {
1340
+ // Replace messages with pruned/summarized version
1341
+ this.messages.length = 0;
1342
+ this.messages.push(...result.pruned);
1343
+ // Notify callback with enriched stats
1344
+ const stats = this.contextManager.getStats(this.messages);
1345
+ const enrichedStats = {
1346
+ ...stats,
1347
+ summarized: result.summarized,
1348
+ method: result.summarized ? 'llm-summary' : 'simple-prune',
1349
+ freedTokens: Math.max(0, beforeTokens - stats.totalTokens),
1350
+ };
1351
+ this.callbacks.onContextPruned?.(result.removed, enrichedStats);
1352
+ if (process.env['DEBUG_CONTEXT']) {
1353
+ logDebug(`[Context Manager] ${result.summarized ? 'Summarized' : 'Pruned'} ${result.removed} messages. ` +
1354
+ `Tokens: ${stats.totalTokens} (${stats.percentage}%)`);
1355
+ }
1356
+ }
1357
+ }
1358
+ }
1359
+ /**
1360
+ * Get current context statistics
1361
+ */
1362
+ getContextStats() {
1363
+ if (!this.contextManager) {
1364
+ return null;
1365
+ }
1366
+ return this.contextManager.getStats(this.messages);
1367
+ }
1368
+ /**
1369
+ * Get context manager instance
1370
+ */
1371
+ getContextManager() {
1372
+ return this.contextManager;
1373
+ }
1374
+ /**
1375
+ * Fetch model info from the provider API.
1376
+ * Returns context window and token limits from the real API.
1377
+ * Results are cached for the lifetime of this agent instance.
1378
+ */
1379
+ async fetchModelInfo() {
1380
+ if (this.modelInfoFetched) {
1381
+ return this.modelInfo;
1382
+ }
1383
+ this.modelInfoFetched = true;
1384
+ if (typeof this.provider.getModelInfo === 'function') {
1385
+ try {
1386
+ this.modelInfo = await this.provider.getModelInfo();
1387
+ }
1388
+ catch {
1389
+ // Ignore errors - fall back to null
1390
+ this.modelInfo = null;
1391
+ }
1392
+ }
1393
+ return this.modelInfo;
1394
+ }
1395
+ /**
1396
+ * Get cached model info (must call fetchModelInfo first)
1397
+ */
1398
+ getModelInfo() {
1399
+ return this.modelInfo;
1400
+ }
1401
+ /**
1402
+ * Get the context window size from the provider API.
1403
+ * Returns null if the provider doesn't support this or the API call fails.
1404
+ */
1405
+ async getContextWindowFromProvider() {
1406
+ const info = await this.fetchModelInfo();
1407
+ return info?.contextWindow ?? null;
1408
+ }
1409
+ /**
1410
+ * Auto-recover from context overflow errors by aggressively pruning messages.
1411
+ *
1412
+ * This is called when an API call fails due to context length exceeding limits.
1413
+ * It performs increasingly aggressive pruning on each attempt:
1414
+ * - Attempt 1: Remove 30% of oldest messages + truncate tool outputs to 5k
1415
+ * - Attempt 2: Remove 50% of oldest messages + truncate tool outputs to 2k
1416
+ * - Attempt 3: Remove 70% of oldest messages + truncate tool outputs to 500 chars
1417
+ *
1418
+ * @returns true if recovery was successful (context was reduced)
1419
+ */
1420
+ async recoverFromContextOverflow(attempt) {
1421
+ // Calculate reduction percentage based on attempt
1422
+ const reductionPercentages = [0.3, 0.5, 0.7];
1423
+ const reductionPercent = reductionPercentages[attempt - 1] ?? 0.7;
1424
+ // Increasingly aggressive tool output truncation limits
1425
+ const toolOutputLimits = [5000, 2000, 500];
1426
+ const toolOutputLimit = toolOutputLimits[attempt - 1] ?? 500;
1427
+ // Notify UI about recovery attempt
1428
+ const message = `Context overflow detected. Auto-squishing context (attempt ${attempt}/${MAX_CONTEXT_RECOVERY_ATTEMPTS}, removing ${Math.round(reductionPercent * 100)}% of history)...`;
1429
+ this.callbacks.onContextRecovery?.(attempt, MAX_CONTEXT_RECOVERY_ATTEMPTS, message);
1430
+ this.callbacks.onContextSquishing?.(message);
1431
+ // Separate system messages from conversation
1432
+ const systemMessages = [];
1433
+ const conversationMessages = [];
1434
+ for (const msg of this.messages) {
1435
+ if (msg.role === 'system') {
1436
+ systemMessages.push(msg);
1437
+ }
1438
+ else {
1439
+ conversationMessages.push(msg);
1440
+ }
1441
+ }
1442
+ // Calculate how many messages to remove (target)
1443
+ const targetRemoveCount = Math.floor(conversationMessages.length * reductionPercent);
1444
+ if (targetRemoveCount === 0 || conversationMessages.length <= 2) {
1445
+ // Nothing to remove or too few messages - can't recover
1446
+ return false;
1447
+ }
1448
+ // Group messages into conversation "turns" to maintain tool call/result pairing
1449
+ // A turn is: [user] or [assistant + tool results] or [assistant without tools]
1450
+ const turns = [];
1451
+ let currentTurn = [];
1452
+ for (let i = 0; i < conversationMessages.length; i++) {
1453
+ const msg = conversationMessages[i];
1454
+ if (msg.role === 'user') {
1455
+ // User messages start a new turn
1456
+ if (currentTurn.length > 0) {
1457
+ turns.push(currentTurn);
1458
+ }
1459
+ currentTurn = [msg];
1460
+ }
1461
+ else if (msg.role === 'assistant') {
1462
+ // Assistant messages start a new turn (flush previous)
1463
+ if (currentTurn.length > 0) {
1464
+ turns.push(currentTurn);
1465
+ }
1466
+ currentTurn = [msg];
1467
+ }
1468
+ else if (msg.role === 'tool') {
1469
+ // Tool results belong to the current assistant turn
1470
+ currentTurn.push(msg);
1471
+ }
1472
+ }
1473
+ // Don't forget the last turn
1474
+ if (currentTurn.length > 0) {
1475
+ turns.push(currentTurn);
1476
+ }
1477
+ // Calculate how many turns to remove
1478
+ const targetTurnsToRemove = Math.floor(turns.length * reductionPercent);
1479
+ if (targetTurnsToRemove === 0 || turns.length <= 2) {
1480
+ return false;
1481
+ }
1482
+ // Keep recent turns (remove from the beginning)
1483
+ const keepTurns = turns.slice(targetTurnsToRemove);
1484
+ // IMPORTANT: Ensure we don't start with orphaned tool messages
1485
+ // The first kept turn must NOT be a tool-only turn
1486
+ let startIndex = 0;
1487
+ while (startIndex < keepTurns.length) {
1488
+ const firstTurn = keepTurns[startIndex];
1489
+ if (firstTurn && firstTurn.length > 0) {
1490
+ const firstMsg = firstTurn[0];
1491
+ // If first message is a tool result, skip this turn
1492
+ if (firstMsg?.role === 'tool') {
1493
+ startIndex++;
1494
+ continue;
1495
+ }
1496
+ // If first message is an assistant with tool calls but we're missing results,
1497
+ // check if all tool results are present
1498
+ if (firstMsg?.role === 'assistant' && firstMsg.toolCalls && firstMsg.toolCalls.length > 0) {
1499
+ // PERF: Pre-compute tool call IDs as array, use direct Set lookup
1500
+ const toolCallIds = firstMsg.toolCalls.map(tc => tc.id);
1501
+ const presentToolResultIds = new Set(firstTurn.filter(m => m.role === 'tool').map(m => m.toolCallId));
1502
+ // PERF: Direct has() calls with early exit instead of spread + every()
1503
+ let allPresent = true;
1504
+ for (const id of toolCallIds) {
1505
+ if (!presentToolResultIds.has(id)) {
1506
+ allPresent = false;
1507
+ break;
1508
+ }
1509
+ }
1510
+ if (allPresent) {
1511
+ break;
1512
+ }
1513
+ // Otherwise skip this turn
1514
+ startIndex++;
1515
+ continue;
1516
+ }
1517
+ }
1518
+ break;
1519
+ }
1520
+ const validTurns = keepTurns.slice(startIndex);
1521
+ if (validTurns.length === 0) {
1522
+ return false;
1523
+ }
1524
+ // Flatten valid turns back to messages
1525
+ const keepMessages = validTurns.flat();
1526
+ const actualRemoveCount = conversationMessages.length - keepMessages.length;
1527
+ // Aggressively truncate tool outputs in remaining messages
1528
+ let truncatedCount = 0;
1529
+ for (const msg of keepMessages) {
1530
+ if (msg.role === 'tool' && msg.content) {
1531
+ const content = typeof msg.content === 'string' ? msg.content : JSON.stringify(msg.content);
1532
+ if (content.length > toolOutputLimit) {
1533
+ // Truncate with smart ending
1534
+ const truncated = content.slice(0, toolOutputLimit);
1535
+ const lastNewline = truncated.lastIndexOf('\n');
1536
+ const cutPoint = lastNewline > toolOutputLimit * 0.7 ? lastNewline : toolOutputLimit;
1537
+ msg.content = `${truncated.slice(0, cutPoint)}\n\n[... truncated ${content.length - cutPoint} chars for context recovery ...]`;
1538
+ truncatedCount++;
1539
+ }
1540
+ }
1541
+ // Also truncate very long assistant messages
1542
+ if (msg.role === 'assistant' && msg.content && msg.content.length > toolOutputLimit * 2) {
1543
+ const content = msg.content;
1544
+ const limit = toolOutputLimit * 2;
1545
+ const truncated = content.slice(0, limit);
1546
+ const lastNewline = truncated.lastIndexOf('\n');
1547
+ const cutPoint = lastNewline > limit * 0.8 ? lastNewline : limit;
1548
+ msg.content = `${truncated.slice(0, cutPoint)}\n\n[... truncated for context recovery ...]`;
1549
+ truncatedCount++;
1550
+ }
1551
+ }
1552
+ // Also truncate system messages if they're huge (except first system prompt)
1553
+ for (let i = 1; i < systemMessages.length; i++) {
1554
+ const sys = systemMessages[i];
1555
+ if (sys && sys.content && sys.content.length > toolOutputLimit) {
1556
+ sys.content = `${sys.content.slice(0, toolOutputLimit)}\n[... truncated ...]`;
1557
+ truncatedCount++;
1558
+ }
1559
+ }
1560
+ // Rebuild message array
1561
+ this.messages.length = 0;
1562
+ // Add system messages
1563
+ for (const sys of systemMessages) {
1564
+ this.messages.push(sys);
1565
+ }
1566
+ // Add summary notice
1567
+ this.messages.push({
1568
+ role: 'system',
1569
+ content: `[Auto Context Recovery] Removed ${actualRemoveCount} messages and truncated ${truncatedCount} large outputs to stay within token limits.`,
1570
+ });
1571
+ // Add remaining conversation (maintaining tool call/result pairing)
1572
+ for (const msg of keepMessages) {
1573
+ this.messages.push(msg);
1574
+ }
1575
+ // Notify about pruning
1576
+ const stats = this.contextManager?.getStats(this.messages) ?? {};
1577
+ this.callbacks.onContextPruned?.(actualRemoveCount, {
1578
+ ...stats,
1579
+ method: 'emergency-recovery',
1580
+ attempt,
1581
+ removedPercent: reductionPercent * 100,
1582
+ turnsRemoved: targetTurnsToRemove + startIndex,
1583
+ truncatedOutputs: truncatedCount,
1584
+ toolOutputLimit,
1585
+ });
1586
+ // Check if we're still over limit after all reductions
1587
+ const newStats = this.contextManager?.getStats(this.messages);
1588
+ if (newStats && newStats.percentage > 100) {
1589
+ // Still over limit - do one more aggressive pass
1590
+ // Truncate ALL tool outputs to absolute minimum
1591
+ const minLimit = 200;
1592
+ for (const msg of this.messages) {
1593
+ if (msg.role === 'tool' && msg.content) {
1594
+ const content = typeof msg.content === 'string' ? msg.content : JSON.stringify(msg.content);
1595
+ if (content.length > minLimit) {
1596
+ msg.content = `${content.slice(0, minLimit)}\n[... severely truncated ...]`;
1597
+ }
1598
+ }
1599
+ }
1600
+ }
1601
+ return true;
1602
+ }
1603
+ }
1604
+ function cloneMessage(message) {
1605
+ switch (message.role) {
1606
+ case 'assistant': {
1607
+ const clone = {
1608
+ role: 'assistant',
1609
+ content: message.content,
1610
+ };
1611
+ if (message.toolCalls) {
1612
+ clone.toolCalls = message.toolCalls.map(cloneToolCall);
1613
+ }
1614
+ return clone;
1615
+ }
1616
+ case 'tool':
1617
+ return {
1618
+ role: 'tool',
1619
+ name: message.name,
1620
+ content: message.content,
1621
+ toolCallId: message.toolCallId,
1622
+ };
1623
+ case 'system':
1624
+ return { role: 'system', content: message.content };
1625
+ case 'user':
1626
+ default:
1627
+ return { role: 'user', content: message.content };
1628
+ }
1629
+ }
1630
+ function cloneToolCall(call) {
1631
+ return {
1632
+ id: call.id,
1633
+ name: call.name,
1634
+ arguments: { ...(call.arguments ?? {}) },
1635
+ };
1636
+ }
1637
+ //# sourceMappingURL=agent.js.map