@project-ava-supernova/core 0.2.71

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 (1497) hide show
  1. package/dist/agent/agent.d.ts +623 -0
  2. package/dist/agent/agent.d.ts.map +1 -0
  3. package/dist/agent/agent.js +3558 -0
  4. package/dist/agent/agent.js.map +1 -0
  5. package/dist/agent/claims-auditor.d.ts +54 -0
  6. package/dist/agent/claims-auditor.d.ts.map +1 -0
  7. package/dist/agent/claims-auditor.js +125 -0
  8. package/dist/agent/claims-auditor.js.map +1 -0
  9. package/dist/agent/context-continuity.d.ts +125 -0
  10. package/dist/agent/context-continuity.d.ts.map +1 -0
  11. package/dist/agent/context-continuity.js +231 -0
  12. package/dist/agent/context-continuity.js.map +1 -0
  13. package/dist/agent/conversation.d.ts +26 -0
  14. package/dist/agent/conversation.d.ts.map +1 -0
  15. package/dist/agent/conversation.js +86 -0
  16. package/dist/agent/conversation.js.map +1 -0
  17. package/dist/agent/design-reinjection.d.ts +93 -0
  18. package/dist/agent/design-reinjection.d.ts.map +1 -0
  19. package/dist/agent/design-reinjection.js +235 -0
  20. package/dist/agent/design-reinjection.js.map +1 -0
  21. package/dist/agent/error-loop-detector.d.ts +59 -0
  22. package/dist/agent/error-loop-detector.d.ts.map +1 -0
  23. package/dist/agent/error-loop-detector.js +124 -0
  24. package/dist/agent/error-loop-detector.js.map +1 -0
  25. package/dist/agent/fresh-eyes.d.ts +52 -0
  26. package/dist/agent/fresh-eyes.d.ts.map +1 -0
  27. package/dist/agent/fresh-eyes.js +118 -0
  28. package/dist/agent/fresh-eyes.js.map +1 -0
  29. package/dist/agent/intent-classifier.d.ts +51 -0
  30. package/dist/agent/intent-classifier.d.ts.map +1 -0
  31. package/dist/agent/intent-classifier.js +125 -0
  32. package/dist/agent/intent-classifier.js.map +1 -0
  33. package/dist/agent/post-edit-verify.d.ts +79 -0
  34. package/dist/agent/post-edit-verify.d.ts.map +1 -0
  35. package/dist/agent/post-edit-verify.js +143 -0
  36. package/dist/agent/post-edit-verify.js.map +1 -0
  37. package/dist/agent/recover-written-calls.d.ts +90 -0
  38. package/dist/agent/recover-written-calls.d.ts.map +1 -0
  39. package/dist/agent/recover-written-calls.js +257 -0
  40. package/dist/agent/recover-written-calls.js.map +1 -0
  41. package/dist/agent/social-craft.d.ts +76 -0
  42. package/dist/agent/social-craft.d.ts.map +1 -0
  43. package/dist/agent/social-craft.js +210 -0
  44. package/dist/agent/social-craft.js.map +1 -0
  45. package/dist/agent/stop-command.d.ts +23 -0
  46. package/dist/agent/stop-command.d.ts.map +1 -0
  47. package/dist/agent/stop-command.js +59 -0
  48. package/dist/agent/stop-command.js.map +1 -0
  49. package/dist/agent/system-prompt.d.ts +125 -0
  50. package/dist/agent/system-prompt.d.ts.map +1 -0
  51. package/dist/agent/system-prompt.js +1334 -0
  52. package/dist/agent/system-prompt.js.map +1 -0
  53. package/dist/agent/task-classifier.d.ts +50 -0
  54. package/dist/agent/task-classifier.d.ts.map +1 -0
  55. package/dist/agent/task-classifier.js +187 -0
  56. package/dist/agent/task-classifier.js.map +1 -0
  57. package/dist/agent/text-tool-parser.d.ts +34 -0
  58. package/dist/agent/text-tool-parser.d.ts.map +1 -0
  59. package/dist/agent/text-tool-parser.js +96 -0
  60. package/dist/agent/text-tool-parser.js.map +1 -0
  61. package/dist/agent/vision-bridge.d.ts +29 -0
  62. package/dist/agent/vision-bridge.d.ts.map +1 -0
  63. package/dist/agent/vision-bridge.js +143 -0
  64. package/dist/agent/vision-bridge.js.map +1 -0
  65. package/dist/audit/cost.d.ts +18 -0
  66. package/dist/audit/cost.d.ts.map +1 -0
  67. package/dist/audit/cost.js +170 -0
  68. package/dist/audit/cost.js.map +1 -0
  69. package/dist/audit/export.d.ts +30 -0
  70. package/dist/audit/export.d.ts.map +1 -0
  71. package/dist/audit/export.js +155 -0
  72. package/dist/audit/export.js.map +1 -0
  73. package/dist/audit/index.d.ts +11 -0
  74. package/dist/audit/index.d.ts.map +1 -0
  75. package/dist/audit/index.js +9 -0
  76. package/dist/audit/index.js.map +1 -0
  77. package/dist/audit/integrity.d.ts +7 -0
  78. package/dist/audit/integrity.d.ts.map +1 -0
  79. package/dist/audit/integrity.js +78 -0
  80. package/dist/audit/integrity.js.map +1 -0
  81. package/dist/audit/logger.d.ts +18 -0
  82. package/dist/audit/logger.d.ts.map +1 -0
  83. package/dist/audit/logger.js +113 -0
  84. package/dist/audit/logger.js.map +1 -0
  85. package/dist/audit/patterns.d.ts +34 -0
  86. package/dist/audit/patterns.d.ts.map +1 -0
  87. package/dist/audit/patterns.js +104 -0
  88. package/dist/audit/patterns.js.map +1 -0
  89. package/dist/audit/security.d.ts +8 -0
  90. package/dist/audit/security.d.ts.map +1 -0
  91. package/dist/audit/security.js +56 -0
  92. package/dist/audit/security.js.map +1 -0
  93. package/dist/audit/summary.d.ts +22 -0
  94. package/dist/audit/summary.d.ts.map +1 -0
  95. package/dist/audit/summary.js +51 -0
  96. package/dist/audit/summary.js.map +1 -0
  97. package/dist/audit/types.d.ts +104 -0
  98. package/dist/audit/types.d.ts.map +1 -0
  99. package/dist/audit/types.js +11 -0
  100. package/dist/audit/types.js.map +1 -0
  101. package/dist/auto/aurora-router.d.ts +71 -0
  102. package/dist/auto/aurora-router.d.ts.map +1 -0
  103. package/dist/auto/aurora-router.js +138 -0
  104. package/dist/auto/aurora-router.js.map +1 -0
  105. package/dist/auto/auto-coordinator.d.ts +232 -0
  106. package/dist/auto/auto-coordinator.d.ts.map +1 -0
  107. package/dist/auto/auto-coordinator.js +1260 -0
  108. package/dist/auto/auto-coordinator.js.map +1 -0
  109. package/dist/auto/brief-generator.d.ts +10 -0
  110. package/dist/auto/brief-generator.d.ts.map +1 -0
  111. package/dist/auto/brief-generator.js +67 -0
  112. package/dist/auto/brief-generator.js.map +1 -0
  113. package/dist/auto/changes-summary.d.ts +19 -0
  114. package/dist/auto/changes-summary.d.ts.map +1 -0
  115. package/dist/auto/changes-summary.js +45 -0
  116. package/dist/auto/changes-summary.js.map +1 -0
  117. package/dist/auto/context-tracker.d.ts +27 -0
  118. package/dist/auto/context-tracker.d.ts.map +1 -0
  119. package/dist/auto/context-tracker.js +56 -0
  120. package/dist/auto/context-tracker.js.map +1 -0
  121. package/dist/auto/coordinator-model.d.ts +31 -0
  122. package/dist/auto/coordinator-model.d.ts.map +1 -0
  123. package/dist/auto/coordinator-model.js +68 -0
  124. package/dist/auto/coordinator-model.js.map +1 -0
  125. package/dist/auto/index.d.ts +12 -0
  126. package/dist/auto/index.d.ts.map +1 -0
  127. package/dist/auto/index.js +11 -0
  128. package/dist/auto/index.js.map +1 -0
  129. package/dist/auto/intent-gate.d.ts +85 -0
  130. package/dist/auto/intent-gate.d.ts.map +1 -0
  131. package/dist/auto/intent-gate.js +247 -0
  132. package/dist/auto/intent-gate.js.map +1 -0
  133. package/dist/auto/longxiang-router.d.ts +123 -0
  134. package/dist/auto/longxiang-router.d.ts.map +1 -0
  135. package/dist/auto/longxiang-router.js +184 -0
  136. package/dist/auto/longxiang-router.js.map +1 -0
  137. package/dist/auto/model-router.d.ts +42 -0
  138. package/dist/auto/model-router.d.ts.map +1 -0
  139. package/dist/auto/model-router.js +208 -0
  140. package/dist/auto/model-router.js.map +1 -0
  141. package/dist/auto/routing-modes.d.ts +33 -0
  142. package/dist/auto/routing-modes.d.ts.map +1 -0
  143. package/dist/auto/routing-modes.js +34 -0
  144. package/dist/auto/routing-modes.js.map +1 -0
  145. package/dist/auto/supernova-router.d.ts +50 -0
  146. package/dist/auto/supernova-router.d.ts.map +1 -0
  147. package/dist/auto/supernova-router.js +116 -0
  148. package/dist/auto/supernova-router.js.map +1 -0
  149. package/dist/auto/task-classifier.d.ts +10 -0
  150. package/dist/auto/task-classifier.d.ts.map +1 -0
  151. package/dist/auto/task-classifier.js +116 -0
  152. package/dist/auto/task-classifier.js.map +1 -0
  153. package/dist/auto/task-executor.d.ts +86 -0
  154. package/dist/auto/task-executor.d.ts.map +1 -0
  155. package/dist/auto/task-executor.js +271 -0
  156. package/dist/auto/task-executor.js.map +1 -0
  157. package/dist/auto/types.d.ts +57 -0
  158. package/dist/auto/types.d.ts.map +1 -0
  159. package/dist/auto/types.js +2 -0
  160. package/dist/auto/types.js.map +1 -0
  161. package/dist/awareness/tick-engine.d.ts +73 -0
  162. package/dist/awareness/tick-engine.d.ts.map +1 -0
  163. package/dist/awareness/tick-engine.js +156 -0
  164. package/dist/awareness/tick-engine.js.map +1 -0
  165. package/dist/billing/credits.d.ts +213 -0
  166. package/dist/billing/credits.d.ts.map +1 -0
  167. package/dist/billing/credits.js +535 -0
  168. package/dist/billing/credits.js.map +1 -0
  169. package/dist/billing/meter.d.ts +43 -0
  170. package/dist/billing/meter.d.ts.map +1 -0
  171. package/dist/billing/meter.js +67 -0
  172. package/dist/billing/meter.js.map +1 -0
  173. package/dist/billing/plans.d.ts +34 -0
  174. package/dist/billing/plans.d.ts.map +1 -0
  175. package/dist/billing/plans.js +108 -0
  176. package/dist/billing/plans.js.map +1 -0
  177. package/dist/briefing/briefing-engine.d.ts +51 -0
  178. package/dist/briefing/briefing-engine.d.ts.map +1 -0
  179. package/dist/briefing/briefing-engine.js +289 -0
  180. package/dist/briefing/briefing-engine.js.map +1 -0
  181. package/dist/briefing/index.d.ts +3 -0
  182. package/dist/briefing/index.d.ts.map +1 -0
  183. package/dist/briefing/index.js +2 -0
  184. package/dist/briefing/index.js.map +1 -0
  185. package/dist/briefing/types.d.ts +49 -0
  186. package/dist/briefing/types.d.ts.map +1 -0
  187. package/dist/briefing/types.js +8 -0
  188. package/dist/briefing/types.js.map +1 -0
  189. package/dist/checkpoint/checkpoint-manager.d.ts +33 -0
  190. package/dist/checkpoint/checkpoint-manager.d.ts.map +1 -0
  191. package/dist/checkpoint/checkpoint-manager.js +144 -0
  192. package/dist/checkpoint/checkpoint-manager.js.map +1 -0
  193. package/dist/config/config.d.ts +10 -0
  194. package/dist/config/config.d.ts.map +1 -0
  195. package/dist/config/config.js +169 -0
  196. package/dist/config/config.js.map +1 -0
  197. package/dist/config/personality.d.ts +32 -0
  198. package/dist/config/personality.d.ts.map +1 -0
  199. package/dist/config/personality.js +96 -0
  200. package/dist/config/personality.js.map +1 -0
  201. package/dist/config/project-config.d.ts +17 -0
  202. package/dist/config/project-config.d.ts.map +1 -0
  203. package/dist/config/project-config.js +38 -0
  204. package/dist/config/project-config.js.map +1 -0
  205. package/dist/config/project.d.ts +155 -0
  206. package/dist/config/project.d.ts.map +1 -0
  207. package/dist/config/project.js +746 -0
  208. package/dist/config/project.js.map +1 -0
  209. package/dist/config/schema.d.ts +61 -0
  210. package/dist/config/schema.d.ts.map +1 -0
  211. package/dist/config/schema.js +11 -0
  212. package/dist/config/schema.js.map +1 -0
  213. package/dist/core/constants.d.ts +13 -0
  214. package/dist/core/constants.d.ts.map +1 -0
  215. package/dist/core/constants.js +15 -0
  216. package/dist/core/constants.js.map +1 -0
  217. package/dist/core/dates.d.ts +35 -0
  218. package/dist/core/dates.d.ts.map +1 -0
  219. package/dist/core/dates.js +45 -0
  220. package/dist/core/dates.js.map +1 -0
  221. package/dist/core/errors.d.ts +27 -0
  222. package/dist/core/errors.d.ts.map +1 -0
  223. package/dist/core/errors.js +172 -0
  224. package/dist/core/errors.js.map +1 -0
  225. package/dist/core/file-lock.d.ts +22 -0
  226. package/dist/core/file-lock.d.ts.map +1 -0
  227. package/dist/core/file-lock.js +120 -0
  228. package/dist/core/file-lock.js.map +1 -0
  229. package/dist/core/logger.d.ts +9 -0
  230. package/dist/core/logger.d.ts.map +1 -0
  231. package/dist/core/logger.js +32 -0
  232. package/dist/core/logger.js.map +1 -0
  233. package/dist/core/types.d.ts +157 -0
  234. package/dist/core/types.d.ts.map +1 -0
  235. package/dist/core/types.js +13 -0
  236. package/dist/core/types.js.map +1 -0
  237. package/dist/creative/index.d.ts +50 -0
  238. package/dist/creative/index.d.ts.map +1 -0
  239. package/dist/creative/index.js +178 -0
  240. package/dist/creative/index.js.map +1 -0
  241. package/dist/dataset/capture.d.ts +15 -0
  242. package/dist/dataset/capture.d.ts.map +1 -0
  243. package/dist/dataset/capture.js +203 -0
  244. package/dist/dataset/capture.js.map +1 -0
  245. package/dist/dataset/config.d.ts +33 -0
  246. package/dist/dataset/config.d.ts.map +1 -0
  247. package/dist/dataset/config.js +60 -0
  248. package/dist/dataset/config.js.map +1 -0
  249. package/dist/dataset/consumer.d.ts +33 -0
  250. package/dist/dataset/consumer.d.ts.map +1 -0
  251. package/dist/dataset/consumer.js +97 -0
  252. package/dist/dataset/consumer.js.map +1 -0
  253. package/dist/dataset/emitter.d.ts +153 -0
  254. package/dist/dataset/emitter.d.ts.map +1 -0
  255. package/dist/dataset/emitter.js +157 -0
  256. package/dist/dataset/emitter.js.map +1 -0
  257. package/dist/dataset/events.d.ts +360 -0
  258. package/dist/dataset/events.d.ts.map +1 -0
  259. package/dist/dataset/events.js +30 -0
  260. package/dist/dataset/events.js.map +1 -0
  261. package/dist/dataset/generation-emit.d.ts +72 -0
  262. package/dist/dataset/generation-emit.d.ts.map +1 -0
  263. package/dist/dataset/generation-emit.js +94 -0
  264. package/dist/dataset/generation-emit.js.map +1 -0
  265. package/dist/dataset/redactor.d.ts +22 -0
  266. package/dist/dataset/redactor.d.ts.map +1 -0
  267. package/dist/dataset/redactor.js +68 -0
  268. package/dist/dataset/redactor.js.map +1 -0
  269. package/dist/dataset/routing.d.ts +17 -0
  270. package/dist/dataset/routing.d.ts.map +1 -0
  271. package/dist/dataset/routing.js +77 -0
  272. package/dist/dataset/routing.js.map +1 -0
  273. package/dist/dataset/summarizers.d.ts +47 -0
  274. package/dist/dataset/summarizers.d.ts.map +1 -0
  275. package/dist/dataset/summarizers.js +98 -0
  276. package/dist/dataset/summarizers.js.map +1 -0
  277. package/dist/dataset/verification.d.ts +29 -0
  278. package/dist/dataset/verification.d.ts.map +1 -0
  279. package/dist/dataset/verification.js +70 -0
  280. package/dist/dataset/verification.js.map +1 -0
  281. package/dist/desktop/budget.d.ts +66 -0
  282. package/dist/desktop/budget.d.ts.map +1 -0
  283. package/dist/desktop/budget.js +159 -0
  284. package/dist/desktop/budget.js.map +1 -0
  285. package/dist/desktop/capability.d.ts +28 -0
  286. package/dist/desktop/capability.d.ts.map +1 -0
  287. package/dist/desktop/capability.js +39 -0
  288. package/dist/desktop/capability.js.map +1 -0
  289. package/dist/desktop/conductor.d.ts +104 -0
  290. package/dist/desktop/conductor.d.ts.map +1 -0
  291. package/dist/desktop/conductor.js +876 -0
  292. package/dist/desktop/conductor.js.map +1 -0
  293. package/dist/desktop/fork-points.d.ts +56 -0
  294. package/dist/desktop/fork-points.d.ts.map +1 -0
  295. package/dist/desktop/fork-points.js +138 -0
  296. package/dist/desktop/fork-points.js.map +1 -0
  297. package/dist/desktop/index.d.ts +22 -0
  298. package/dist/desktop/index.d.ts.map +1 -0
  299. package/dist/desktop/index.js +21 -0
  300. package/dist/desktop/index.js.map +1 -0
  301. package/dist/desktop/perception.d.ts +24 -0
  302. package/dist/desktop/perception.d.ts.map +1 -0
  303. package/dist/desktop/perception.js +106 -0
  304. package/dist/desktop/perception.js.map +1 -0
  305. package/dist/desktop/personas.d.ts +30 -0
  306. package/dist/desktop/personas.d.ts.map +1 -0
  307. package/dist/desktop/personas.js +206 -0
  308. package/dist/desktop/personas.js.map +1 -0
  309. package/dist/desktop/safety.d.ts +86 -0
  310. package/dist/desktop/safety.d.ts.map +1 -0
  311. package/dist/desktop/safety.js +272 -0
  312. package/dist/desktop/safety.js.map +1 -0
  313. package/dist/desktop/screen-key.d.ts +54 -0
  314. package/dist/desktop/screen-key.d.ts.map +1 -0
  315. package/dist/desktop/screen-key.js +187 -0
  316. package/dist/desktop/screen-key.js.map +1 -0
  317. package/dist/desktop/types.d.ts +141 -0
  318. package/dist/desktop/types.d.ts.map +1 -0
  319. package/dist/desktop/types.js +8 -0
  320. package/dist/desktop/types.js.map +1 -0
  321. package/dist/docs/adapter.d.ts +71 -0
  322. package/dist/docs/adapter.d.ts.map +1 -0
  323. package/dist/docs/adapter.js +75 -0
  324. package/dist/docs/adapter.js.map +1 -0
  325. package/dist/docs/content/concepts.d.ts +3 -0
  326. package/dist/docs/content/concepts.d.ts.map +1 -0
  327. package/dist/docs/content/concepts.js +304 -0
  328. package/dist/docs/content/concepts.js.map +1 -0
  329. package/dist/docs/content/features.d.ts +3 -0
  330. package/dist/docs/content/features.d.ts.map +1 -0
  331. package/dist/docs/content/features.js +275 -0
  332. package/dist/docs/content/features.js.map +1 -0
  333. package/dist/docs/content/index.d.ts +3 -0
  334. package/dist/docs/content/index.d.ts.map +1 -0
  335. package/dist/docs/content/index.js +15 -0
  336. package/dist/docs/content/index.js.map +1 -0
  337. package/dist/docs/content/reference.d.ts +3 -0
  338. package/dist/docs/content/reference.d.ts.map +1 -0
  339. package/dist/docs/content/reference.js +220 -0
  340. package/dist/docs/content/reference.js.map +1 -0
  341. package/dist/docs/content/start.d.ts +3 -0
  342. package/dist/docs/content/start.d.ts.map +1 -0
  343. package/dist/docs/content/start.js +203 -0
  344. package/dist/docs/content/start.js.map +1 -0
  345. package/dist/docs/content/troubleshooting.d.ts +3 -0
  346. package/dist/docs/content/troubleshooting.d.ts.map +1 -0
  347. package/dist/docs/content/troubleshooting.js +98 -0
  348. package/dist/docs/content/troubleshooting.js.map +1 -0
  349. package/dist/docs/corpus.d.ts +7 -0
  350. package/dist/docs/corpus.d.ts.map +1 -0
  351. package/dist/docs/corpus.js +18 -0
  352. package/dist/docs/corpus.js.map +1 -0
  353. package/dist/docs/data/capabilities.d.ts +24 -0
  354. package/dist/docs/data/capabilities.d.ts.map +1 -0
  355. package/dist/docs/data/capabilities.js +63 -0
  356. package/dist/docs/data/capabilities.js.map +1 -0
  357. package/dist/docs/data/modes.d.ts +11 -0
  358. package/dist/docs/data/modes.d.ts.map +1 -0
  359. package/dist/docs/data/modes.js +60 -0
  360. package/dist/docs/data/modes.js.map +1 -0
  361. package/dist/docs/data/permissions.d.ts +12 -0
  362. package/dist/docs/data/permissions.d.ts.map +1 -0
  363. package/dist/docs/data/permissions.js +62 -0
  364. package/dist/docs/data/permissions.js.map +1 -0
  365. package/dist/docs/data/personas.d.ts +13 -0
  366. package/dist/docs/data/personas.d.ts.map +1 -0
  367. package/dist/docs/data/personas.js +42 -0
  368. package/dist/docs/data/personas.js.map +1 -0
  369. package/dist/docs/data/providers.d.ts +21 -0
  370. package/dist/docs/data/providers.d.ts.map +1 -0
  371. package/dist/docs/data/providers.js +210 -0
  372. package/dist/docs/data/providers.js.map +1 -0
  373. package/dist/docs/data/shortcuts.d.ts +9 -0
  374. package/dist/docs/data/shortcuts.d.ts.map +1 -0
  375. package/dist/docs/data/shortcuts.js +18 -0
  376. package/dist/docs/data/shortcuts.js.map +1 -0
  377. package/dist/docs/data/tasks.d.ts +17 -0
  378. package/dist/docs/data/tasks.d.ts.map +1 -0
  379. package/dist/docs/data/tasks.js +64 -0
  380. package/dist/docs/data/tasks.js.map +1 -0
  381. package/dist/docs/data/tools.d.ts +12 -0
  382. package/dist/docs/data/tools.d.ts.map +1 -0
  383. package/dist/docs/data/tools.js +100 -0
  384. package/dist/docs/data/tools.js.map +1 -0
  385. package/dist/docs/filter.d.ts +35 -0
  386. package/dist/docs/filter.d.ts.map +1 -0
  387. package/dist/docs/filter.js +68 -0
  388. package/dist/docs/filter.js.map +1 -0
  389. package/dist/docs/i18n/translations.d.ts +2 -0
  390. package/dist/docs/i18n/translations.d.ts.map +1 -0
  391. package/dist/docs/i18n/translations.js +4 -0
  392. package/dist/docs/i18n/translations.js.map +1 -0
  393. package/dist/docs/i18n.d.ts +15 -0
  394. package/dist/docs/i18n.d.ts.map +1 -0
  395. package/dist/docs/i18n.js +88 -0
  396. package/dist/docs/i18n.js.map +1 -0
  397. package/dist/docs/index.d.ts +16 -0
  398. package/dist/docs/index.d.ts.map +1 -0
  399. package/dist/docs/index.js +15 -0
  400. package/dist/docs/index.js.map +1 -0
  401. package/dist/docs/product-knowledge.d.ts +45 -0
  402. package/dist/docs/product-knowledge.d.ts.map +1 -0
  403. package/dist/docs/product-knowledge.js +147 -0
  404. package/dist/docs/product-knowledge.js.map +1 -0
  405. package/dist/docs/types.d.ts +138 -0
  406. package/dist/docs/types.d.ts.map +1 -0
  407. package/dist/docs/types.js +14 -0
  408. package/dist/docs/types.js.map +1 -0
  409. package/dist/events/event-detector.d.ts +39 -0
  410. package/dist/events/event-detector.d.ts.map +1 -0
  411. package/dist/events/event-detector.js +223 -0
  412. package/dist/events/event-detector.js.map +1 -0
  413. package/dist/events/index.d.ts +3 -0
  414. package/dist/events/index.d.ts.map +1 -0
  415. package/dist/events/index.js +2 -0
  416. package/dist/events/index.js.map +1 -0
  417. package/dist/events/types.d.ts +35 -0
  418. package/dist/events/types.d.ts.map +1 -0
  419. package/dist/events/types.js +8 -0
  420. package/dist/events/types.js.map +1 -0
  421. package/dist/exercises/index.d.ts +249 -0
  422. package/dist/exercises/index.d.ts.map +1 -0
  423. package/dist/exercises/index.js +181 -0
  424. package/dist/exercises/index.js.map +1 -0
  425. package/dist/health/aisles.d.ts +33 -0
  426. package/dist/health/aisles.d.ts.map +1 -0
  427. package/dist/health/aisles.js +417 -0
  428. package/dist/health/aisles.js.map +1 -0
  429. package/dist/health/cooking.d.ts +13 -0
  430. package/dist/health/cooking.d.ts.map +1 -0
  431. package/dist/health/cooking.js +19 -0
  432. package/dist/health/cooking.js.map +1 -0
  433. package/dist/health/duplicate.d.ts +47 -0
  434. package/dist/health/duplicate.d.ts.map +1 -0
  435. package/dist/health/duplicate.js +136 -0
  436. package/dist/health/duplicate.js.map +1 -0
  437. package/dist/health/index.d.ts +7 -0
  438. package/dist/health/index.d.ts.map +1 -0
  439. package/dist/health/index.js +10 -0
  440. package/dist/health/index.js.map +1 -0
  441. package/dist/health/node-store.d.ts +30 -0
  442. package/dist/health/node-store.d.ts.map +1 -0
  443. package/dist/health/node-store.js +250 -0
  444. package/dist/health/node-store.js.map +1 -0
  445. package/dist/health/prep.d.ts +86 -0
  446. package/dist/health/prep.d.ts.map +1 -0
  447. package/dist/health/prep.js +152 -0
  448. package/dist/health/prep.js.map +1 -0
  449. package/dist/health/profile-fields.d.ts +46 -0
  450. package/dist/health/profile-fields.d.ts.map +1 -0
  451. package/dist/health/profile-fields.js +194 -0
  452. package/dist/health/profile-fields.js.map +1 -0
  453. package/dist/health/session-store.d.ts +30 -0
  454. package/dist/health/session-store.d.ts.map +1 -0
  455. package/dist/health/session-store.js +20 -0
  456. package/dist/health/session-store.js.map +1 -0
  457. package/dist/health/session-summary.d.ts +10 -0
  458. package/dist/health/session-summary.d.ts.map +1 -0
  459. package/dist/health/session-summary.js +106 -0
  460. package/dist/health/session-summary.js.map +1 -0
  461. package/dist/health/session-types.d.ts +74 -0
  462. package/dist/health/session-types.d.ts.map +1 -0
  463. package/dist/health/session-types.js +53 -0
  464. package/dist/health/session-types.js.map +1 -0
  465. package/dist/health/shopping-list.d.ts +226 -0
  466. package/dist/health/shopping-list.d.ts.map +1 -0
  467. package/dist/health/shopping-list.js +432 -0
  468. package/dist/health/shopping-list.js.map +1 -0
  469. package/dist/health/starters.d.ts +155 -0
  470. package/dist/health/starters.d.ts.map +1 -0
  471. package/dist/health/starters.js +270 -0
  472. package/dist/health/starters.js.map +1 -0
  473. package/dist/health/store.d.ts +59 -0
  474. package/dist/health/store.d.ts.map +1 -0
  475. package/dist/health/store.js +17 -0
  476. package/dist/health/store.js.map +1 -0
  477. package/dist/health/types.d.ts +213 -0
  478. package/dist/health/types.d.ts.map +1 -0
  479. package/dist/health/types.js +11 -0
  480. package/dist/health/types.js.map +1 -0
  481. package/dist/history/conversation-title.d.ts +67 -0
  482. package/dist/history/conversation-title.d.ts.map +1 -0
  483. package/dist/history/conversation-title.js +184 -0
  484. package/dist/history/conversation-title.js.map +1 -0
  485. package/dist/history/history-manager.d.ts +50 -0
  486. package/dist/history/history-manager.d.ts.map +1 -0
  487. package/dist/history/history-manager.js +168 -0
  488. package/dist/history/history-manager.js.map +1 -0
  489. package/dist/history/storage.d.ts +41 -0
  490. package/dist/history/storage.d.ts.map +1 -0
  491. package/dist/history/storage.js +133 -0
  492. package/dist/history/storage.js.map +1 -0
  493. package/dist/i18n/index.d.ts +41 -0
  494. package/dist/i18n/index.d.ts.map +1 -0
  495. package/dist/i18n/index.js +188 -0
  496. package/dist/i18n/index.js.map +1 -0
  497. package/dist/i18n/keep-english.d.ts +12 -0
  498. package/dist/i18n/keep-english.d.ts.map +1 -0
  499. package/dist/i18n/keep-english.js +351 -0
  500. package/dist/i18n/keep-english.js.map +1 -0
  501. package/dist/i18n/locales/ar.d.ts +4 -0
  502. package/dist/i18n/locales/ar.d.ts.map +1 -0
  503. package/dist/i18n/locales/ar.js +4143 -0
  504. package/dist/i18n/locales/ar.js.map +1 -0
  505. package/dist/i18n/locales/de.d.ts +4 -0
  506. package/dist/i18n/locales/de.d.ts.map +1 -0
  507. package/dist/i18n/locales/de.js +4133 -0
  508. package/dist/i18n/locales/de.js.map +1 -0
  509. package/dist/i18n/locales/en.d.ts +4068 -0
  510. package/dist/i18n/locales/en.d.ts.map +1 -0
  511. package/dist/i18n/locales/en.js +4347 -0
  512. package/dist/i18n/locales/en.js.map +1 -0
  513. package/dist/i18n/locales/es.d.ts +4 -0
  514. package/dist/i18n/locales/es.d.ts.map +1 -0
  515. package/dist/i18n/locales/es.js +4128 -0
  516. package/dist/i18n/locales/es.js.map +1 -0
  517. package/dist/i18n/locales/fr.d.ts +4 -0
  518. package/dist/i18n/locales/fr.d.ts.map +1 -0
  519. package/dist/i18n/locales/fr.js +4140 -0
  520. package/dist/i18n/locales/fr.js.map +1 -0
  521. package/dist/i18n/locales/hi.d.ts +4 -0
  522. package/dist/i18n/locales/hi.d.ts.map +1 -0
  523. package/dist/i18n/locales/hi.js +4143 -0
  524. package/dist/i18n/locales/hi.js.map +1 -0
  525. package/dist/i18n/locales/id.d.ts +4 -0
  526. package/dist/i18n/locales/id.d.ts.map +1 -0
  527. package/dist/i18n/locales/id.js +4142 -0
  528. package/dist/i18n/locales/id.js.map +1 -0
  529. package/dist/i18n/locales/it.d.ts +4 -0
  530. package/dist/i18n/locales/it.d.ts.map +1 -0
  531. package/dist/i18n/locales/it.js +4143 -0
  532. package/dist/i18n/locales/it.js.map +1 -0
  533. package/dist/i18n/locales/ja.d.ts +4 -0
  534. package/dist/i18n/locales/ja.d.ts.map +1 -0
  535. package/dist/i18n/locales/ja.js +4144 -0
  536. package/dist/i18n/locales/ja.js.map +1 -0
  537. package/dist/i18n/locales/ko.d.ts +4 -0
  538. package/dist/i18n/locales/ko.d.ts.map +1 -0
  539. package/dist/i18n/locales/ko.js +4143 -0
  540. package/dist/i18n/locales/ko.js.map +1 -0
  541. package/dist/i18n/locales/nl.d.ts +4 -0
  542. package/dist/i18n/locales/nl.d.ts.map +1 -0
  543. package/dist/i18n/locales/nl.js +4143 -0
  544. package/dist/i18n/locales/nl.js.map +1 -0
  545. package/dist/i18n/locales/pl.d.ts +4 -0
  546. package/dist/i18n/locales/pl.d.ts.map +1 -0
  547. package/dist/i18n/locales/pl.js +4133 -0
  548. package/dist/i18n/locales/pl.js.map +1 -0
  549. package/dist/i18n/locales/pt.d.ts +4 -0
  550. package/dist/i18n/locales/pt.d.ts.map +1 -0
  551. package/dist/i18n/locales/pt.js +4141 -0
  552. package/dist/i18n/locales/pt.js.map +1 -0
  553. package/dist/i18n/locales/ru.d.ts +4 -0
  554. package/dist/i18n/locales/ru.d.ts.map +1 -0
  555. package/dist/i18n/locales/ru.js +4143 -0
  556. package/dist/i18n/locales/ru.js.map +1 -0
  557. package/dist/i18n/locales/th.d.ts +4 -0
  558. package/dist/i18n/locales/th.d.ts.map +1 -0
  559. package/dist/i18n/locales/th.js +4143 -0
  560. package/dist/i18n/locales/th.js.map +1 -0
  561. package/dist/i18n/locales/tr.d.ts +4 -0
  562. package/dist/i18n/locales/tr.d.ts.map +1 -0
  563. package/dist/i18n/locales/tr.js +4143 -0
  564. package/dist/i18n/locales/tr.js.map +1 -0
  565. package/dist/i18n/locales/uk.d.ts +4 -0
  566. package/dist/i18n/locales/uk.d.ts.map +1 -0
  567. package/dist/i18n/locales/uk.js +4133 -0
  568. package/dist/i18n/locales/uk.js.map +1 -0
  569. package/dist/i18n/locales/vi.d.ts +4 -0
  570. package/dist/i18n/locales/vi.d.ts.map +1 -0
  571. package/dist/i18n/locales/vi.js +4143 -0
  572. package/dist/i18n/locales/vi.js.map +1 -0
  573. package/dist/i18n/locales/zh-CN.d.ts +4 -0
  574. package/dist/i18n/locales/zh-CN.d.ts.map +1 -0
  575. package/dist/i18n/locales/zh-CN.js +4133 -0
  576. package/dist/i18n/locales/zh-CN.js.map +1 -0
  577. package/dist/i18n/locales/zh-TW.d.ts +4 -0
  578. package/dist/i18n/locales/zh-TW.d.ts.map +1 -0
  579. package/dist/i18n/locales/zh-TW.js +4143 -0
  580. package/dist/i18n/locales/zh-TW.js.map +1 -0
  581. package/dist/i18n/types.d.ts +5 -0
  582. package/dist/i18n/types.d.ts.map +1 -0
  583. package/dist/i18n/types.js +46 -0
  584. package/dist/i18n/types.js.map +1 -0
  585. package/dist/index.d.ts +142 -0
  586. package/dist/index.d.ts.map +1 -0
  587. package/dist/index.js +159 -0
  588. package/dist/index.js.map +1 -0
  589. package/dist/indexer/project-indexer.d.ts +66 -0
  590. package/dist/indexer/project-indexer.d.ts.map +1 -0
  591. package/dist/indexer/project-indexer.js +574 -0
  592. package/dist/indexer/project-indexer.js.map +1 -0
  593. package/dist/indexer/symbol-indexer.d.ts +56 -0
  594. package/dist/indexer/symbol-indexer.d.ts.map +1 -0
  595. package/dist/indexer/symbol-indexer.js +579 -0
  596. package/dist/indexer/symbol-indexer.js.map +1 -0
  597. package/dist/journal/journal-manager.d.ts +143 -0
  598. package/dist/journal/journal-manager.d.ts.map +1 -0
  599. package/dist/journal/journal-manager.js +472 -0
  600. package/dist/journal/journal-manager.js.map +1 -0
  601. package/dist/journal/platform-sync.d.ts +17 -0
  602. package/dist/journal/platform-sync.d.ts.map +1 -0
  603. package/dist/journal/platform-sync.js +21 -0
  604. package/dist/journal/platform-sync.js.map +1 -0
  605. package/dist/journal/types.d.ts +78 -0
  606. package/dist/journal/types.d.ts.map +1 -0
  607. package/dist/journal/types.js +74 -0
  608. package/dist/journal/types.js.map +1 -0
  609. package/dist/knowledge/index.d.ts +2 -0
  610. package/dist/knowledge/index.d.ts.map +1 -0
  611. package/dist/knowledge/index.js +2 -0
  612. package/dist/knowledge/index.js.map +1 -0
  613. package/dist/learning/index.d.ts +6 -0
  614. package/dist/learning/index.d.ts.map +1 -0
  615. package/dist/learning/index.js +9 -0
  616. package/dist/learning/index.js.map +1 -0
  617. package/dist/learning/learner-context.d.ts +11 -0
  618. package/dist/learning/learner-context.d.ts.map +1 -0
  619. package/dist/learning/learner-context.js +57 -0
  620. package/dist/learning/learner-context.js.map +1 -0
  621. package/dist/learning/library-fork.d.ts +33 -0
  622. package/dist/learning/library-fork.d.ts.map +1 -0
  623. package/dist/learning/library-fork.js +92 -0
  624. package/dist/learning/library-fork.js.map +1 -0
  625. package/dist/learning/progression-export.d.ts +4 -0
  626. package/dist/learning/progression-export.d.ts.map +1 -0
  627. package/dist/learning/progression-export.js +21 -0
  628. package/dist/learning/progression-export.js.map +1 -0
  629. package/dist/learning/progression-markdown.d.ts +14 -0
  630. package/dist/learning/progression-markdown.d.ts.map +1 -0
  631. package/dist/learning/progression-markdown.js +73 -0
  632. package/dist/learning/progression-markdown.js.map +1 -0
  633. package/dist/learning/progression.d.ts +65 -0
  634. package/dist/learning/progression.d.ts.map +1 -0
  635. package/dist/learning/progression.js +186 -0
  636. package/dist/learning/progression.js.map +1 -0
  637. package/dist/memory/ambient-capture.d.ts +26 -0
  638. package/dist/memory/ambient-capture.d.ts.map +1 -0
  639. package/dist/memory/ambient-capture.js +174 -0
  640. package/dist/memory/ambient-capture.js.map +1 -0
  641. package/dist/memory/auto-extract.d.ts +54 -0
  642. package/dist/memory/auto-extract.d.ts.map +1 -0
  643. package/dist/memory/auto-extract.js +499 -0
  644. package/dist/memory/auto-extract.js.map +1 -0
  645. package/dist/memory/config.d.ts +47 -0
  646. package/dist/memory/config.d.ts.map +1 -0
  647. package/dist/memory/config.js +52 -0
  648. package/dist/memory/config.js.map +1 -0
  649. package/dist/memory/consolidation.d.ts +60 -0
  650. package/dist/memory/consolidation.d.ts.map +1 -0
  651. package/dist/memory/consolidation.js +311 -0
  652. package/dist/memory/consolidation.js.map +1 -0
  653. package/dist/memory/embedding-service.d.ts +50 -0
  654. package/dist/memory/embedding-service.d.ts.map +1 -0
  655. package/dist/memory/embedding-service.js +86 -0
  656. package/dist/memory/embedding-service.js.map +1 -0
  657. package/dist/memory/embedding-store.d.ts +31 -0
  658. package/dist/memory/embedding-store.d.ts.map +1 -0
  659. package/dist/memory/embedding-store.js +109 -0
  660. package/dist/memory/embedding-store.js.map +1 -0
  661. package/dist/memory/graph-engine.d.ts +145 -0
  662. package/dist/memory/graph-engine.d.ts.map +1 -0
  663. package/dist/memory/graph-engine.js +628 -0
  664. package/dist/memory/graph-engine.js.map +1 -0
  665. package/dist/memory/insights.d.ts +39 -0
  666. package/dist/memory/insights.d.ts.map +1 -0
  667. package/dist/memory/insights.js +138 -0
  668. package/dist/memory/insights.js.map +1 -0
  669. package/dist/memory/memory-agent.d.ts +93 -0
  670. package/dist/memory/memory-agent.d.ts.map +1 -0
  671. package/dist/memory/memory-agent.js +475 -0
  672. package/dist/memory/memory-agent.js.map +1 -0
  673. package/dist/memory/memory-manager.d.ts +232 -0
  674. package/dist/memory/memory-manager.d.ts.map +1 -0
  675. package/dist/memory/memory-manager.js +1633 -0
  676. package/dist/memory/memory-manager.js.map +1 -0
  677. package/dist/memory/migration-v3.d.ts +22 -0
  678. package/dist/memory/migration-v3.d.ts.map +1 -0
  679. package/dist/memory/migration-v3.js +193 -0
  680. package/dist/memory/migration-v3.js.map +1 -0
  681. package/dist/memory/mode-recall.d.ts +20 -0
  682. package/dist/memory/mode-recall.d.ts.map +1 -0
  683. package/dist/memory/mode-recall.js +104 -0
  684. package/dist/memory/mode-recall.js.map +1 -0
  685. package/dist/memory/patterns.d.ts +92 -0
  686. package/dist/memory/patterns.d.ts.map +1 -0
  687. package/dist/memory/patterns.js +389 -0
  688. package/dist/memory/patterns.js.map +1 -0
  689. package/dist/memory/platform-sync.d.ts +54 -0
  690. package/dist/memory/platform-sync.d.ts.map +1 -0
  691. package/dist/memory/platform-sync.js +176 -0
  692. package/dist/memory/platform-sync.js.map +1 -0
  693. package/dist/memory/procedural.d.ts +47 -0
  694. package/dist/memory/procedural.d.ts.map +1 -0
  695. package/dist/memory/procedural.js +182 -0
  696. package/dist/memory/procedural.js.map +1 -0
  697. package/dist/memory/project-brain.d.ts +27 -0
  698. package/dist/memory/project-brain.d.ts.map +1 -0
  699. package/dist/memory/project-brain.js +158 -0
  700. package/dist/memory/project-brain.js.map +1 -0
  701. package/dist/memory/self-improvement.d.ts +45 -0
  702. package/dist/memory/self-improvement.d.ts.map +1 -0
  703. package/dist/memory/self-improvement.js +178 -0
  704. package/dist/memory/self-improvement.js.map +1 -0
  705. package/dist/memory/tfidf.d.ts +71 -0
  706. package/dist/memory/tfidf.d.ts.map +1 -0
  707. package/dist/memory/tfidf.js +223 -0
  708. package/dist/memory/tfidf.js.map +1 -0
  709. package/dist/memory/training-export.d.ts +51 -0
  710. package/dist/memory/training-export.d.ts.map +1 -0
  711. package/dist/memory/training-export.js +122 -0
  712. package/dist/memory/training-export.js.map +1 -0
  713. package/dist/memory/types.d.ts +380 -0
  714. package/dist/memory/types.d.ts.map +1 -0
  715. package/dist/memory/types.js +99 -0
  716. package/dist/memory/types.js.map +1 -0
  717. package/dist/news/index.d.ts +163 -0
  718. package/dist/news/index.d.ts.map +1 -0
  719. package/dist/news/index.js +184 -0
  720. package/dist/news/index.js.map +1 -0
  721. package/dist/papers/arxiv-client.d.ts +21 -0
  722. package/dist/papers/arxiv-client.d.ts.map +1 -0
  723. package/dist/papers/arxiv-client.js +130 -0
  724. package/dist/papers/arxiv-client.js.map +1 -0
  725. package/dist/papers/crossref-client.d.ts +3 -0
  726. package/dist/papers/crossref-client.d.ts.map +1 -0
  727. package/dist/papers/crossref-client.js +96 -0
  728. package/dist/papers/crossref-client.js.map +1 -0
  729. package/dist/papers/discipline.d.ts +19 -0
  730. package/dist/papers/discipline.d.ts.map +1 -0
  731. package/dist/papers/discipline.js +96 -0
  732. package/dist/papers/discipline.js.map +1 -0
  733. package/dist/papers/index.d.ts +10 -0
  734. package/dist/papers/index.d.ts.map +1 -0
  735. package/dist/papers/index.js +7 -0
  736. package/dist/papers/index.js.map +1 -0
  737. package/dist/papers/openalex-client.d.ts +13 -0
  738. package/dist/papers/openalex-client.d.ts.map +1 -0
  739. package/dist/papers/openalex-client.js +206 -0
  740. package/dist/papers/openalex-client.js.map +1 -0
  741. package/dist/papers/paper-fetcher.d.ts +37 -0
  742. package/dist/papers/paper-fetcher.d.ts.map +1 -0
  743. package/dist/papers/paper-fetcher.js +75 -0
  744. package/dist/papers/paper-fetcher.js.map +1 -0
  745. package/dist/papers/types.d.ts +80 -0
  746. package/dist/papers/types.d.ts.map +1 -0
  747. package/dist/papers/types.js +25 -0
  748. package/dist/papers/types.js.map +1 -0
  749. package/dist/personas/conductor.d.ts +184 -0
  750. package/dist/personas/conductor.d.ts.map +1 -0
  751. package/dist/personas/conductor.js +871 -0
  752. package/dist/personas/conductor.js.map +1 -0
  753. package/dist/personas/definitions.d.ts +48 -0
  754. package/dist/personas/definitions.d.ts.map +1 -0
  755. package/dist/personas/definitions.js +828 -0
  756. package/dist/personas/definitions.js.map +1 -0
  757. package/dist/personas/index.d.ts +4 -0
  758. package/dist/personas/index.d.ts.map +1 -0
  759. package/dist/personas/index.js +3 -0
  760. package/dist/personas/index.js.map +1 -0
  761. package/dist/personas/types.d.ts +148 -0
  762. package/dist/personas/types.d.ts.map +1 -0
  763. package/dist/personas/types.js +5 -0
  764. package/dist/personas/types.js.map +1 -0
  765. package/dist/personas/verification-matrix.d.ts +44 -0
  766. package/dist/personas/verification-matrix.d.ts.map +1 -0
  767. package/dist/personas/verification-matrix.js +232 -0
  768. package/dist/personas/verification-matrix.js.map +1 -0
  769. package/dist/portability/bundle.d.ts +80 -0
  770. package/dist/portability/bundle.d.ts.map +1 -0
  771. package/dist/portability/bundle.js +155 -0
  772. package/dist/portability/bundle.js.map +1 -0
  773. package/dist/portability/crypto.d.ts +42 -0
  774. package/dist/portability/crypto.d.ts.map +1 -0
  775. package/dist/portability/crypto.js +89 -0
  776. package/dist/portability/crypto.js.map +1 -0
  777. package/dist/portability/data-types.d.ts +54 -0
  778. package/dist/portability/data-types.d.ts.map +1 -0
  779. package/dist/portability/data-types.js +281 -0
  780. package/dist/portability/data-types.js.map +1 -0
  781. package/dist/portability/index.d.ts +35 -0
  782. package/dist/portability/index.d.ts.map +1 -0
  783. package/dist/portability/index.js +41 -0
  784. package/dist/portability/index.js.map +1 -0
  785. package/dist/providers/base-provider.d.ts +65 -0
  786. package/dist/providers/base-provider.d.ts.map +1 -0
  787. package/dist/providers/base-provider.js +305 -0
  788. package/dist/providers/base-provider.js.map +1 -0
  789. package/dist/providers/catalog.d.ts +25 -0
  790. package/dist/providers/catalog.d.ts.map +1 -0
  791. package/dist/providers/catalog.js +53 -0
  792. package/dist/providers/catalog.js.map +1 -0
  793. package/dist/providers/deepseek/index.d.ts +9 -0
  794. package/dist/providers/deepseek/index.d.ts.map +1 -0
  795. package/dist/providers/deepseek/index.js +19 -0
  796. package/dist/providers/deepseek/index.js.map +1 -0
  797. package/dist/providers/deepseek/models.d.ts +3 -0
  798. package/dist/providers/deepseek/models.d.ts.map +1 -0
  799. package/dist/providers/deepseek/models.js +56 -0
  800. package/dist/providers/deepseek/models.js.map +1 -0
  801. package/dist/providers/generic/index.d.ts +25 -0
  802. package/dist/providers/generic/index.d.ts.map +1 -0
  803. package/dist/providers/generic/index.js +51 -0
  804. package/dist/providers/generic/index.js.map +1 -0
  805. package/dist/providers/generic/models.d.ts +3 -0
  806. package/dist/providers/generic/models.d.ts.map +1 -0
  807. package/dist/providers/generic/models.js +13 -0
  808. package/dist/providers/generic/models.js.map +1 -0
  809. package/dist/providers/health-tracker.d.ts +44 -0
  810. package/dist/providers/health-tracker.d.ts.map +1 -0
  811. package/dist/providers/health-tracker.js +110 -0
  812. package/dist/providers/health-tracker.js.map +1 -0
  813. package/dist/providers/kimi/index.d.ts +9 -0
  814. package/dist/providers/kimi/index.d.ts.map +1 -0
  815. package/dist/providers/kimi/index.js +13 -0
  816. package/dist/providers/kimi/index.js.map +1 -0
  817. package/dist/providers/kimi/models.d.ts +3 -0
  818. package/dist/providers/kimi/models.d.ts.map +1 -0
  819. package/dist/providers/kimi/models.js +60 -0
  820. package/dist/providers/kimi/models.js.map +1 -0
  821. package/dist/providers/minimax/index.d.ts +9 -0
  822. package/dist/providers/minimax/index.d.ts.map +1 -0
  823. package/dist/providers/minimax/index.js +13 -0
  824. package/dist/providers/minimax/index.js.map +1 -0
  825. package/dist/providers/minimax/models.d.ts +3 -0
  826. package/dist/providers/minimax/models.d.ts.map +1 -0
  827. package/dist/providers/minimax/models.js +65 -0
  828. package/dist/providers/minimax/models.js.map +1 -0
  829. package/dist/providers/mistral/index.d.ts +11 -0
  830. package/dist/providers/mistral/index.d.ts.map +1 -0
  831. package/dist/providers/mistral/index.js +24 -0
  832. package/dist/providers/mistral/index.js.map +1 -0
  833. package/dist/providers/mistral/models.d.ts +3 -0
  834. package/dist/providers/mistral/models.d.ts.map +1 -0
  835. package/dist/providers/mistral/models.js +65 -0
  836. package/dist/providers/mistral/models.js.map +1 -0
  837. package/dist/providers/nvidia/index.d.ts +16 -0
  838. package/dist/providers/nvidia/index.d.ts.map +1 -0
  839. package/dist/providers/nvidia/index.js +20 -0
  840. package/dist/providers/nvidia/index.js.map +1 -0
  841. package/dist/providers/nvidia/models.d.ts +3 -0
  842. package/dist/providers/nvidia/models.d.ts.map +1 -0
  843. package/dist/providers/nvidia/models.js +56 -0
  844. package/dist/providers/nvidia/models.js.map +1 -0
  845. package/dist/providers/platform/index.d.ts +10 -0
  846. package/dist/providers/platform/index.d.ts.map +1 -0
  847. package/dist/providers/platform/index.js +16 -0
  848. package/dist/providers/platform/index.js.map +1 -0
  849. package/dist/providers/platform/models.d.ts +3 -0
  850. package/dist/providers/platform/models.d.ts.map +1 -0
  851. package/dist/providers/platform/models.js +233 -0
  852. package/dist/providers/platform/models.js.map +1 -0
  853. package/dist/providers/provider-registry.d.ts +44 -0
  854. package/dist/providers/provider-registry.d.ts.map +1 -0
  855. package/dist/providers/provider-registry.js +197 -0
  856. package/dist/providers/provider-registry.js.map +1 -0
  857. package/dist/providers/qwen/index.d.ts +9 -0
  858. package/dist/providers/qwen/index.d.ts.map +1 -0
  859. package/dist/providers/qwen/index.js +13 -0
  860. package/dist/providers/qwen/index.js.map +1 -0
  861. package/dist/providers/qwen/models.d.ts +3 -0
  862. package/dist/providers/qwen/models.d.ts.map +1 -0
  863. package/dist/providers/qwen/models.js +164 -0
  864. package/dist/providers/qwen/models.js.map +1 -0
  865. package/dist/providers/request-shaping/index.d.ts +29 -0
  866. package/dist/providers/request-shaping/index.d.ts.map +1 -0
  867. package/dist/providers/request-shaping/index.js +31 -0
  868. package/dist/providers/request-shaping/index.js.map +1 -0
  869. package/dist/providers/request-shaping/messages.d.ts +61 -0
  870. package/dist/providers/request-shaping/messages.d.ts.map +1 -0
  871. package/dist/providers/request-shaping/messages.js +114 -0
  872. package/dist/providers/request-shaping/messages.js.map +1 -0
  873. package/dist/providers/request-shaping/model-ids.d.ts +44 -0
  874. package/dist/providers/request-shaping/model-ids.d.ts.map +1 -0
  875. package/dist/providers/request-shaping/model-ids.js +96 -0
  876. package/dist/providers/request-shaping/model-ids.js.map +1 -0
  877. package/dist/providers/request-shaping/params.d.ts +38 -0
  878. package/dist/providers/request-shaping/params.d.ts.map +1 -0
  879. package/dist/providers/request-shaping/params.js +79 -0
  880. package/dist/providers/request-shaping/params.js.map +1 -0
  881. package/dist/providers/resilient-provider.d.ts +44 -0
  882. package/dist/providers/resilient-provider.d.ts.map +1 -0
  883. package/dist/providers/resilient-provider.js +141 -0
  884. package/dist/providers/resilient-provider.js.map +1 -0
  885. package/dist/providers/tencent/index.d.ts +19 -0
  886. package/dist/providers/tencent/index.d.ts.map +1 -0
  887. package/dist/providers/tencent/index.js +50 -0
  888. package/dist/providers/tencent/index.js.map +1 -0
  889. package/dist/providers/tencent/models.d.ts +3 -0
  890. package/dist/providers/tencent/models.d.ts.map +1 -0
  891. package/dist/providers/tencent/models.js +29 -0
  892. package/dist/providers/tencent/models.js.map +1 -0
  893. package/dist/providers/types.d.ts +98 -0
  894. package/dist/providers/types.d.ts.map +1 -0
  895. package/dist/providers/types.js +2 -0
  896. package/dist/providers/types.js.map +1 -0
  897. package/dist/providers/xiaomi/index.d.ts +9 -0
  898. package/dist/providers/xiaomi/index.d.ts.map +1 -0
  899. package/dist/providers/xiaomi/index.js +16 -0
  900. package/dist/providers/xiaomi/index.js.map +1 -0
  901. package/dist/providers/xiaomi/models.d.ts +3 -0
  902. package/dist/providers/xiaomi/models.d.ts.map +1 -0
  903. package/dist/providers/xiaomi/models.js +37 -0
  904. package/dist/providers/xiaomi/models.js.map +1 -0
  905. package/dist/providers/zhipu/index.d.ts +11 -0
  906. package/dist/providers/zhipu/index.d.ts.map +1 -0
  907. package/dist/providers/zhipu/index.js +44 -0
  908. package/dist/providers/zhipu/index.js.map +1 -0
  909. package/dist/providers/zhipu/models.d.ts +3 -0
  910. package/dist/providers/zhipu/models.d.ts.map +1 -0
  911. package/dist/providers/zhipu/models.js +37 -0
  912. package/dist/providers/zhipu/models.js.map +1 -0
  913. package/dist/recipes/index.d.ts +215 -0
  914. package/dist/recipes/index.d.ts.map +1 -0
  915. package/dist/recipes/index.js +232 -0
  916. package/dist/recipes/index.js.map +1 -0
  917. package/dist/remote/client.d.ts +67 -0
  918. package/dist/remote/client.d.ts.map +1 -0
  919. package/dist/remote/client.js +202 -0
  920. package/dist/remote/client.js.map +1 -0
  921. package/dist/remote/index.d.ts +9 -0
  922. package/dist/remote/index.d.ts.map +1 -0
  923. package/dist/remote/index.js +7 -0
  924. package/dist/remote/index.js.map +1 -0
  925. package/dist/remote/schema.d.ts +152 -0
  926. package/dist/remote/schema.d.ts.map +1 -0
  927. package/dist/remote/schema.js +26 -0
  928. package/dist/remote/schema.js.map +1 -0
  929. package/dist/security/index.d.ts +6 -0
  930. package/dist/security/index.d.ts.map +1 -0
  931. package/dist/security/index.js +5 -0
  932. package/dist/security/index.js.map +1 -0
  933. package/dist/security/scanner.d.ts +67 -0
  934. package/dist/security/scanner.d.ts.map +1 -0
  935. package/dist/security/scanner.js +422 -0
  936. package/dist/security/scanner.js.map +1 -0
  937. package/dist/social/day-plan.d.ts +38 -0
  938. package/dist/social/day-plan.d.ts.map +1 -0
  939. package/dist/social/day-plan.js +16 -0
  940. package/dist/social/day-plan.js.map +1 -0
  941. package/dist/social/index.d.ts +290 -0
  942. package/dist/social/index.d.ts.map +1 -0
  943. package/dist/social/index.js +96 -0
  944. package/dist/social/index.js.map +1 -0
  945. package/dist/tasks/platform-sync.d.ts +22 -0
  946. package/dist/tasks/platform-sync.d.ts.map +1 -0
  947. package/dist/tasks/platform-sync.js +132 -0
  948. package/dist/tasks/platform-sync.js.map +1 -0
  949. package/dist/tasks/task-manager.d.ts +164 -0
  950. package/dist/tasks/task-manager.d.ts.map +1 -0
  951. package/dist/tasks/task-manager.js +643 -0
  952. package/dist/tasks/task-manager.js.map +1 -0
  953. package/dist/tasks/types.d.ts +105 -0
  954. package/dist/tasks/types.d.ts.map +1 -0
  955. package/dist/tasks/types.js +23 -0
  956. package/dist/tasks/types.js.map +1 -0
  957. package/dist/tools/analyze-architecture.d.ts +17 -0
  958. package/dist/tools/analyze-architecture.d.ts.map +1 -0
  959. package/dist/tools/analyze-architecture.js +316 -0
  960. package/dist/tools/analyze-architecture.js.map +1 -0
  961. package/dist/tools/apply-plan.d.ts +12 -0
  962. package/dist/tools/apply-plan.d.ts.map +1 -0
  963. package/dist/tools/apply-plan.js +119 -0
  964. package/dist/tools/apply-plan.js.map +1 -0
  965. package/dist/tools/ask-user.d.ts +11 -0
  966. package/dist/tools/ask-user.d.ts.map +1 -0
  967. package/dist/tools/ask-user.js +32 -0
  968. package/dist/tools/ask-user.js.map +1 -0
  969. package/dist/tools/audit-dependencies.d.ts +13 -0
  970. package/dist/tools/audit-dependencies.d.ts.map +1 -0
  971. package/dist/tools/audit-dependencies.js +156 -0
  972. package/dist/tools/audit-dependencies.js.map +1 -0
  973. package/dist/tools/authoring/doc-model.d.ts +128 -0
  974. package/dist/tools/authoring/doc-model.d.ts.map +1 -0
  975. package/dist/tools/authoring/doc-model.js +82 -0
  976. package/dist/tools/authoring/doc-model.js.map +1 -0
  977. package/dist/tools/authoring/frontmatter.d.ts +51 -0
  978. package/dist/tools/authoring/frontmatter.d.ts.map +1 -0
  979. package/dist/tools/authoring/frontmatter.js +203 -0
  980. package/dist/tools/authoring/frontmatter.js.map +1 -0
  981. package/dist/tools/authoring/image-size.d.ts +17 -0
  982. package/dist/tools/authoring/image-size.d.ts.map +1 -0
  983. package/dist/tools/authoring/image-size.js +47 -0
  984. package/dist/tools/authoring/image-size.js.map +1 -0
  985. package/dist/tools/authoring/index.d.ts +34 -0
  986. package/dist/tools/authoring/index.d.ts.map +1 -0
  987. package/dist/tools/authoring/index.js +44 -0
  988. package/dist/tools/authoring/index.js.map +1 -0
  989. package/dist/tools/authoring/md-parse.d.ts +18 -0
  990. package/dist/tools/authoring/md-parse.d.ts.map +1 -0
  991. package/dist/tools/authoring/md-parse.js +262 -0
  992. package/dist/tools/authoring/md-parse.js.map +1 -0
  993. package/dist/tools/authoring/render-docx.d.ts +18 -0
  994. package/dist/tools/authoring/render-docx.d.ts.map +1 -0
  995. package/dist/tools/authoring/render-docx.js +272 -0
  996. package/dist/tools/authoring/render-docx.js.map +1 -0
  997. package/dist/tools/authoring/render-md.d.ts +13 -0
  998. package/dist/tools/authoring/render-md.d.ts.map +1 -0
  999. package/dist/tools/authoring/render-md.js +139 -0
  1000. package/dist/tools/authoring/render-md.js.map +1 -0
  1001. package/dist/tools/authoring/render-pdf.d.ts +16 -0
  1002. package/dist/tools/authoring/render-pdf.d.ts.map +1 -0
  1003. package/dist/tools/authoring/render-pdf.js +298 -0
  1004. package/dist/tools/authoring/render-pdf.js.map +1 -0
  1005. package/dist/tools/authoring/sections.d.ts +35 -0
  1006. package/dist/tools/authoring/sections.d.ts.map +1 -0
  1007. package/dist/tools/authoring/sections.js +81 -0
  1008. package/dist/tools/authoring/sections.js.map +1 -0
  1009. package/dist/tools/authoring/templates/builtins.d.ts +11 -0
  1010. package/dist/tools/authoring/templates/builtins.d.ts.map +1 -0
  1011. package/dist/tools/authoring/templates/builtins.js +478 -0
  1012. package/dist/tools/authoring/templates/builtins.js.map +1 -0
  1013. package/dist/tools/authoring/templates/index.d.ts +15 -0
  1014. package/dist/tools/authoring/templates/index.d.ts.map +1 -0
  1015. package/dist/tools/authoring/templates/index.js +25 -0
  1016. package/dist/tools/authoring/templates/index.js.map +1 -0
  1017. package/dist/tools/authoring/templates/store.d.ts +15 -0
  1018. package/dist/tools/authoring/templates/store.d.ts.map +1 -0
  1019. package/dist/tools/authoring/templates/store.js +63 -0
  1020. package/dist/tools/authoring/templates/store.js.map +1 -0
  1021. package/dist/tools/authoring/templates/template-model.d.ts +44 -0
  1022. package/dist/tools/authoring/templates/template-model.d.ts.map +1 -0
  1023. package/dist/tools/authoring/templates/template-model.js +39 -0
  1024. package/dist/tools/authoring/templates/template-model.js.map +1 -0
  1025. package/dist/tools/bash.d.ts +34 -0
  1026. package/dist/tools/bash.d.ts.map +1 -0
  1027. package/dist/tools/bash.js +353 -0
  1028. package/dist/tools/bash.js.map +1 -0
  1029. package/dist/tools/benchmark.d.ts +12 -0
  1030. package/dist/tools/benchmark.d.ts.map +1 -0
  1031. package/dist/tools/benchmark.js +147 -0
  1032. package/dist/tools/benchmark.js.map +1 -0
  1033. package/dist/tools/browse-library.d.ts +12 -0
  1034. package/dist/tools/browse-library.d.ts.map +1 -0
  1035. package/dist/tools/browse-library.js +188 -0
  1036. package/dist/tools/browse-library.js.map +1 -0
  1037. package/dist/tools/browser-click.d.ts +12 -0
  1038. package/dist/tools/browser-click.d.ts.map +1 -0
  1039. package/dist/tools/browser-click.js +58 -0
  1040. package/dist/tools/browser-click.js.map +1 -0
  1041. package/dist/tools/browser-close.d.ts +11 -0
  1042. package/dist/tools/browser-close.d.ts.map +1 -0
  1043. package/dist/tools/browser-close.js +27 -0
  1044. package/dist/tools/browser-close.js.map +1 -0
  1045. package/dist/tools/browser-navigate.d.ts +12 -0
  1046. package/dist/tools/browser-navigate.d.ts.map +1 -0
  1047. package/dist/tools/browser-navigate.js +66 -0
  1048. package/dist/tools/browser-navigate.js.map +1 -0
  1049. package/dist/tools/browser-snapshot.d.ts +11 -0
  1050. package/dist/tools/browser-snapshot.d.ts.map +1 -0
  1051. package/dist/tools/browser-snapshot.js +56 -0
  1052. package/dist/tools/browser-snapshot.js.map +1 -0
  1053. package/dist/tools/browser-type.d.ts +12 -0
  1054. package/dist/tools/browser-type.d.ts.map +1 -0
  1055. package/dist/tools/browser-type.js +68 -0
  1056. package/dist/tools/browser-type.js.map +1 -0
  1057. package/dist/tools/browser.d.ts +29 -0
  1058. package/dist/tools/browser.d.ts.map +1 -0
  1059. package/dist/tools/browser.js +307 -0
  1060. package/dist/tools/browser.js.map +1 -0
  1061. package/dist/tools/conversation-recall.d.ts +43 -0
  1062. package/dist/tools/conversation-recall.d.ts.map +1 -0
  1063. package/dist/tools/conversation-recall.js +177 -0
  1064. package/dist/tools/conversation-recall.js.map +1 -0
  1065. package/dist/tools/creative-asset-sync.d.ts +33 -0
  1066. package/dist/tools/creative-asset-sync.d.ts.map +1 -0
  1067. package/dist/tools/creative-asset-sync.js +39 -0
  1068. package/dist/tools/creative-asset-sync.js.map +1 -0
  1069. package/dist/tools/curator.d.ts +34 -0
  1070. package/dist/tools/curator.d.ts.map +1 -0
  1071. package/dist/tools/curator.js +191 -0
  1072. package/dist/tools/curator.js.map +1 -0
  1073. package/dist/tools/database-query.d.ts +11 -0
  1074. package/dist/tools/database-query.d.ts.map +1 -0
  1075. package/dist/tools/database-query.js +228 -0
  1076. package/dist/tools/database-query.js.map +1 -0
  1077. package/dist/tools/day-plan.d.ts +27 -0
  1078. package/dist/tools/day-plan.d.ts.map +1 -0
  1079. package/dist/tools/day-plan.js +207 -0
  1080. package/dist/tools/day-plan.js.map +1 -0
  1081. package/dist/tools/debug-logs.d.ts +13 -0
  1082. package/dist/tools/debug-logs.d.ts.map +1 -0
  1083. package/dist/tools/debug-logs.js +141 -0
  1084. package/dist/tools/debug-logs.js.map +1 -0
  1085. package/dist/tools/deploy-manifest.d.ts +37 -0
  1086. package/dist/tools/deploy-manifest.d.ts.map +1 -0
  1087. package/dist/tools/deploy-manifest.js +71 -0
  1088. package/dist/tools/deploy-manifest.js.map +1 -0
  1089. package/dist/tools/design-studio-tools.d.ts +83 -0
  1090. package/dist/tools/design-studio-tools.d.ts.map +1 -0
  1091. package/dist/tools/design-studio-tools.js +761 -0
  1092. package/dist/tools/design-studio-tools.js.map +1 -0
  1093. package/dist/tools/desktop-click-by-name.d.ts +12 -0
  1094. package/dist/tools/desktop-click-by-name.d.ts.map +1 -0
  1095. package/dist/tools/desktop-click-by-name.js +64 -0
  1096. package/dist/tools/desktop-click-by-name.js.map +1 -0
  1097. package/dist/tools/desktop-focus-window.d.ts +12 -0
  1098. package/dist/tools/desktop-focus-window.d.ts.map +1 -0
  1099. package/dist/tools/desktop-focus-window.js +59 -0
  1100. package/dist/tools/desktop-focus-window.js.map +1 -0
  1101. package/dist/tools/desktop-key-press.d.ts +12 -0
  1102. package/dist/tools/desktop-key-press.d.ts.map +1 -0
  1103. package/dist/tools/desktop-key-press.js +73 -0
  1104. package/dist/tools/desktop-key-press.js.map +1 -0
  1105. package/dist/tools/desktop-launch-app.d.ts +12 -0
  1106. package/dist/tools/desktop-launch-app.d.ts.map +1 -0
  1107. package/dist/tools/desktop-launch-app.js +58 -0
  1108. package/dist/tools/desktop-launch-app.js.map +1 -0
  1109. package/dist/tools/desktop-list-elements.d.ts +11 -0
  1110. package/dist/tools/desktop-list-elements.d.ts.map +1 -0
  1111. package/dist/tools/desktop-list-elements.js +65 -0
  1112. package/dist/tools/desktop-list-elements.js.map +1 -0
  1113. package/dist/tools/desktop-plan-approve.d.ts +30 -0
  1114. package/dist/tools/desktop-plan-approve.d.ts.map +1 -0
  1115. package/dist/tools/desktop-plan-approve.js +135 -0
  1116. package/dist/tools/desktop-plan-approve.js.map +1 -0
  1117. package/dist/tools/desktop-providers.d.ts +144 -0
  1118. package/dist/tools/desktop-providers.d.ts.map +1 -0
  1119. package/dist/tools/desktop-providers.js +12 -0
  1120. package/dist/tools/desktop-providers.js.map +1 -0
  1121. package/dist/tools/desktop-safety-gate.d.ts +84 -0
  1122. package/dist/tools/desktop-safety-gate.d.ts.map +1 -0
  1123. package/dist/tools/desktop-safety-gate.js +171 -0
  1124. package/dist/tools/desktop-safety-gate.js.map +1 -0
  1125. package/dist/tools/desktop-tools.d.ts +22 -0
  1126. package/dist/tools/desktop-tools.d.ts.map +1 -0
  1127. package/dist/tools/desktop-tools.js +71 -0
  1128. package/dist/tools/desktop-tools.js.map +1 -0
  1129. package/dist/tools/desktop-type.d.ts +12 -0
  1130. package/dist/tools/desktop-type.d.ts.map +1 -0
  1131. package/dist/tools/desktop-type.js +63 -0
  1132. package/dist/tools/desktop-type.js.map +1 -0
  1133. package/dist/tools/detect-language.d.ts +11 -0
  1134. package/dist/tools/detect-language.d.ts.map +1 -0
  1135. package/dist/tools/detect-language.js +235 -0
  1136. package/dist/tools/detect-language.js.map +1 -0
  1137. package/dist/tools/discover-news.d.ts +11 -0
  1138. package/dist/tools/discover-news.d.ts.map +1 -0
  1139. package/dist/tools/discover-news.js +261 -0
  1140. package/dist/tools/discover-news.js.map +1 -0
  1141. package/dist/tools/doc-generate.d.ts +17 -0
  1142. package/dist/tools/doc-generate.d.ts.map +1 -0
  1143. package/dist/tools/doc-generate.js +222 -0
  1144. package/dist/tools/doc-generate.js.map +1 -0
  1145. package/dist/tools/docs-lookup.d.ts +38 -0
  1146. package/dist/tools/docs-lookup.d.ts.map +1 -0
  1147. package/dist/tools/docs-lookup.js +174 -0
  1148. package/dist/tools/docs-lookup.js.map +1 -0
  1149. package/dist/tools/document-author.d.ts +40 -0
  1150. package/dist/tools/document-author.d.ts.map +1 -0
  1151. package/dist/tools/document-author.js +465 -0
  1152. package/dist/tools/document-author.js.map +1 -0
  1153. package/dist/tools/document-manage.d.ts +43 -0
  1154. package/dist/tools/document-manage.d.ts.map +1 -0
  1155. package/dist/tools/document-manage.js +665 -0
  1156. package/dist/tools/document-manage.js.map +1 -0
  1157. package/dist/tools/document-styling.d.ts +132 -0
  1158. package/dist/tools/document-styling.d.ts.map +1 -0
  1159. package/dist/tools/document-styling.js +360 -0
  1160. package/dist/tools/document-styling.js.map +1 -0
  1161. package/dist/tools/document-templates.d.ts +32 -0
  1162. package/dist/tools/document-templates.d.ts.map +1 -0
  1163. package/dist/tools/document-templates.js +95 -0
  1164. package/dist/tools/document-templates.js.map +1 -0
  1165. package/dist/tools/email-draft.d.ts +20 -0
  1166. package/dist/tools/email-draft.d.ts.map +1 -0
  1167. package/dist/tools/email-draft.js +191 -0
  1168. package/dist/tools/email-draft.js.map +1 -0
  1169. package/dist/tools/env-write.d.ts +32 -0
  1170. package/dist/tools/env-write.d.ts.map +1 -0
  1171. package/dist/tools/env-write.js +145 -0
  1172. package/dist/tools/env-write.js.map +1 -0
  1173. package/dist/tools/error-guidance.d.ts +38 -0
  1174. package/dist/tools/error-guidance.d.ts.map +1 -0
  1175. package/dist/tools/error-guidance.js +261 -0
  1176. package/dist/tools/error-guidance.js.map +1 -0
  1177. package/dist/tools/exercise-repair.d.ts +89 -0
  1178. package/dist/tools/exercise-repair.d.ts.map +1 -0
  1179. package/dist/tools/exercise-repair.js +350 -0
  1180. package/dist/tools/exercise-repair.js.map +1 -0
  1181. package/dist/tools/fact-check.d.ts +24 -0
  1182. package/dist/tools/fact-check.d.ts.map +1 -0
  1183. package/dist/tools/fact-check.js +78 -0
  1184. package/dist/tools/fact-check.js.map +1 -0
  1185. package/dist/tools/file-edit.d.ts +11 -0
  1186. package/dist/tools/file-edit.d.ts.map +1 -0
  1187. package/dist/tools/file-edit.js +166 -0
  1188. package/dist/tools/file-edit.js.map +1 -0
  1189. package/dist/tools/file-read.d.ts +12 -0
  1190. package/dist/tools/file-read.d.ts.map +1 -0
  1191. package/dist/tools/file-read.js +98 -0
  1192. package/dist/tools/file-read.js.map +1 -0
  1193. package/dist/tools/file-write.d.ts +13 -0
  1194. package/dist/tools/file-write.d.ts.map +1 -0
  1195. package/dist/tools/file-write.js +163 -0
  1196. package/dist/tools/file-write.js.map +1 -0
  1197. package/dist/tools/find-symbol.d.ts +13 -0
  1198. package/dist/tools/find-symbol.d.ts.map +1 -0
  1199. package/dist/tools/find-symbol.js +144 -0
  1200. package/dist/tools/find-symbol.js.map +1 -0
  1201. package/dist/tools/generate-image.d.ts +19 -0
  1202. package/dist/tools/generate-image.d.ts.map +1 -0
  1203. package/dist/tools/generate-image.js +438 -0
  1204. package/dist/tools/generate-image.js.map +1 -0
  1205. package/dist/tools/generate-video.d.ts +21 -0
  1206. package/dist/tools/generate-video.d.ts.map +1 -0
  1207. package/dist/tools/generate-video.js +197 -0
  1208. package/dist/tools/generate-video.js.map +1 -0
  1209. package/dist/tools/generate-voice.d.ts +13 -0
  1210. package/dist/tools/generate-voice.d.ts.map +1 -0
  1211. package/dist/tools/generate-voice.js +156 -0
  1212. package/dist/tools/generate-voice.js.map +1 -0
  1213. package/dist/tools/generation-manager.d.ts +75 -0
  1214. package/dist/tools/generation-manager.d.ts.map +1 -0
  1215. package/dist/tools/generation-manager.js +150 -0
  1216. package/dist/tools/generation-manager.js.map +1 -0
  1217. package/dist/tools/get-datetime.d.ts +11 -0
  1218. package/dist/tools/get-datetime.d.ts.map +1 -0
  1219. package/dist/tools/get-datetime.js +59 -0
  1220. package/dist/tools/get-datetime.js.map +1 -0
  1221. package/dist/tools/git-commit.d.ts +13 -0
  1222. package/dist/tools/git-commit.d.ts.map +1 -0
  1223. package/dist/tools/git-commit.js +136 -0
  1224. package/dist/tools/git-commit.js.map +1 -0
  1225. package/dist/tools/git-create-pr.d.ts +13 -0
  1226. package/dist/tools/git-create-pr.d.ts.map +1 -0
  1227. package/dist/tools/git-create-pr.js +134 -0
  1228. package/dist/tools/git-create-pr.js.map +1 -0
  1229. package/dist/tools/git-diff.d.ts +12 -0
  1230. package/dist/tools/git-diff.d.ts.map +1 -0
  1231. package/dist/tools/git-diff.js +134 -0
  1232. package/dist/tools/git-diff.js.map +1 -0
  1233. package/dist/tools/git.d.ts +11 -0
  1234. package/dist/tools/git.d.ts.map +1 -0
  1235. package/dist/tools/git.js +89 -0
  1236. package/dist/tools/git.js.map +1 -0
  1237. package/dist/tools/glob.d.ts +11 -0
  1238. package/dist/tools/glob.d.ts.map +1 -0
  1239. package/dist/tools/glob.js +64 -0
  1240. package/dist/tools/glob.js.map +1 -0
  1241. package/dist/tools/grep.d.ts +11 -0
  1242. package/dist/tools/grep.d.ts.map +1 -0
  1243. package/dist/tools/grep.js +131 -0
  1244. package/dist/tools/grep.js.map +1 -0
  1245. package/dist/tools/health-catalogue-search.d.ts +11 -0
  1246. package/dist/tools/health-catalogue-search.d.ts.map +1 -0
  1247. package/dist/tools/health-catalogue-search.js +179 -0
  1248. package/dist/tools/health-catalogue-search.js.map +1 -0
  1249. package/dist/tools/health-plan-create.d.ts +24 -0
  1250. package/dist/tools/health-plan-create.d.ts.map +1 -0
  1251. package/dist/tools/health-plan-create.js +325 -0
  1252. package/dist/tools/health-plan-create.js.map +1 -0
  1253. package/dist/tools/health-plan-delete.d.ts +11 -0
  1254. package/dist/tools/health-plan-delete.d.ts.map +1 -0
  1255. package/dist/tools/health-plan-delete.js +97 -0
  1256. package/dist/tools/health-plan-delete.js.map +1 -0
  1257. package/dist/tools/health-plan-list.d.ts +11 -0
  1258. package/dist/tools/health-plan-list.d.ts.map +1 -0
  1259. package/dist/tools/health-plan-list.js +123 -0
  1260. package/dist/tools/health-plan-list.js.map +1 -0
  1261. package/dist/tools/health-plan-update-day.d.ts +18 -0
  1262. package/dist/tools/health-plan-update-day.d.ts.map +1 -0
  1263. package/dist/tools/health-plan-update-day.js +134 -0
  1264. package/dist/tools/health-plan-update-day.js.map +1 -0
  1265. package/dist/tools/health-plan-update.d.ts +11 -0
  1266. package/dist/tools/health-plan-update.d.ts.map +1 -0
  1267. package/dist/tools/health-plan-update.js +103 -0
  1268. package/dist/tools/health-plan-update.js.map +1 -0
  1269. package/dist/tools/health-profile-ask.d.ts +21 -0
  1270. package/dist/tools/health-profile-ask.d.ts.map +1 -0
  1271. package/dist/tools/health-profile-ask.js +50 -0
  1272. package/dist/tools/health-profile-ask.js.map +1 -0
  1273. package/dist/tools/http-request.d.ts +12 -0
  1274. package/dist/tools/http-request.d.ts.map +1 -0
  1275. package/dist/tools/http-request.js +329 -0
  1276. package/dist/tools/http-request.js.map +1 -0
  1277. package/dist/tools/journal.d.ts +17 -0
  1278. package/dist/tools/journal.d.ts.map +1 -0
  1279. package/dist/tools/journal.js +159 -0
  1280. package/dist/tools/journal.js.map +1 -0
  1281. package/dist/tools/learning.d.ts +154 -0
  1282. package/dist/tools/learning.d.ts.map +1 -0
  1283. package/dist/tools/learning.js +1344 -0
  1284. package/dist/tools/learning.js.map +1 -0
  1285. package/dist/tools/list-directory.d.ts +11 -0
  1286. package/dist/tools/list-directory.d.ts.map +1 -0
  1287. package/dist/tools/list-directory.js +95 -0
  1288. package/dist/tools/list-directory.js.map +1 -0
  1289. package/dist/tools/memory-delete.d.ts +11 -0
  1290. package/dist/tools/memory-delete.d.ts.map +1 -0
  1291. package/dist/tools/memory-delete.js +54 -0
  1292. package/dist/tools/memory-delete.js.map +1 -0
  1293. package/dist/tools/memory-recall.d.ts +11 -0
  1294. package/dist/tools/memory-recall.d.ts.map +1 -0
  1295. package/dist/tools/memory-recall.js +101 -0
  1296. package/dist/tools/memory-recall.js.map +1 -0
  1297. package/dist/tools/memory-save.d.ts +11 -0
  1298. package/dist/tools/memory-save.d.ts.map +1 -0
  1299. package/dist/tools/memory-save.js +110 -0
  1300. package/dist/tools/memory-save.js.map +1 -0
  1301. package/dist/tools/memory-update.d.ts +11 -0
  1302. package/dist/tools/memory-update.d.ts.map +1 -0
  1303. package/dist/tools/memory-update.js +88 -0
  1304. package/dist/tools/memory-update.js.map +1 -0
  1305. package/dist/tools/news.d.ts +11 -0
  1306. package/dist/tools/news.d.ts.map +1 -0
  1307. package/dist/tools/news.js +137 -0
  1308. package/dist/tools/news.js.map +1 -0
  1309. package/dist/tools/open-design-studio.d.ts +20 -0
  1310. package/dist/tools/open-design-studio.d.ts.map +1 -0
  1311. package/dist/tools/open-design-studio.js +45 -0
  1312. package/dist/tools/open-design-studio.js.map +1 -0
  1313. package/dist/tools/open-health-room.d.ts +19 -0
  1314. package/dist/tools/open-health-room.d.ts.map +1 -0
  1315. package/dist/tools/open-health-room.js +44 -0
  1316. package/dist/tools/open-health-room.js.map +1 -0
  1317. package/dist/tools/open-learning-room.d.ts +20 -0
  1318. package/dist/tools/open-learning-room.d.ts.map +1 -0
  1319. package/dist/tools/open-learning-room.js +44 -0
  1320. package/dist/tools/open-learning-room.js.map +1 -0
  1321. package/dist/tools/paper-fetch.d.ts +11 -0
  1322. package/dist/tools/paper-fetch.d.ts.map +1 -0
  1323. package/dist/tools/paper-fetch.js +236 -0
  1324. package/dist/tools/paper-fetch.js.map +1 -0
  1325. package/dist/tools/post-performance.d.ts +17 -0
  1326. package/dist/tools/post-performance.d.ts.map +1 -0
  1327. package/dist/tools/post-performance.js +58 -0
  1328. package/dist/tools/post-performance.js.map +1 -0
  1329. package/dist/tools/present-plan.d.ts +34 -0
  1330. package/dist/tools/present-plan.d.ts.map +1 -0
  1331. package/dist/tools/present-plan.js +117 -0
  1332. package/dist/tools/present-plan.js.map +1 -0
  1333. package/dist/tools/project-index.d.ts +11 -0
  1334. package/dist/tools/project-index.d.ts.map +1 -0
  1335. package/dist/tools/project-index.js +88 -0
  1336. package/dist/tools/project-index.js.map +1 -0
  1337. package/dist/tools/propose-hooks.d.ts +17 -0
  1338. package/dist/tools/propose-hooks.d.ts.map +1 -0
  1339. package/dist/tools/propose-hooks.js +71 -0
  1340. package/dist/tools/propose-hooks.js.map +1 -0
  1341. package/dist/tools/propose-tool.d.ts +11 -0
  1342. package/dist/tools/propose-tool.d.ts.map +1 -0
  1343. package/dist/tools/propose-tool.js +155 -0
  1344. package/dist/tools/propose-tool.js.map +1 -0
  1345. package/dist/tools/recipe-repair.d.ts +116 -0
  1346. package/dist/tools/recipe-repair.d.ts.map +1 -0
  1347. package/dist/tools/recipe-repair.js +404 -0
  1348. package/dist/tools/recipe-repair.js.map +1 -0
  1349. package/dist/tools/record-machine-rule.d.ts +24 -0
  1350. package/dist/tools/record-machine-rule.d.ts.map +1 -0
  1351. package/dist/tools/record-machine-rule.js +60 -0
  1352. package/dist/tools/record-machine-rule.js.map +1 -0
  1353. package/dist/tools/release-notes.d.ts +11 -0
  1354. package/dist/tools/release-notes.d.ts.map +1 -0
  1355. package/dist/tools/release-notes.js +59 -0
  1356. package/dist/tools/release-notes.js.map +1 -0
  1357. package/dist/tools/remove-background.d.ts +15 -0
  1358. package/dist/tools/remove-background.d.ts.map +1 -0
  1359. package/dist/tools/remove-background.js +117 -0
  1360. package/dist/tools/remove-background.js.map +1 -0
  1361. package/dist/tools/report-generate.d.ts +30 -0
  1362. package/dist/tools/report-generate.d.ts.map +1 -0
  1363. package/dist/tools/report-generate.js +325 -0
  1364. package/dist/tools/report-generate.js.map +1 -0
  1365. package/dist/tools/research-post.d.ts +20 -0
  1366. package/dist/tools/research-post.d.ts.map +1 -0
  1367. package/dist/tools/research-post.js +87 -0
  1368. package/dist/tools/research-post.js.map +1 -0
  1369. package/dist/tools/research-story.d.ts +29 -0
  1370. package/dist/tools/research-story.d.ts.map +1 -0
  1371. package/dist/tools/research-story.js +78 -0
  1372. package/dist/tools/research-story.js.map +1 -0
  1373. package/dist/tools/rollback.d.ts +11 -0
  1374. package/dist/tools/rollback.d.ts.map +1 -0
  1375. package/dist/tools/rollback.js +81 -0
  1376. package/dist/tools/rollback.js.map +1 -0
  1377. package/dist/tools/scan-industry.d.ts +19 -0
  1378. package/dist/tools/scan-industry.d.ts.map +1 -0
  1379. package/dist/tools/scan-industry.js +82 -0
  1380. package/dist/tools/scan-industry.js.map +1 -0
  1381. package/dist/tools/screenshot.d.ts +11 -0
  1382. package/dist/tools/screenshot.d.ts.map +1 -0
  1383. package/dist/tools/screenshot.js +82 -0
  1384. package/dist/tools/screenshot.js.map +1 -0
  1385. package/dist/tools/secret-request.d.ts +24 -0
  1386. package/dist/tools/secret-request.d.ts.map +1 -0
  1387. package/dist/tools/secret-request.js +75 -0
  1388. package/dist/tools/secret-request.js.map +1 -0
  1389. package/dist/tools/security.d.ts +26 -0
  1390. package/dist/tools/security.d.ts.map +1 -0
  1391. package/dist/tools/security.js +78 -0
  1392. package/dist/tools/security.js.map +1 -0
  1393. package/dist/tools/self-inspect.d.ts +34 -0
  1394. package/dist/tools/self-inspect.d.ts.map +1 -0
  1395. package/dist/tools/self-inspect.js +427 -0
  1396. package/dist/tools/self-inspect.js.map +1 -0
  1397. package/dist/tools/suggest-beats.d.ts +17 -0
  1398. package/dist/tools/suggest-beats.d.ts.map +1 -0
  1399. package/dist/tools/suggest-beats.js +71 -0
  1400. package/dist/tools/suggest-beats.js.map +1 -0
  1401. package/dist/tools/suggest-stories.d.ts +26 -0
  1402. package/dist/tools/suggest-stories.d.ts.map +1 -0
  1403. package/dist/tools/suggest-stories.js +102 -0
  1404. package/dist/tools/suggest-stories.js.map +1 -0
  1405. package/dist/tools/support-request.d.ts +11 -0
  1406. package/dist/tools/support-request.d.ts.map +1 -0
  1407. package/dist/tools/support-request.js +110 -0
  1408. package/dist/tools/support-request.js.map +1 -0
  1409. package/dist/tools/switch-mode.d.ts +21 -0
  1410. package/dist/tools/switch-mode.d.ts.map +1 -0
  1411. package/dist/tools/switch-mode.js +57 -0
  1412. package/dist/tools/switch-mode.js.map +1 -0
  1413. package/dist/tools/task-manage.d.ts +16 -0
  1414. package/dist/tools/task-manage.d.ts.map +1 -0
  1415. package/dist/tools/task-manage.js +256 -0
  1416. package/dist/tools/task-manage.js.map +1 -0
  1417. package/dist/tools/task-suggest.d.ts +27 -0
  1418. package/dist/tools/task-suggest.d.ts.map +1 -0
  1419. package/dist/tools/task-suggest.js +70 -0
  1420. package/dist/tools/task-suggest.js.map +1 -0
  1421. package/dist/tools/test-generate.d.ts +20 -0
  1422. package/dist/tools/test-generate.d.ts.map +1 -0
  1423. package/dist/tools/test-generate.js +259 -0
  1424. package/dist/tools/test-generate.js.map +1 -0
  1425. package/dist/tools/test-run.d.ts +13 -0
  1426. package/dist/tools/test-run.d.ts.map +1 -0
  1427. package/dist/tools/test-run.js +177 -0
  1428. package/dist/tools/test-run.js.map +1 -0
  1429. package/dist/tools/todo-write.d.ts +11 -0
  1430. package/dist/tools/todo-write.d.ts.map +1 -0
  1431. package/dist/tools/todo-write.js +70 -0
  1432. package/dist/tools/todo-write.js.map +1 -0
  1433. package/dist/tools/tool-registry.d.ts +84 -0
  1434. package/dist/tools/tool-registry.d.ts.map +1 -0
  1435. package/dist/tools/tool-registry.js +728 -0
  1436. package/dist/tools/tool-registry.js.map +1 -0
  1437. package/dist/tools/types.d.ts +128 -0
  1438. package/dist/tools/types.d.ts.map +1 -0
  1439. package/dist/tools/types.js +2 -0
  1440. package/dist/tools/types.js.map +1 -0
  1441. package/dist/tools/verification-trust.d.ts +39 -0
  1442. package/dist/tools/verification-trust.d.ts.map +1 -0
  1443. package/dist/tools/verification-trust.js +135 -0
  1444. package/dist/tools/verification-trust.js.map +1 -0
  1445. package/dist/tools/verify-change.d.ts +11 -0
  1446. package/dist/tools/verify-change.d.ts.map +1 -0
  1447. package/dist/tools/verify-change.js +775 -0
  1448. package/dist/tools/verify-change.js.map +1 -0
  1449. package/dist/tools/weather.d.ts +11 -0
  1450. package/dist/tools/weather.d.ts.map +1 -0
  1451. package/dist/tools/weather.js +96 -0
  1452. package/dist/tools/weather.js.map +1 -0
  1453. package/dist/tools/web-search.d.ts +12 -0
  1454. package/dist/tools/web-search.d.ts.map +1 -0
  1455. package/dist/tools/web-search.js +137 -0
  1456. package/dist/tools/web-search.js.map +1 -0
  1457. package/dist/tools/write-article.d.ts +33 -0
  1458. package/dist/tools/write-article.d.ts.map +1 -0
  1459. package/dist/tools/write-article.js +206 -0
  1460. package/dist/tools/write-article.js.map +1 -0
  1461. package/dist/tools/write-exercise.d.ts +31 -0
  1462. package/dist/tools/write-exercise.d.ts.map +1 -0
  1463. package/dist/tools/write-exercise.js +278 -0
  1464. package/dist/tools/write-exercise.js.map +1 -0
  1465. package/dist/tools/write-post-image.d.ts +31 -0
  1466. package/dist/tools/write-post-image.d.ts.map +1 -0
  1467. package/dist/tools/write-post-image.js +102 -0
  1468. package/dist/tools/write-post-image.js.map +1 -0
  1469. package/dist/tools/write-post.d.ts +20 -0
  1470. package/dist/tools/write-post.d.ts.map +1 -0
  1471. package/dist/tools/write-post.js +113 -0
  1472. package/dist/tools/write-post.js.map +1 -0
  1473. package/dist/tools/write-recipe.d.ts +29 -0
  1474. package/dist/tools/write-recipe.d.ts.map +1 -0
  1475. package/dist/tools/write-recipe.js +262 -0
  1476. package/dist/tools/write-recipe.js.map +1 -0
  1477. package/dist/tools/write-video-post.d.ts +25 -0
  1478. package/dist/tools/write-video-post.d.ts.map +1 -0
  1479. package/dist/tools/write-video-post.js +238 -0
  1480. package/dist/tools/write-video-post.js.map +1 -0
  1481. package/dist/tools/write-voiceover.d.ts +29 -0
  1482. package/dist/tools/write-voiceover.d.ts.map +1 -0
  1483. package/dist/tools/write-voiceover.js +98 -0
  1484. package/dist/tools/write-voiceover.js.map +1 -0
  1485. package/dist/workflows/index.d.ts +4 -0
  1486. package/dist/workflows/index.d.ts.map +1 -0
  1487. package/dist/workflows/index.js +2 -0
  1488. package/dist/workflows/index.js.map +1 -0
  1489. package/dist/workflows/types.d.ts +112 -0
  1490. package/dist/workflows/types.d.ts.map +1 -0
  1491. package/dist/workflows/types.js +10 -0
  1492. package/dist/workflows/types.js.map +1 -0
  1493. package/dist/workflows/workflow-manager.d.ts +54 -0
  1494. package/dist/workflows/workflow-manager.d.ts.map +1 -0
  1495. package/dist/workflows/workflow-manager.js +277 -0
  1496. package/dist/workflows/workflow-manager.js.map +1 -0
  1497. package/package.json +223 -0
@@ -0,0 +1,3558 @@
1
+ import { recoverWrittenToolCalls, WrittenCallStreamFilter } from './recover-written-calls.js';
2
+ import { getTextContent } from '../core/types.js';
3
+ import { MAX_TOOL_CALL_ITERATIONS, ITERATION_WARNING_THRESHOLD } from '../core/constants.js';
4
+ import { t } from '../i18n/index.js';
5
+ import { logger } from '../core/logger.js';
6
+ import { buildToolPrompt, parseToolCalls, formatToolResult } from './text-tool-parser.js';
7
+ import { bridgeImagesForTextModel } from './vision-bridge.js';
8
+ import { auditClaims } from './claims-auditor.js';
9
+ import { autoExtractAndSave } from '../memory/auto-extract.js';
10
+ import { maybeBuildDesignReinjection, isUIFilePath as isUIFilePathLocal } from './design-reinjection.js';
11
+ import { isStopCommand } from './stop-command.js';
12
+ import { findOriginalUserTaskIndex, formatSessionTasksBlock, buildCompressionContinuationHeader, buildVerbatimUserTurnsBlock, extractStructuredFields, trimMessageBody, OLD_MESSAGE_BODY_MAX_CHARS, isMetaPrefix, } from './context-continuity.js';
13
+ import { classifyTaskComplexity, formatDirectnessHint, COMPLEXITY_BUDGETS, } from './task-classifier.js';
14
+ import { avaEvents, withTrajectory, withChildTrajectory, getTrajectory } from '../dataset/emitter.js';
15
+ import { chargeCredits, extractUsage } from '../billing/meter.js';
16
+ import { summarizeToolArgs, summarizeToolResult, summarizeChainOutcome, categorizeToolPurpose } from '../dataset/summarizers.js';
17
+ import { pickVerificationTools, categorizeCorrection, VERIFICATION_TOOLS } from '../dataset/verification.js';
18
+ import { matchToolError } from '../tools/error-guidance.js';
19
+ import { recordEditFromTool, pendingFilesAtClosure, runPendingVerify, buildVerifyFailureNudge, } from './post-edit-verify.js';
20
+ import { signatureForFailure, recordFailure, shouldEscalateFreshEyes, markFreshEyesEscalated, describeFailureLoop, } from './error-loop-detector.js';
21
+ import { runFreshEyesReview, buildFreshEyesContext } from './fresh-eyes.js';
22
+ import { randomUUID } from 'node:crypto';
23
+ import { PNG } from 'pngjs';
24
+ // ─── Image downsampling ────────────────────────────────────────────────────
25
+ // Screenshots at native resolution are the single biggest token sink in
26
+ // vision-heavy sessions. A 1920×1080 full-page PNG encodes to ~100KB base64
27
+ // = ~25K tokens. Re-sent across 10 turns = 250K tokens for one image.
28
+ // Downsampling to max 1024px preserves all semantic information the model
29
+ // needs (layout, hierarchy, colour, typography visibility) while cutting
30
+ // the byte cost by 60-80%. Nearest-neighbor sampling is fine — this is not
31
+ // photo restoration, it's context for reasoning.
32
+ const IMAGE_MAX_DIMENSION = 1024;
33
+ function downsampleScreenshotBase64(base64) {
34
+ try {
35
+ const buffer = Buffer.from(base64, 'base64');
36
+ const src = PNG.sync.read(buffer);
37
+ const maxCurrent = Math.max(src.width, src.height);
38
+ if (maxCurrent <= IMAGE_MAX_DIMENSION)
39
+ return base64;
40
+ const scale = IMAGE_MAX_DIMENSION / maxCurrent;
41
+ const newW = Math.max(1, Math.round(src.width * scale));
42
+ const newH = Math.max(1, Math.round(src.height * scale));
43
+ const dst = new PNG({ width: newW, height: newH });
44
+ for (let y = 0; y < newH; y++) {
45
+ const srcY = Math.min(src.height - 1, Math.floor(y / scale));
46
+ for (let x = 0; x < newW; x++) {
47
+ const srcX = Math.min(src.width - 1, Math.floor(x / scale));
48
+ const srcIdx = (src.width * srcY + srcX) << 2;
49
+ const dstIdx = (newW * y + x) << 2;
50
+ dst.data[dstIdx] = src.data[srcIdx];
51
+ dst.data[dstIdx + 1] = src.data[srcIdx + 1];
52
+ dst.data[dstIdx + 2] = src.data[srcIdx + 2];
53
+ dst.data[dstIdx + 3] = src.data[srcIdx + 3];
54
+ }
55
+ }
56
+ return PNG.sync.write(dst).toString('base64');
57
+ }
58
+ catch (err) {
59
+ // If decode/resize fails for any reason, fall back to the original.
60
+ // Never block the vision pipeline on a resize failure.
61
+ logger.debug(`[agent] Image downsample failed, using original: ${err instanceof Error ? err.message : String(err)}`);
62
+ return base64;
63
+ }
64
+ }
65
+ // ─── Mode-aware tool filtering ──────────────────────────────────────────────
66
+ // When a non-work mode is active, restrict the tool schema sent to the model
67
+ // so it can only call tools listed in that mode's system prompt.
68
+ // Without this, the model sees all tools in the schema and ignores text restrictions.
69
+ // ─── Continuation-stall detection ──────────────────────────────────────────
70
+ // Identifies assistant responses that narrate intent ("Let me rewrite the
71
+ // sidebar...") but terminate without making any tool calls. These are worse
72
+ // than empty responses because the user sees a promise that never gets
73
+ // fulfilled. Detected via prefix matching on common narration patterns.
74
+ //
75
+ // False positives (real closures that look like stalls) are preferable to
76
+ // false negatives (stalls that slip through) because the cost of a redundant
77
+ // "continue" nudge is small while the cost of invisible stalled work is
78
+ // catastrophic for UX.
79
+ const STALL_PREFIX_PATTERNS = [
80
+ 'let me ',
81
+ "i'll ",
82
+ 'i will ',
83
+ "i'm going to ",
84
+ 'i am going to ',
85
+ 'first, let me ',
86
+ 'first, i',
87
+ 'now let me ',
88
+ "now i'll ",
89
+ 'okay, let me ',
90
+ 'ok, let me ',
91
+ 'right, let me ',
92
+ 'alright, let me ',
93
+ 'starting the ',
94
+ 'starting with ',
95
+ 'beginning the ',
96
+ "let's ",
97
+ ];
98
+ function looksLikeContinuationStall(content) {
99
+ const trimmed = content.trim().toLowerCase();
100
+ if (trimmed.length === 0)
101
+ return false;
102
+ // Long responses are probably genuine explanations, not stalls
103
+ if (trimmed.length > 500)
104
+ return false;
105
+ // Check known continuation-narration prefixes
106
+ for (const prefix of STALL_PREFIX_PATTERNS) {
107
+ if (trimmed.startsWith(prefix))
108
+ return true;
109
+ }
110
+ return false;
111
+ }
112
+ /**
113
+ * Detect context drift: the model produced a greeting or social response
114
+ * after a turn with tool usage. This happens when attention on the original
115
+ * task fades under the weight of many file reads / tool results and the
116
+ * model defaults to a safe social response instead of summarising findings.
117
+ *
118
+ * Only fires when the turn had 3+ tool calls AND the response is short
119
+ * and contains greeting patterns. A greeting in a zero-tool turn is fine
120
+ * (that's just a Chat-mode response).
121
+ */
122
+ const GREETING_PATTERNS = [
123
+ /\bhey\b/i, /\bhello\b/i, /\bhi\b/i, /\bgood\s+(?:morning|afternoon|evening)\b/i,
124
+ /\bhow(?:'s| is) your (?:day|morning|evening|afternoon)\b/i,
125
+ /\bhow are you\b/i, /\bwhat(?:'s| is) up\b/i, /\bnice to (?:see|hear|meet)\b/i,
126
+ ];
127
+ function looksLikePostToolDrift(content, toolCallCount) {
128
+ if (toolCallCount < 3)
129
+ return false; // Only relevant after real tool usage
130
+ const trimmed = content.trim();
131
+ if (trimmed.length > 200)
132
+ return false; // Short response after many tools = suspicious
133
+ const lower = trimmed.toLowerCase();
134
+ return GREETING_PATTERNS.some(p => p.test(lower));
135
+ }
136
+ /**
137
+ * Desktop-automation + browser-control tools. Two separate jobs ride on this
138
+ * one list, which is why it's exported rather than local:
139
+ *
140
+ * 1. MODE GATING (any surface) — they're only valid in desktop mode, so the
141
+ * no-prefix default path filters them out. Otherwise the model hallucinates
142
+ * desktop_click_by_name mid-coding-session and the safety gate eats the noise.
143
+ *
144
+ * 2. MARKETPLACE COMPLIANCE (extension only) — Microsoft blocked this extension
145
+ * over exactly these tools and required their removal to reinstate it
146
+ * (v0.48.1, 2026-04-21). The extension host passes this list to
147
+ * registerBuiltins({ exclude }) so they are NEVER CONSTRUCTED there.
148
+ *
149
+ * Until 2026-07-17 only job 1 existed, and job 2 was believed done but wasn't:
150
+ * the tools were registered on every surface and the only thing standing
151
+ * between a marketplace user and a desktop_* schema was mode detection, which
152
+ * keys off a literal '[Desktop Automation Mode]' prefix in the user's own
153
+ * message text and had no idea which surface it was running on. They couldn't
154
+ * actually drive anything (the extension supplies no uiaProvider/inputProvider),
155
+ * but "inert" is not the promise we made to MS.
156
+ *
157
+ * Keep this list as the ONE definition. If a desktop_* or browser_* tool is
158
+ * added to the registry and not added here, it ships to the marketplace.
159
+ */
160
+ export const DESKTOP_TOOL_NAMES = [
161
+ 'desktop_plan_approve',
162
+ 'desktop_launch_app',
163
+ 'desktop_list_elements',
164
+ 'desktop_click_by_name',
165
+ 'desktop_focus_window',
166
+ 'desktop_type',
167
+ 'desktop_key_press',
168
+ 'browser_navigate',
169
+ 'browser_snapshot',
170
+ 'browser_click',
171
+ 'browser_type',
172
+ 'browser_close',
173
+ ];
174
+ const DESKTOP_ONLY_TOOLS = new Set(DESKTOP_TOOL_NAMES);
175
+ /**
176
+ * Tools every mode gets, unioned in on top of MODE_ALLOWED_TOOLS.
177
+ *
178
+ * `self_inspect` is read-only by construction — it reads Ava's own source and
179
+ * her deploy state. It cannot write a file, run a command, or spend anything;
180
+ * changing the code is a thing the operator does directly, never Ava. So there
181
+ * is no mode where "don't let her read her own source" is the right answer.
182
+ *
183
+ * It was previously allowed in `work` and `plan` only, which meant that in the
184
+ * other ten modes the schema filter removed it and she'd correctly say she
185
+ * didn't have it — including in Chat, where "what can you do?" is exactly the
186
+ * question you'd ask, and which the README promises she answers by reading her
187
+ * own source.
188
+ *
189
+ * This is a union rather than twelve list edits on purpose: a new mode gets it
190
+ * automatically, so it can't rot back out the way it did the first time.
191
+ *
192
+ * conversation_recall joins it for the same reason. The system prompt tells Ava
193
+ * every turn to "call conversation_recall to read the real transcript instead
194
+ * of guessing" — but it was in no mode's list, so the filter dropped it in every
195
+ * mode that carries a prefix (which the dashboard always sends). The exact-recall
196
+ * backstop was unreachable on the main surface. Read-only: it reads the current
197
+ * run's transcript off sharedState, writes nothing.
198
+ */
199
+ const ALWAYS_ALLOWED_TOOLS = new Set(['self_inspect', 'conversation_recall']);
200
+ const MODE_ALLOWED_TOOLS = {
201
+ // Work mode — the bread-and-butter coding surface. Ships every turn
202
+ // to users writing code, so the schema list is the single biggest
203
+ // per-turn token line item. Keep what a coder actually reaches for;
204
+ // push anything that belongs to a different state-of-thought out.
205
+ // Cross-mode asks (image generation, email drafts, weather) still
206
+ // work — Ava calls switch_mode. The friction of one switch is worth
207
+ // the 2-3K tokens saved on every single coding turn.
208
+ //
209
+ // Deliberately OUT:
210
+ // - journal_write / learning / weather / news — Chat / Teach.
211
+ // - email_draft / report_generate / document_manage /
212
+ // document_templates — office work, rarely mixed with coding.
213
+ // - memory_delete — destructive, never a normal-flow tool.
214
+ work: new Set([
215
+ // File operations
216
+ 'read', 'write', 'edit',
217
+ // Search
218
+ 'glob', 'grep', 'list_directory', 'find_symbol', 'project_index',
219
+ // Shell
220
+ 'bash',
221
+ // The build's own check. The pre-closure guard constructs this tool
222
+ // directly rather than asking for it, so verify has always run — but a
223
+ // mode that edits files should be able to ASK for it too, and a name
224
+ // absent from the list it belongs on is how the next audit gets it wrong.
225
+ 'verify_change',
226
+ // Git
227
+ 'git_status', 'git_diff', 'rollback', 'git_commit', 'git_create_pr',
228
+ // Web
229
+ 'web_search', 'http_request', 'browser',
230
+ // Creative Studio — making an asset happens in the Studio, not inline
231
+ // here. This gate used to list 'generate_image' / 'generate_video' /
232
+ // but the registry only ever builds the design_* tools,
233
+ // so those three names resolved to nothing — dead entries. The handoff
234
+ // (open_design_studio, further down) and browse_library are what work mode
235
+ // actually needs: point at the Studio to make, read the library to reuse.
236
+ 'browse_library',
237
+ 'remove_background',
238
+ // Data
239
+ 'database_query',
240
+ // Memory — delete is out (rare, destructive)
241
+ 'memory_save', 'memory_recall', 'memory_update',
242
+ // Planning / tasks
243
+ 'present_plan', 'todo_write', 'task_manage', 'task_suggest', 'apply_plan',
244
+ // Testing
245
+ 'test_run', 'test_generate',
246
+ // Architecture / docs gen
247
+ 'analyze_architecture', 'doc_generate',
248
+ // Security audits
249
+ 'audit_dependencies',
250
+ // Performance
251
+ 'benchmark',
252
+ // Debug
253
+ 'debug_logs',
254
+ // Interaction
255
+ 'ask_user', 'support_request',
256
+ // Secret vault → project. The prompt tells Ava to call secret_request for a
257
+ // {{secret:<id>}} handle, then env_write to put a granted key into the
258
+ // project's gitignored .env — the host swaps the real value in at write
259
+ // time so she never sees it. Both were missing from every build surface
260
+ // (secret_request was desktop-only, env_write in no mode), so the flow the
261
+ // prompt describes couldn't run. Confirmation-gated writes; env_write
262
+ // refuses any non-gitignored target.
263
+ 'secret_request', 'env_write',
264
+ // Self
265
+ 'docs_lookup', 'propose_tool', 'self_inspect', 'release_notes',
266
+ // Taste specialist — fresh-context Curator for design/voice/microcopy
267
+ // calls that would otherwise degrade under cognitive load.
268
+ 'curator',
269
+ // Utility
270
+ 'get_datetime', 'detect_language',
271
+ // Hand a fitness/meal plan request off to the focused Health room, a
272
+ // learn-a-topic request off to the focused Learning room, or an icon /
273
+ // on-brand asset request off to the focused Design Studio.
274
+ 'open_health_room', 'open_learning_room', 'open_design_studio',
275
+ // Mode switch
276
+ 'switch_mode',
277
+ ]),
278
+ plan: new Set([
279
+ // Read + nav
280
+ 'read', 'glob', 'grep', 'list_directory', 'find_symbol', 'project_index',
281
+ // Research surface — http_request/browser/news added so the
282
+ // coordinator-direct path (the most common Plan flow) has the same
283
+ // research tools the Researcher persona gets in orchestrated mode.
284
+ 'web_search', 'http_request', 'browser', 'news',
285
+ // Memory + planning + analysis
286
+ 'memory_save', 'memory_recall', 'present_plan', 'analyze_architecture',
287
+ // Reference / introspection (already used; advertised below now too)
288
+ 'docs_lookup', 'self_inspect',
289
+ // Taste specialist for design/voice/microcopy decisions
290
+ 'curator',
291
+ // Interaction + utilities
292
+ 'ask_user', 'get_datetime', 'detect_language',
293
+ 'switch_mode',
294
+ ]),
295
+ chat: new Set([
296
+ 'web_search', 'memory_save', 'memory_recall', 'memory_update', 'journal_write',
297
+ // todo_write for Ava's own session steps. task_suggest is her DEFAULT for a
298
+ // task-worthy thing she notices (a tap-to-add card); task_manage is for when
299
+ // the user explicitly says "add X to my list" — create directly then.
300
+ 'todo_write', 'task_suggest', 'task_manage',
301
+ 'get_datetime', 'weather', 'news', 'ask_user',
302
+ // Hand a fitness/meal plan request off to the focused Health room, a
303
+ // learn-a-topic request off to the focused Learning room, or an icon /
304
+ // on-brand asset request off to the focused Design Studio.
305
+ 'open_health_room', 'open_learning_room', 'open_design_studio',
306
+ 'switch_mode',
307
+ ]),
308
+ brainstorm: new Set([
309
+ // Research signals (web + news) so coordinator-direct ideation has the
310
+ // same research surface the orchestrated team gets via IDEATION_TOOLS.
311
+ 'web_search', 'http_request', 'browser', 'news',
312
+ // Memory — update is in for refining accumulated ideas across sessions.
313
+ 'memory_save', 'memory_recall', 'memory_update',
314
+ // Output shape
315
+ 'present_plan', 'journal_write', 'todo_write',
316
+ // Taste specialist for naming/voice/microcopy decisions
317
+ 'curator',
318
+ 'ask_user', 'get_datetime',
319
+ 'switch_mode',
320
+ ]),
321
+ teach: new Set([
322
+ // Reading + project navigation
323
+ 'read', 'glob', 'grep', 'list_directory', 'find_symbol', 'project_index',
324
+ // Writing — needed because the Tutor system prompt says "create sample
325
+ // files" and runs live code examples. file_write/file_edit + bash only.
326
+ // Git commit/PR tools intentionally OUT — wrong blast radius for a
327
+ // teaching session.
328
+ 'write', 'edit', 'bash',
329
+ // Web (verify facts, fetch docs, browse references)
330
+ 'web_search', 'http_request', 'browser',
331
+ // Memory — update is in so the learner profile evolves across sessions
332
+ 'memory_save', 'memory_recall', 'memory_update',
333
+ // Journal — Ava reflects on the learner + their progress in her own voice
334
+ 'journal_write',
335
+ // Learning subsystem
336
+ 'learning_create', 'learning_teach', 'learning_progress',
337
+ // Scientific Papers library — "Read with Ava" hands her an arXiv/DOI paper
338
+ // to explain. The tool's own description says "use this in Teach mode", and
339
+ // this was the one mode that didn't allow it, so the button pointed at a
340
+ // tool she couldn't reach.
341
+ 'paper_fetch_full_text',
342
+ // Interaction + utilities
343
+ 'ask_user', 'get_datetime', 'detect_language',
344
+ 'switch_mode',
345
+ ]),
346
+ security: new Set([
347
+ 'read', 'glob', 'grep', 'list_directory', 'find_symbol', 'project_index',
348
+ 'bash', 'git_status', 'git_diff', 'web_search', 'analyze_architecture',
349
+ 'audit_dependencies', 'debug_logs', 'memory_save', 'memory_recall',
350
+ 'test_run', 'ask_user',
351
+ // get_datetime was missing here and nowhere else — eleven of the twelve
352
+ // rooms had it. Security is the room where it matters most: whether an
353
+ // advisory predates the version you run, whether a dependency is actually
354
+ // behind, how old an unpatched CVE is. Without it she dates from her
355
+ // training cutoff, which is not a vague answer but a confident wrong one,
356
+ // and it gets more wrong every month the model ages.
357
+ 'get_datetime',
358
+ 'switch_mode',
359
+ ]),
360
+ // Health Room — Ava focused entirely on the user's health & fitness. Same
361
+ // Ava, health-scoped: the plan + catalogue tools are her kit, memory carries
362
+ // the relationship, web_search backs facts. Coding / file / shell tools are
363
+ // deliberately OUT — this room composes plans from the real catalogue, it
364
+ // doesn't touch the codebase.
365
+ health: new Set([
366
+ // health_plan_list before create: activating archives another plan of the
367
+ // same type, and she cannot warn about what she cannot see.
368
+ // health_plan_update was missing from this set while the room prompt told
369
+ // her to use it for "make that draft active" — so the only tool she could
370
+ // reach was create, which is precisely how the library ended up with two
371
+ // copies of the same week, one draft and one active on the wrong day.
372
+ 'health_plan_list', 'health_plan_create', 'health_plan_update',
373
+ 'health_plan_update_day', 'health_plan_delete', 'health_catalogue_search',
374
+ 'health_profile_ask',
375
+ 'memory_save', 'memory_recall', 'memory_update',
376
+ // Journal — Ava reflects on the person + their health journey in her voice
377
+ 'journal_write',
378
+ 'web_search', 'ask_user', 'get_datetime',
379
+ 'switch_mode',
380
+ ]),
381
+ // Design Studio — Ava the Design Architect, focused entirely on making the
382
+ // user on-brand icons. Same Ava, design-scoped: the shape/generate/brand/save
383
+ // tools are her kit, memory carries their taste, journal her read on their eye.
384
+ // Coding / file / shell tools are deliberately OUT — this room makes assets,
385
+ // it doesn't touch the codebase. Scope is icons for now.
386
+ design: new Set([
387
+ 'design_find_shape', 'design_generate_icon', 'design_generate_set',
388
+ 'design_generate_video', 'design_generate_image', 'design_generate_voice',
389
+ 'design_generate_logo', 'design_explore_logos', 'design_brand_kit', 'design_save',
390
+ 'memory_save', 'memory_recall', 'memory_update',
391
+ // Journal — Ava reflects on the person + their taste in her own voice
392
+ 'journal_write',
393
+ // Research — ground a look in current design references / trends before authoring it
394
+ 'web_search',
395
+ 'ask_user', 'get_datetime',
396
+ 'switch_mode',
397
+ ]),
398
+ // Social Studio — Ava as the social-media & marketing lead, driving the
399
+ // Posts floor. Same Ava, marketing-scoped: research/hook/write/performance
400
+ // tools are her kit, docs_lookup grounds every product claim, generate_image
401
+ // makes visuals, memory carries the mission's voice, journal her read on the
402
+ // brand's public presence. Coding / file / shell tools are deliberately OUT —
403
+ // this room ships posts, it doesn't touch the codebase.
404
+ social: new Set([
405
+ 'research_post', 'propose_hooks', 'write_post', 'post_performance', 'suggest_beats',
406
+ // The day plan. She and the operator agree the day together, so she
407
+ // needs to read it (items may already be carried from yesterday),
408
+ // write what was agreed, and tick what she can verify. Registering
409
+ // the tools was not enough on its own — see write_video_post below,
410
+ // which sat unusable in exactly this way while she correctly reported
411
+ // it was not in her toolset. This Set is the real gate.
412
+ 'day_plan_read', 'day_plan_write', 'day_plan_item_status',
413
+ // Short-form video — the Video Posts room. Registering the tool in the
414
+ // builtins was not enough: this Set is the real gate, and while it was
415
+ // missing she reported "write_video_post isn't in my toolset" and pointed
416
+ // people at the Design Studio instead. She was reading her list correctly.
417
+ 'write_video_post',
418
+ // A post's picture is part of the post. Made here, not via a trip to the
419
+ // Design Studio — she cannot finish a post she is not allowed to illustrate.
420
+ 'write_post_image',
421
+ // Our own catalogue, read-only. A food or fitness post should be about a
422
+ // dish or a movement we actually have, with the real name, ingredients and
423
+ // method — and the video store already pulls OUR photography for the hero
424
+ // image, so the copy has to match the picture it is paired with. Without
425
+ // these she told users the recipe catalogue "lives in a different part of
426
+ // the system that I can't query from this room", which was true and is the
427
+ // same gap write_video_post had: registered in the builtins, missing here.
428
+ //
429
+ // Authoring stays out. Recipes are written at the Pantry desk and
430
+ // exercises in the Gym; this room reads the shelf, it does not stock it.
431
+ 'find_recipe', 'read_recipe', 'find_exercise', 'read_exercise',
432
+ // Industry radar — what AI leaders/labs actually said this week (sourced)
433
+ 'scan_industry',
434
+ // Ground every product claim in the real docs, never training memory
435
+ 'docs_lookup', 'release_notes',
436
+ // Visuals for posts are made in the Creative Studio, not inline. Point the
437
+ // user there (open_design_studio) and reuse what they already have
438
+ // (browse_library). 'generate_image' used to sit here — a name the registry
439
+ // never builds — so a post's visual silently did nothing.
440
+ 'open_design_studio', 'browse_library',
441
+ // A voiceover ON ITS OWN, not attached to a clip — audio is its own
442
+ // deliverable (a read for footage they already have), and routing it
443
+ // through a video they don't want burns a video generation.
444
+ //
445
+ // NOT design_generate_voice: that tool speaks through `designControl`, the
446
+ // Design Studio's canvas channel, which this surface never mounts. Adding
447
+ // it here would have registered a tool that answers "no canvas" every time.
448
+ // write_voiceover carries its own store, the way write_video_post does.
449
+ 'write_voiceover',
450
+ // Research to make the angle current
451
+ 'web_search',
452
+ // Memory carries the mission voice; journal her read on the brand
453
+ 'memory_save', 'memory_recall', 'memory_update', 'journal_write',
454
+ 'ask_user', 'get_datetime',
455
+ 'switch_mode',
456
+ ]),
457
+ // Newsroom — Ava as Correspondent. She reads what outlets published, stands
458
+ // the story up, and writes her OWN account with the receipts attached.
459
+ //
460
+ // The kit is small on purpose. web_search is deliberately OUT: an open web
461
+ // search returns blogs, forums and SEO sludge, and once that is in the corpus
462
+ // a quote "verifies" against a source that was never journalism. The news
463
+ // index is the only door in, so the evidence write_article checks against is
464
+ // evidence from a publisher. Coding / file / shell tools are out entirely.
465
+ news: new Set([
466
+ 'discover_news', 'suggest_stories', 'research_story', 'fact_check', 'write_article',
467
+ // Header images are authored in the Creative Studio, never a lifted press
468
+ // photo — point the user there rather than generating inline. 'generate_image'
469
+ // used to be here, but the registry never builds that name.
470
+ 'open_design_studio', 'browse_library',
471
+ // Continuity: running stories, corrections owed, what she has already covered.
472
+ 'memory_save', 'memory_recall', 'memory_update', 'journal_write',
473
+ 'ask_user', 'get_datetime',
474
+ 'switch_mode',
475
+ ]),
476
+ // Write mode — the author's surface. Markdown is the editable source;
477
+ // Word/PDF are exports. Ships the authoring tool + the supporting cast a
478
+ // writer reaches for (research, images for covers, the file ops the .md
479
+ // lives in, memory for house style/templates). Coding tools stay out — this
480
+ // is writing, not building.
481
+ write: new Set([
482
+ // Authoring
483
+ 'document_author', 'document_manage', 'report_generate', 'email_draft',
484
+ // The .md source lives on disk
485
+ 'read', 'write', 'edit', 'glob', 'grep', 'list_directory',
486
+ // Research to ground the writing
487
+ 'web_search', 'http_request', 'browser',
488
+ // Illustrations / covers are made in the Creative Studio — hand off there
489
+ // and reuse existing assets. 'generate_image' used to sit here (a name the
490
+ // registry never builds), which is why write mode's "cover image she
491
+ // generates herself" never actually generated one. remove_background stays:
492
+ // it's a one-shot edit utility, not creative generation.
493
+ 'open_design_studio', 'browse_library', 'remove_background',
494
+ // Memory — house style, saved templates, continuity across a long piece
495
+ 'memory_save', 'memory_recall', 'memory_update',
496
+ // Light planning for long documents
497
+ 'present_plan', 'todo_write',
498
+ // Utility + interaction
499
+ 'get_datetime', 'detect_language', 'ask_user', 'support_request',
500
+ 'docs_lookup', 'curator',
501
+ // Mode switch
502
+ 'switch_mode',
503
+ ]),
504
+ // Desktop Automation mode. Two layers of hands:
505
+ // - desktop_launch_app to open apps (denylist-scoped — no shell)
506
+ // - desktop_* for UIA-tree targeting of native windows
507
+ // - browser_* for driving the visible Ava Chromium via DOM
508
+ // File-editing and coordinate-based native input are deliberately absent
509
+ // here — file changes go through Work mode, and native UIA input is the
510
+ // stable targeting layer. `bash` is intentionally OUT: it's too broad
511
+ // for this surface and gives the model an escape hatch we don't want.
512
+ desktop: new Set([
513
+ // Trajectory-level plan approval (one card, many steps)
514
+ 'desktop_plan_approve',
515
+ // Launch apps — scoped, no shell interpreter
516
+ 'desktop_launch_app',
517
+ // Native desktop via UIA tree — stable selectors, not pixel coords.
518
+ // No desktop_screenshot or desktop_click_xy: vision + coordinate
519
+ // guessing is a failure mode. Ava must use tree/DOM-based targeting.
520
+ 'desktop_list_elements', 'desktop_click_by_name', 'desktop_focus_window',
521
+ 'desktop_type', 'desktop_key_press',
522
+ // Persist a standing rule the user wants obeyed on this machine forever
523
+ 'record_machine_rule',
524
+ // Browser automation via Playwright DOM — visible Chromium, stable.
525
+ 'browser_navigate', 'browser_snapshot', 'browser_click', 'browser_type', 'browser_close',
526
+ // Light support
527
+ 'web_search', 'memory_recall', 'ask_user', 'get_datetime',
528
+ 'switch_mode',
529
+ // Capability-style secret grant — required so Ava can fetch a
530
+ // {{secret:<id>}} handle when the safety gate blocks typing into
531
+ // a sensitive field. Without this in the allowlist the fix below
532
+ // would be a dead end — Ava would be told to call secret_request
533
+ // but the registry would reject the call for being out-of-mode.
534
+ 'secret_request',
535
+ ]),
536
+ };
537
+ /**
538
+ * The mode this turn is in, read from the tag the surfaces prepend.
539
+ *
540
+ * Exported because the AutoCoordinator had grown a second copy of this that
541
+ * sniffed the SYSTEM prompt for the string `'Plan mode'`. The marker is
542
+ * `[Plan Mode]`, on the user message, with a capital M — so the copy matched
543
+ * nothing and answered `'work'` for all seven modes, every turn, since it was
544
+ * written. One fact, two detectors, and the quiet one was wrong: the same
545
+ * shape as the dead tool names in the mode allowlists.
546
+ */
547
+ export function detectModeFromMessages(messages) {
548
+ for (let i = messages.length - 1; i >= 0; i--) {
549
+ const msg = messages[i];
550
+ if (msg.role !== 'user')
551
+ continue;
552
+ const text = getTextContent(msg.content);
553
+ if (text.startsWith('[Internal Planning'))
554
+ continue;
555
+ if (text.startsWith('[Plan Mode]'))
556
+ return 'plan';
557
+ if (text.startsWith('[Chat Mode]'))
558
+ return 'chat';
559
+ if (text.startsWith('[Brainstorm Mode]'))
560
+ return 'brainstorm';
561
+ if (text.startsWith('[Write Mode]'))
562
+ return 'write';
563
+ if (text.startsWith('[Teach Mode]'))
564
+ return 'teach';
565
+ if (text.startsWith('[Security Audit Mode]'))
566
+ return 'security';
567
+ if (text.startsWith('[Desktop Automation Mode]'))
568
+ return 'desktop';
569
+ if (text.startsWith('[Health Room]'))
570
+ return 'health';
571
+ if (text.startsWith('[Design Studio]'))
572
+ return 'design';
573
+ if (text.startsWith('[Social Studio]'))
574
+ return 'social';
575
+ if (text.startsWith('[Newsroom]'))
576
+ return 'news';
577
+ break;
578
+ }
579
+ return null;
580
+ }
581
+ /**
582
+ * Can this mode change files?
583
+ *
584
+ * Read from `MODE_ALLOWED_TOOLS` rather than a list of its own. A mode that is
585
+ * not handed `write` or `edit` is read-only by design, and nothing downstream
586
+ * should be able to grant it more than the mode itself has.
587
+ *
588
+ * Exists because the AutoCoordinator's Builder hand-off had no idea what mode
589
+ * it was in. Seen live 2026-08-19: a Plan-mode turn produced a plan card, wrote
590
+ * eleven todos, announced "Builder dispatched — executing 11 tasks", and started
591
+ * editing an Unreal project. Plan mode is read-only — it cannot open a file to
592
+ * write it — and the orchestrator went around that by spawning agents that
593
+ * could. The operator's words were "why are you coding when did i say to do
594
+ * anything but plan".
595
+ *
596
+ * An unknown mode returns true: work mode carries no prefix, so callers that
597
+ * default to it must not be silently blocked.
598
+ */
599
+ /**
600
+ * The hard ceiling on what a persona may be handed in a READ-ONLY mode.
601
+ *
602
+ * The conductor scopes each persona by its own `allowedTools`, taken from the
603
+ * full registry — the mode allowlist is not consulted at all. So a mode's
604
+ * read-only guarantee held only for as long as nobody put a write-capable
605
+ * persona on its team, which is a promise about a roster rather than about the
606
+ * mode. Audited 2026-08-19: Plan's team happened to be clean, and the
607
+ * guarantee was one persona away from being false.
608
+ *
609
+ * Returns null for a mode that can edit files, deliberately. Work's allowlist
610
+ * is known to be stale and has never applied (it has no prefix, so the filter
611
+ * never runs) — clamping the Builder to it here would enforce a list nobody
612
+ * has checked, and would land as a breakage dressed as a tightening.
613
+ */
614
+ export function readOnlyModeToolCeiling(mode) {
615
+ if (!mode || modeCanEditFiles(mode))
616
+ return null;
617
+ const allowed = MODE_ALLOWED_TOOLS[mode];
618
+ if (!allowed)
619
+ return null;
620
+ return new Set([...allowed, ...ALWAYS_ALLOWED_TOOLS]);
621
+ }
622
+ export function modeCanEditFiles(mode) {
623
+ if (!mode)
624
+ return true;
625
+ const allowed = MODE_ALLOWED_TOOLS[mode];
626
+ if (!allowed)
627
+ return true;
628
+ return allowed.has('write') || allowed.has('edit');
629
+ }
630
+ // ─── Agent ───────────────────────────────────────────────────────────────────
631
+ export class Agent {
632
+ provider;
633
+ model;
634
+ // Vision bridge — a dedicated vision-capable provider+model (e.g. Qwen Omni)
635
+ // used to DESCRIBE images when the main coordinator is text-only (DeepSeek,
636
+ // Mistral Codestral, etc.), so it can "see" the image as text. Optional; when
637
+ // absent, text-only models fall back to a "switch model" note.
638
+ visionProvider;
639
+ visionModel;
640
+ // Image descriptions cached per session (keyed by the image data URL) so a
641
+ // text-only coordinator doesn't re-run the vision model on every turn.
642
+ visionDescriptionCache = new Map();
643
+ toolRegistry;
644
+ toolContext;
645
+ pendingInterjections = [];
646
+ // Graceful-pause flag — set by requestPause(), checked at each loop
647
+ // boundary. A "pause" finishes the current step then exits cleanly, unlike
648
+ // the abort signal which is an immediate hard stop.
649
+ pauseRequested = false;
650
+ // Verifying tools that ran this run() (name + success), for the soft
651
+ // honesty gate (claims-auditor) at final-answer time. Reset per run.
652
+ runToolEvidence = [];
653
+ /**
654
+ * The id of the turn currently running — one per run(), shared by every
655
+ * model call the turn makes, sent to our platform as X-Ava-Turn-Id.
656
+ *
657
+ * A tool-using turn calls the model repeatedly, and each call was landing in
658
+ * usage_logs as its own row with nothing connecting them. So a turn where
659
+ * the third call failed and the fourth succeeded was indistinguishable from
660
+ * four separate turns: retries invisible, and no way to tell a price change
661
+ * from a behaviour change. This is what makes cost-per-outcome answerable
662
+ * rather than just cost-per-call.
663
+ */
664
+ runTurnId;
665
+ // Did the soft honesty gate flag an unbacked factual claim this run?
666
+ // Set by the claims-auditor branch; read by the verification_evidence
667
+ // dataset emit in run()'s finally. Reset per run.
668
+ runClaimFlagged = false;
669
+ // Latest claims-auditor result this run (set at answer finalization).
670
+ // Read by the verify-or-restate guard in runInner. Reset per run.
671
+ lastAudit = null;
672
+ // Did the honesty gate already fire its one verify-or-restate re-prompt
673
+ // this run? Caps the active loop at a single attempt. Reset per run.
674
+ honestyVerifyAttempted = false;
675
+ /** Build the verify-or-restate re-prompt for the honesty gate. Maps the
676
+ * claim to the tool that would actually check it, and offers the honest
677
+ * alternative (restate without asserting). Internal — never shown as text. */
678
+ buildHonestyVerifyNudge(audit) {
679
+ const claim = audit.claims[0] ?? 'a completion/state claim';
680
+ const how = audit.tier === 'critical'
681
+ ? 'run a real check (audit_dependencies, a scan, or grep for the actual pattern)'
682
+ : 'run the tool that checks it (test_run for tests, bash or git_diff for code changes, http_request or browser for an endpoint, file_read for a file)';
683
+ return `[Honesty check — you stated "${claim}" but ran no tool that verifies it this turn. Do ONE of two things now, no exceptions: (a) ${how}, then report the actual result; or (b) restate without asserting it as done — e.g. "I changed X, but haven't verified it yet." Do not repeat the unbacked claim as fact.]`;
684
+ }
685
+ _inThinkTag = false;
686
+ // ─── Exploration budget tracking (token-cost discipline) ────────────────
687
+ // Per-run state: the task classification and how many read-only tool calls
688
+ // the agent has made before its first write-capable call. When the count
689
+ // exceeds the budget for the current task complexity, a soft nudge is
690
+ // injected into the next LLM call ("you're stalling — commit to a
691
+ // direction"). Reset on each Agent.run() call.
692
+ currentTaskComplexity = 'moderate';
693
+ readCountBeforeFirstWrite = 0;
694
+ hasWrittenInThisRun = false;
695
+ explorationNudgeFired = false;
696
+ // Design re-injection state — tracks last re-injection turn and file mtimes
697
+ // so we don't re-read the same design files 20 times in a single session.
698
+ designReinjectionTurn = 0;
699
+ designReinjectionLastTurn = -Infinity;
700
+ designReinjectionLastMtimes = new Map();
701
+ /** Which surface this Agent is running in (cli/extension/ide/companion). */
702
+ surface;
703
+ /** Stable session UUID — one per Agent instance unless caller overrides. */
704
+ sessionId;
705
+ /**
706
+ * Loop-prevention master switch. When false, the pre-closure verify
707
+ * guard skips the verify_change call and falls straight through to the
708
+ * existing closure logic — the agent behaves exactly like it did before
709
+ * post-edit-verify shipped. Used as an emergency off switch and a way
710
+ * for power users who hate any extra LLM round-trips to opt out. The
711
+ * `recordEditFromTool` post-tool hook still runs (it's free) so the
712
+ * trajectory has the data if the flag flips on mid-session.
713
+ *
714
+ * Default true — the guard catches real bugs (build-broken closures,
715
+ * stuck-loop credit burn) and the cost is bounded (one verify pass +
716
+ * at most one fresh-eyes call per turn).
717
+ */
718
+ loopPreventionEnabled;
719
+ /**
720
+ * Recovery hook that returns the new messages accumulated during the
721
+ * currently-running Agent.run() call. Set at the top of run(),
722
+ * cleared (left as a stale closure) when run() exits — the next run
723
+ * overwrites it. Lets the caller reach into a cancelled run and
724
+ * persist Ava's partial work (completed tool calls, streamed
725
+ * assistant text) before the AbortError throws her out, instead of
726
+ * losing all of it because run() never reached its return statement.
727
+ *
728
+ * Without this, pressing Stop mid-task discarded every tool call and
729
+ * file edit Ava had completed — the next user message saw a
730
+ * conversation history with a gap where her work used to be, and
731
+ * Ava had no memory of what she'd done. See cancelRun() in the host.
732
+ */
733
+ currentRunRecoveryHook = null;
734
+ /**
735
+ * Consecutive identical failures, per tool+arguments, within one run.
736
+ *
737
+ * A tool that fails the SAME WAY on the same arguments is not a retry
738
+ * situation — nothing has changed, so the next attempt cannot go differently.
739
+ * Observed in the wild: health_plan_create failed seven times running while
740
+ * the model cheerfully called it again each time, spending credits per turn,
741
+ * until the operator hit Stop. Nothing counted the repeats and nothing told
742
+ * anyone.
743
+ *
744
+ * Keyed on tool + arguments so a genuine retry with DIFFERENT arguments is
745
+ * untouched — changing the call is exactly the productive response to a
746
+ * failure, and this must not punish it.
747
+ */
748
+ repeatedToolFailures = new Map();
749
+ /**
750
+ * Trajectory metadata from the previous Agent.run() in this session.
751
+ * Used to attach `correction_received` events to the trajectory the
752
+ * user is correcting, and to record whether that prior trajectory
753
+ * had verified before answering. Reset across agent instances.
754
+ */
755
+ lastTrajectoryMetadata = null;
756
+ /**
757
+ * The mode of the previous Agent.run() in this session. Used to fire
758
+ * `mode_switch` dataset events when the user changes modes between
759
+ * turns (e.g. switches from Work to Plan via the [Plan Mode] prefix).
760
+ */
761
+ lastDetectedMode = null;
762
+ constructor(opts) {
763
+ this.provider = opts.provider;
764
+ this.model = opts.model;
765
+ this.visionProvider = opts.visionProvider;
766
+ this.visionModel = opts.visionModel;
767
+ this.toolRegistry = opts.toolRegistry;
768
+ const sf = opts.surface ?? 'cli';
769
+ this.toolContext = {
770
+ cwd: opts.cwd,
771
+ sharedState: opts.sharedState,
772
+ secretGranter: opts.secretGranter,
773
+ // Map the host's AvaSurface to the docs Surface ('extension' -> 'ext') so
774
+ // surface-aware tools (docs_lookup) know where Ava is running.
775
+ surface: sf === 'extension' ? 'ext' : sf,
776
+ };
777
+ this.surface = sf;
778
+ this.sessionId = opts.sessionId ?? randomUUID();
779
+ this.loopPreventionEnabled = opts.loopPreventionEnabled ?? true;
780
+ }
781
+ /**
782
+ * Return the new messages accumulated by the currently-running (or
783
+ * just-aborted) Agent.run() call. Used by the host's cancellation
784
+ * path to persist Ava's partial work — completed tool calls, streamed
785
+ * assistant text — before the conversation history is updated with
786
+ * the stop marker. Returns an empty array if no run has happened.
787
+ *
788
+ * Safe to call after run() has exited normally — the hook still
789
+ * returns a snapshot of the run that just finished.
790
+ */
791
+ getCurrentRunPartialMessages() {
792
+ return this.currentRunRecoveryHook?.() ?? [];
793
+ }
794
+ /**
795
+ * Update the working directory used by all tool executions.
796
+ * Called when the user opens a different project folder mid-session.
797
+ */
798
+ setCwd(cwd) {
799
+ this.toolContext.cwd = cwd;
800
+ }
801
+ /**
802
+ * Run a one-shot completion — single prompt, no tools, no streaming,
803
+ * timeout-bounded. Returns the assistant's trimmed text content, or
804
+ * null on error / timeout / empty response.
805
+ *
806
+ * Intended for utility callers that need a quick model round-trip
807
+ * without spinning up the full agent loop — e.g. the extension host's
808
+ * auto-journal reflection that writes a 2–4 sentence session summary
809
+ * after a completed turn. Cheap: one LLM call, no tool schemas, small
810
+ * max_tokens.
811
+ *
812
+ * Errors (network, provider, parse, timeout) all collapse to null so
813
+ * the caller can fall back gracefully without try/catch plumbing.
814
+ */
815
+ async completeOneShot(prompt, opts) {
816
+ const maxTokens = opts?.maxTokens ?? 200;
817
+ const timeoutMs = opts?.timeoutMs ?? 10_000;
818
+ try {
819
+ const response = await Promise.race([
820
+ this.provider.createCompletion({
821
+ model: this.model.id,
822
+ messages: [{ role: 'user', content: prompt }],
823
+ max_tokens: maxTokens,
824
+ }),
825
+ new Promise((resolve) => setTimeout(() => resolve(null), timeoutMs)),
826
+ ]);
827
+ if (!response)
828
+ return null;
829
+ const choice = response.choices?.[0];
830
+ const content = choice?.message?.content;
831
+ if (typeof content !== 'string')
832
+ return null;
833
+ const trimmed = content.trim();
834
+ return trimmed || null;
835
+ }
836
+ catch {
837
+ return null;
838
+ }
839
+ }
840
+ /**
841
+ * Inject a user message mid-run. The message will be appended to the
842
+ * conversation between the current and next agent iteration, allowing
843
+ * the user to steer, add context, or redirect without cancelling.
844
+ */
845
+ inject(message) {
846
+ // Guard against empty or whitespace-only injections.
847
+ //
848
+ // Without this, any caller that accidentally passes an empty string
849
+ // (missing translation key, race condition on a programmatic send,
850
+ // stale callback, IPC edge case) ends up appending an empty user
851
+ // message into the conversation mid-run — and the model reasonably
852
+ // responds "did you send something?" to a blank turn. That's the
853
+ // "blonde moment" failure mode: not attention drift, just an empty
854
+ // turn being treated as a real one.
855
+ //
856
+ // Drop silently with a debug log so bugs upstream stay visible in
857
+ // logs but don't manifest as weird agent behaviour to the user.
858
+ if (typeof message !== 'string' || message.trim().length === 0) {
859
+ logger.debug('[agent] inject() called with empty/invalid message — dropped');
860
+ return;
861
+ }
862
+ this.pendingInterjections.push(message);
863
+ }
864
+ /**
865
+ * Request a graceful pause. Unlike the abort signal (a hard, immediate
866
+ * stop), this lets the current step finish and then exits the loop cleanly
867
+ * at the next boundary — so a typed "wait"/"pause" never rips Ava out
868
+ * mid-write. The conversation keeps everything completed; the user's next
869
+ * message continues from there.
870
+ */
871
+ requestPause() {
872
+ this.pauseRequested = true;
873
+ }
874
+ /**
875
+ * Drop every queued interjection without processing it.
876
+ *
877
+ * Called from the extension's cancel/stop handler so user injections
878
+ * queued during the aborted task don't silently replay as the first
879
+ * message of the next run. Without this, "stop" + "new task" would
880
+ * carry the last typed-but-not-yet-processed interjection into the
881
+ * fresh turn and feel like the old task was still alive.
882
+ */
883
+ clearPendingInterjections() {
884
+ if (this.pendingInterjections.length > 0) {
885
+ logger.debug(`[agent] clearPendingInterjections — dropped ${this.pendingInterjections.length} queued message(s)`);
886
+ this.pendingInterjections.length = 0;
887
+ }
888
+ }
889
+ /**
890
+ * True when at least one interjection is queued waiting to be consumed.
891
+ * Used by Conductor orchestration (passed via the orchestrate options'
892
+ * `hasPendingInjection` callback) to break out of the blocking persona
893
+ * loop early when the user sends a message — without this poll, the
894
+ * Conductor can hold the main Agent loop for 10–60 seconds on a full
895
+ * team and silently drop the injection when control returns.
896
+ */
897
+ hasPendingInterjections() {
898
+ return this.pendingInterjections.length > 0;
899
+ }
900
+ /**
901
+ * Run one agent turn against the given conversation history.
902
+ *
903
+ * Contract: `messages` is the CALLER'S current conversation history
904
+ * — system prompt + user turns + assistant turns + tool calls/results,
905
+ * ending with the user's latest message to act on. The agent uses this
906
+ * as read-only input to build its own working context (which may be
907
+ * compressed, truncated, or trimmed internally for token economy);
908
+ * none of those transforms leak back to the caller.
909
+ *
910
+ * Returns ONLY the new messages produced by this turn — assistant
911
+ * replies, tool results, mid-turn user interjections. The caller is
912
+ * responsible for appending them to its conversation
913
+ * (`conversation.appendMessages(result)`).
914
+ *
915
+ * Before the Option 2 refactor (commit trail leading up to this) the
916
+ * return value was the full history plus the new turn's messages, and
917
+ * callers did `conversation.setMessages(result)` — which meant
918
+ * compression's destructive transforms could leak across the
919
+ * conversation boundary and silently clear user scrollback. Returning
920
+ * only new messages makes that class of bug impossible by construction.
921
+ */
922
+ /**
923
+ * Mode detection with the surface rule applied.
924
+ *
925
+ * `detectModeFromMessages` keys off a literal prefix in the USER's own
926
+ * message text ('[Desktop Automation Mode]'), and knows nothing about which
927
+ * surface it's running on. On the VS Code extension, desktop mode does not
928
+ * exist: Microsoft blocked us over the desktop/browser tools and required
929
+ * their removal to reinstate (v0.48.1). Without this, a marketplace user
930
+ * could type the prefix by hand and pull desktop_* into the turn — schemas,
931
+ * personas, mode-switch events, the lot.
932
+ *
933
+ * This is the second of two locks. The first is the extension host excluding
934
+ * DESKTOP_TOOL_NAMES at registerBuiltins, so the tools are never constructed
935
+ * there at all. Two locks, because we already broke this promise once by
936
+ * assuming one implicit one was enough.
937
+ */
938
+ detectModeForSurface(messages) {
939
+ const mode = detectModeFromMessages(messages);
940
+ if (mode === 'desktop' && this.toolContext.sharedState?.clientSurface === 'extension') {
941
+ return null;
942
+ }
943
+ return mode;
944
+ }
945
+ async run(messages, onEvent, signal) {
946
+ // Open a dataset trajectory for the entire run. Every avaEvents.emit()
947
+ // inside (sync or async, in this method or any helper it calls) inherits
948
+ // this envelope. trajectory_id is auto-generated; the consumer in
949
+ // packages/core/src/dataset/consumer.ts only writes if a user has
950
+ // explicitly opted in via ~/.ava/datasets/config.json — defaults are
951
+ // all-off so this scope opens but emits nothing for unconsenting users.
952
+ const detectedMode = (this.detectModeForSurface(messages) ?? 'work');
953
+ const previousMode = this.lastDetectedMode;
954
+ this.lastDetectedMode = detectedMode;
955
+ // One id for this whole turn, however many model calls it takes.
956
+ // Generated here rather than server-side because only the agent knows
957
+ // where a turn begins — to the platform route, each call is just another
958
+ // HTTP request.
959
+ this.runTurnId = randomUUID();
960
+ // Reset per-run tool evidence for the honesty gate (claims-auditor).
961
+ this.runToolEvidence = [];
962
+ this.runClaimFlagged = false;
963
+ this.lastAudit = null;
964
+ this.honestyVerifyAttempted = false;
965
+ // If we're nested inside an outer trajectory (e.g. AutoCoordinator
966
+ // wrapped its own run), open a child trajectory so the chain is
967
+ // preserved via parent_trajectory_id. Otherwise open a fresh root.
968
+ const openTrajectory = getTrajectory()
969
+ ? withChildTrajectory
970
+ : withTrajectory;
971
+ return openTrajectory({
972
+ session_id: this.sessionId,
973
+ surface: this.surface,
974
+ mode: detectedMode,
975
+ model_id: this.model.id,
976
+ }, async () => {
977
+ const traj = getTrajectory();
978
+ let finalContent = null;
979
+ // ── Dataset event: mode switched between turns ──────────────
980
+ // Mode is detected per-run from message prefixes ([Plan Mode]
981
+ // etc.). When this run's mode differs from the prior one, the
982
+ // user explicitly switched modes — capture that as a distinct
983
+ // event so we can train on the conditions under which mode
984
+ // shifts happen.
985
+ if (previousMode && previousMode !== detectedMode) {
986
+ avaEvents.emit('mode_switch', {
987
+ from_mode: previousMode,
988
+ to_mode: detectedMode,
989
+ trigger: 'user_prefix',
990
+ });
991
+ }
992
+ // ── Dataset event: did the user just correct the prior turn? ──
993
+ // Fires at the START of the new trajectory, references the
994
+ // previous trajectory's id so training-time joins know which
995
+ // response was the wrong one.
996
+ const latestUser = this.findLatestNonMetaUserMessage(messages);
997
+ if (latestUser && this.lastTrajectoryMetadata) {
998
+ const correctionKind = categorizeCorrection(latestUser);
999
+ if (correctionKind) {
1000
+ avaEvents.emit('correction_received', {
1001
+ corrected_trajectory_id: this.lastTrajectoryMetadata.trajectory_id,
1002
+ original_verification: this.lastTrajectoryMetadata.verified,
1003
+ correction_signature: correctionKind,
1004
+ });
1005
+ }
1006
+ }
1007
+ try {
1008
+ const result = await this.runInner(messages, onEvent, signal);
1009
+ // Best-effort: pull the final assistant text for the chain-complete
1010
+ // summary. This is shape-only (word count, not content).
1011
+ for (let i = result.length - 1; i >= 0; i--) {
1012
+ if (result[i].role === 'assistant') {
1013
+ finalContent = getTextContent(result[i].content);
1014
+ break;
1015
+ }
1016
+ }
1017
+ return result;
1018
+ }
1019
+ finally {
1020
+ // ── Dataset event: did Ava verify before answering? ──────
1021
+ const verifTools = pickVerificationTools(traj.toolsSoFar);
1022
+ const responseWords = finalContent
1023
+ ? finalContent.trim().split(/\s+/).filter(Boolean).length
1024
+ : 0;
1025
+ avaEvents.emit('verification_decision', {
1026
+ verified: verifTools.length > 0,
1027
+ verification_tools_used: verifTools,
1028
+ response_word_count: responseWords,
1029
+ // Question signature is mode + question-mark presence — never
1030
+ // raw text. The mode is already on the envelope so this is a
1031
+ // small additional categorisation.
1032
+ question_signature: latestUser && /\?/.test(latestUser) ? 'question' : 'imperative',
1033
+ });
1034
+ // ── Dataset event: did the evidence-gathering actually succeed? ──
1035
+ // Complements verification_decision (which only says verification
1036
+ // was *attempted*). This is the verifiability signal — whether the
1037
+ // verify tools came back ok and whether the honesty gate flagged
1038
+ // an unbacked claim. Shape-only counts/booleans from runToolEvidence.
1039
+ const verifyEvidence = this.runToolEvidence.filter((e) => VERIFICATION_TOOLS.has(e.name));
1040
+ avaEvents.emit('verification_evidence', {
1041
+ verify_tool_calls: verifyEvidence.length,
1042
+ verify_tool_successes: verifyEvidence.filter((e) => e.ok).length,
1043
+ distinct_verify_tools: new Set(verifyEvidence.map((e) => e.name)).size,
1044
+ verified_before_response: verifyEvidence.length > 0,
1045
+ claim_flagged: this.runClaimFlagged,
1046
+ });
1047
+ avaEvents.emit('tool_chain_complete', {
1048
+ tool_count: traj.toolsSoFar.length,
1049
+ total_duration_ms: Date.now() - traj.startedAt,
1050
+ outcome: traj.outcome ?? 'task_completed',
1051
+ outcome_summary: summarizeChainOutcome(finalContent),
1052
+ });
1053
+ // ── Dataset event: continuation nudge outcome ───────────────
1054
+ // If a stall was detected and a nudge fired during this run,
1055
+ // report whether the nudge actually got things back on track.
1056
+ // Optimistic default (recovered=true) is set at fire time and
1057
+ // flipped to false by the fallback-exhausted branch.
1058
+ if (traj.pendingStallEventId) {
1059
+ avaEvents.emit('continuation_nudge_fired', {
1060
+ stall_event_id: traj.pendingStallEventId,
1061
+ nudge_action: 'forcing-prompt',
1062
+ recovered: traj.nudgeRecovered ?? true,
1063
+ });
1064
+ }
1065
+ // Stash this trajectory's metadata so the NEXT run can attach
1066
+ // correction_received events to it if the user pushes back.
1067
+ this.lastTrajectoryMetadata = {
1068
+ trajectory_id: traj.trajectory_id,
1069
+ verified: verifTools.length > 0,
1070
+ };
1071
+ }
1072
+ });
1073
+ }
1074
+ async runInner(messages, onEvent, signal) {
1075
+ // ─── History preservation across destructive context transforms ────────
1076
+ //
1077
+ // This function treats `messages` as a working context — the array
1078
+ // that gets sent to the model each iteration. Several transforms in
1079
+ // the loop below mutate `messages` destructively: compressContext()
1080
+ // replaces older turns with a summary, truncateMessages() drops
1081
+ // messages from the start to stay under the context window. These
1082
+ // mutations are correct for model context (saving tokens) but
1083
+ // WRONG for user history — if the return value reflects the
1084
+ // compressed state, the caller persists a lossy transcript to disk
1085
+ // and the user's scrollback disappears on next load.
1086
+ //
1087
+ // Fix: track what the user's history actually is separately from
1088
+ // the model's working context. `realEvents` collects messages
1089
+ // genuinely added this turn — assistant replies, tool results,
1090
+ // interjections. Destructive transforms intercept first so anything
1091
+ // between the last snapshot point and the transform gets absorbed
1092
+ // into realEvents before the transform mutates messages out from
1093
+ // under us. `isMetaPrefix` filters synthetic user-role injections
1094
+ // (iteration warnings, compression continuation headers,
1095
+ // task-re-injection blocks) so only real events land here.
1096
+ //
1097
+ // Agent.run returns only realEvents — the caller appends them to
1098
+ // its canonical conversation. Compression and truncation are now
1099
+ // strictly internal to the working context and cannot cross the
1100
+ // conversation boundary.
1101
+ const realEvents = [];
1102
+ let lastSnapshotOffset = messages.length;
1103
+ /** Pick up new non-meta messages since the last snapshot point. */
1104
+ const absorbSinceLastSnapshot = () => {
1105
+ if (messages.length > lastSnapshotOffset) {
1106
+ for (let i = lastSnapshotOffset; i < messages.length; i++) {
1107
+ const m = messages[i];
1108
+ const text = getTextContent(m.content);
1109
+ if (typeof text === 'string' && text.length > 0 && isMetaPrefix(text))
1110
+ continue;
1111
+ realEvents.push(m);
1112
+ }
1113
+ }
1114
+ };
1115
+ /**
1116
+ * Return ONLY the new messages produced this turn. Caller appends
1117
+ * to their conversation. See the Agent.run() docstring for why this
1118
+ * is the return shape — compression's destructive transforms cannot
1119
+ * leak across the conversation boundary when the agent explicitly
1120
+ * returns "what was new" rather than "what the full context now
1121
+ * looks like after compression."
1122
+ */
1123
+ const finalHistory = () => {
1124
+ absorbSinceLastSnapshot();
1125
+ return [...realEvents];
1126
+ };
1127
+ // Expose finalHistory as the recovery hook so cancelRun() in the host
1128
+ // can pull whatever Ava had accumulated when the user pressed Stop.
1129
+ // Reassigned every run; the previous run's reference becomes stale
1130
+ // (closes over a dead realEvents) but is never read again.
1131
+ this.currentRunRecoveryHook = finalHistory;
1132
+ this.repeatedToolFailures.clear();
1133
+ // ─── Recoverability backstop for compression ───────────────────────────
1134
+ // Stash the uncompressed transcript as it stands at the START of this turn
1135
+ // so the conversation_recall tool can read it. `messages` here is the full
1136
+ // canonical history the caller passed in; the compression/truncation
1137
+ // transforms below only ever REASSIGN the local `messages` variable (they
1138
+ // build new arrays, never mutate in place), so a shallow copy taken now
1139
+ // stays lossless for the whole turn even after the working context is
1140
+ // compressed. This is what makes "Ava lost context" structurally
1141
+ // impossible: the summary is the fast path, this is the source of truth.
1142
+ if (this.toolContext.sharedState) {
1143
+ this.toolContext.sharedState.recallTranscript = [...messages];
1144
+ }
1145
+ // ─── Stop-command detection ────────────────────────────────────────────
1146
+ // If the user's latest message is an explicit stop command ("stop",
1147
+ // "halt", "leave it", "don't touch", "how dare you i said stop", etc.),
1148
+ // DO NOT start a new task. Acknowledge and return immediately. The
1149
+ // previous run was already aborted by the signal; this new turn should
1150
+ // not re-engage with the work the user told us to leave alone.
1151
+ //
1152
+ // This is the architectural enforcement of Rule 10. The prompt rule
1153
+ // tells the model to stop; this code ensures the agent loop doesn't
1154
+ // even give the model a chance to decide otherwise.
1155
+ const earlyUserMsg = this.findLatestNonMetaUserMessage(messages);
1156
+ if (earlyUserMsg) {
1157
+ // A typed stop/pause directive aimed at Ava ("stop", "leave it", "wait",
1158
+ // "hold on") halts here, so the model never gets a chance to re-engage
1159
+ // work the user told us to drop. Shared with the mid-run path via
1160
+ // isStopCommand so a typed halt behaves identically whenever it arrives.
1161
+ if (isStopCommand(earlyUserMsg)) {
1162
+ const stopResponse = {
1163
+ role: 'assistant',
1164
+ content: 'Stopped. Not touching anything else. Let me know when you want to continue.',
1165
+ };
1166
+ messages = [...messages, stopResponse];
1167
+ onEvent({ type: 'stream_start' });
1168
+ onEvent({ type: 'stream_delta', content: stopResponse.content });
1169
+ onEvent({ type: 'stream_end', message: stopResponse });
1170
+ onEvent({ type: 'done', finalMessage: stopResponse });
1171
+ return finalHistory();
1172
+ }
1173
+ }
1174
+ // ─── Post-stop context restriction ─────────────────────────────────────
1175
+ // When the user pressed Stop and has now sent a new message, the full
1176
+ // prior conversation (could be 150K+ tokens) is still in `messages`.
1177
+ // Sending all of that back to the model means it draws on the prior
1178
+ // task context and continues the work the user asked us to stop.
1179
+ //
1180
+ // Fix: for the first turn after a stop marker, strip everything
1181
+ // between the system message and the marker. Keep:
1182
+ // - system prompt (with marker content merged in as a directive)
1183
+ // - the new user message (the only non-meta user message after the marker)
1184
+ //
1185
+ // After this turn, normal accumulation resumes. Subsequent turns see
1186
+ // the post-stop conversation as a fresh sub-thread — no leakage from
1187
+ // the terminated task.
1188
+ //
1189
+ // Pairs with the intent gate (Fix E): if the post-stop message is
1190
+ // short/conversational, the intent gate will disable tools too.
1191
+ // Combined effect: user presses Stop → types something → Ava responds
1192
+ // on the user's actual terms, not the prior task's terms.
1193
+ messages = this.maybeRestrictPostStopContext(messages);
1194
+ // ─── Classify this task for directness discipline ─────────────────────
1195
+ // Find the latest non-meta user message and run the lightweight
1196
+ // classifier. The result sets the exploration budget for this run and
1197
+ // is injected into the system prompt as a directness hint so Ava knows
1198
+ // up front how aggressively to scope her work.
1199
+ //
1200
+ // Reset exploration budget state on each run — it's per-task, not
1201
+ // per-session.
1202
+ this.readCountBeforeFirstWrite = 0;
1203
+ this.hasWrittenInThisRun = false;
1204
+ this.explorationNudgeFired = false;
1205
+ // Closure fallback state — if the agent exits the main loop with an
1206
+ // empty final assistant message, we try once more with a forcing
1207
+ // "one-sentence summary" nudge. Prevents the "she didn't say anything
1208
+ // to close out" failure where the model terminates cleanly but leaves
1209
+ // the user staring at a wall of tool calls with no visible confirmation.
1210
+ let closureFallbackAttempted = false;
1211
+ // Pre-closure verify guard — bounded to MAX_CLOSURE_VERIFY cycles per run.
1212
+ // One cycle isn't enough: when the first verify fails and the model fixes
1213
+ // it, that *recovery* edit must itself be re-verified — otherwise the fix
1214
+ // sails through unchecked. A small cap (not a single boolean) lets the fix
1215
+ // be checked while still guaranteeing exit so an unfixable verify can't
1216
+ // trap a turn. The fresh-eyes/signature escalation still fires on repeated
1217
+ // same-cause failures within these cycles.
1218
+ const MAX_CLOSURE_VERIFY = 3;
1219
+ let closureVerifyCount = 0;
1220
+ const latestUserMessage = this.findLatestNonMetaUserMessage(messages);
1221
+ if (latestUserMessage) {
1222
+ const classification = classifyTaskComplexity(latestUserMessage);
1223
+ this.currentTaskComplexity = classification.complexity;
1224
+ logger.debug(`[agent] Task classified as ${classification.complexity} (${classification.confidence} confidence) — ${classification.reasoning}`);
1225
+ // Merge directness hint into the first system message. This keeps the
1226
+ // hint anchored to the session identity rather than floating as a
1227
+ // separate message that could be compressed away.
1228
+ const hint = formatDirectnessHint(classification);
1229
+ messages = this.appendToSystemMessage(messages, `\n\n${hint}`);
1230
+ }
1231
+ else {
1232
+ // No user task — default to moderate budget just in case.
1233
+ this.currentTaskComplexity = 'moderate';
1234
+ }
1235
+ // Detect mode early — needed for tool filtering downstream.
1236
+ const detectedMode = this.detectModeForSurface(messages);
1237
+ // Knowledge-pack auto-activation removed in v0.59.2. The 12 builtin
1238
+ // domain packs (marketing, finance, devops, etc.) added ~750-1000
1239
+ // tokens of static framework guidance per matched keyword, which
1240
+ // frontier models like Qwen 3.7 Plus / DeepSeek V4 Pro / Mistral
1241
+ // Large 3 already cover from training. After the chat-tier
1242
+ // rebalance, that silent injection started bumping ~1-credit chat
1243
+ // turns into the next bracket without the user asking for it.
1244
+ // Net: small lift on rare turns, opaque cost on every match.
1245
+ // Removed wholesale; the desktop-automation knowledge under the
1246
+ // same module survives because it's genuine model-novel content.
1247
+ // ─── Intent nudge (Qwen Flash classifier) ─────────────────────────────
1248
+ // Soft preference, not a hard gate. Classifies the user's message as
1249
+ // task/conversational/ambiguous and injects a brief guidance nudge
1250
+ // into the system prompt. Tools remain available in all cases — the
1251
+ // nudge shapes the default response style, but the model retains
1252
+ // judgment to call tools when the request clearly warrants action.
1253
+ //
1254
+ // Why soft instead of hard: hard blocks fail catastrophically when
1255
+ // the classifier is wrong (model can't use tools on a real task,
1256
+ // users see "Tools are disabled this turn" leaking into output).
1257
+ // Soft nudges fail gracefully — false positives waste a few tokens,
1258
+ // false negatives still let the model do the right thing.
1259
+ //
1260
+ // The nudge wording is intentionally generic and non-recitable so
1261
+ // the model won't quote it back to users.
1262
+ let userIntent = 'task';
1263
+ const intentClassifier = this.toolContext.sharedState?.intentClassifier;
1264
+ if (intentClassifier && latestUserMessage) {
1265
+ try {
1266
+ userIntent = await intentClassifier.classify(latestUserMessage);
1267
+ logger.info(`[agent] Intent classified as '${userIntent}' for message: "${latestUserMessage.slice(0, 80)}"`);
1268
+ }
1269
+ catch {
1270
+ userIntent = 'task';
1271
+ }
1272
+ }
1273
+ const useNativeTools = this.model.supportsToolCalls !== false;
1274
+ const allSchemas = this.toolRegistry.getSchemas();
1275
+ // Mode-aware filtering: restrict tool schemas to only those allowed in
1276
+ // the active mode. When the user message lacks an explicit mode prefix
1277
+ // (default work / code path on both surfaces), the per-mode allowlist
1278
+ // can't apply — but we still need to keep desktop-only tools out of
1279
+ // non-desktop turns, otherwise the model can hallucinate desktop_*
1280
+ // calls in code mode and the gate noise leaks into a coding turn.
1281
+ const modeAllowed = detectedMode ? MODE_ALLOWED_TOOLS[detectedMode] : null;
1282
+ let filteredSchemas;
1283
+ if (modeAllowed) {
1284
+ filteredSchemas = allSchemas.filter(s => modeAllowed.has(s.function.name) || ALWAYS_ALLOWED_TOOLS.has(s.function.name));
1285
+ }
1286
+ else {
1287
+ // No prefix detected. Filter only the desktop-only tools so the
1288
+ // surgical leak closes without tightening any other tool the user
1289
+ // may have been calling in unprefixed turns (journal_write, weather,
1290
+ // etc. — those stay available exactly as before).
1291
+ filteredSchemas = allSchemas.filter(s => !DESKTOP_ONLY_TOOLS.has(s.function.name));
1292
+ }
1293
+ // Tools always available when the model supports them. Intent shapes
1294
+ // the response style via the nudge below, not via schema removal.
1295
+ const toolSchemas = useNativeTools ? filteredSchemas : [];
1296
+ if (userIntent === 'conversational') {
1297
+ // Brief, generic guidance. Not framed as a command so the model
1298
+ // internalises it rather than quoting it back to the user.
1299
+ messages = this.appendToSystemMessage(messages, `\n\nStyle note: this turn's message reads conversational. Lead with a worded reply. Reach for tools only if the request clearly requires concrete action on files, commands, or the project.`);
1300
+ }
1301
+ else if (userIntent === 'ambiguous') {
1302
+ messages = this.appendToSystemMessage(messages, `\n\nStyle note: this turn's message is ambiguous in intent. If you are not sure what action is wanted, ask a short clarifying question before acting. Tools remain available if action is clearly warranted.`);
1303
+ }
1304
+ logger.info(`[agent] Starting run: model=${this.model.id} supportsToolCalls=${useNativeTools} toolSchemas=${toolSchemas.length} intent=${userIntent}${detectedMode ? ` mode=${detectedMode}` : ''}`);
1305
+ // For models without native tool_calls, inject tool descriptions into the system prompt
1306
+ if (!useNativeTools && filteredSchemas.length > 0) {
1307
+ const toolPrompt = buildToolPrompt(filteredSchemas);
1308
+ const firstMsg = messages[0];
1309
+ if (firstMsg?.role === 'system') {
1310
+ messages = [
1311
+ { ...firstMsg, content: firstMsg.content + '\n\n' + toolPrompt },
1312
+ ...messages.slice(1),
1313
+ ];
1314
+ }
1315
+ else {
1316
+ messages = [
1317
+ { role: 'system', content: toolPrompt },
1318
+ ...messages,
1319
+ ];
1320
+ }
1321
+ }
1322
+ // Pass signal to tool execution context so tools (esp. bash) can be cancelled
1323
+ const runContext = { ...this.toolContext, signal };
1324
+ let iterations = 0;
1325
+ this.pauseRequested = false; // clear any stale pause from a prior run
1326
+ this._inThinkTag = false;
1327
+ let warningInjected = false;
1328
+ let lastToolName = null;
1329
+ let repeatCount = 0;
1330
+ const MAX_SAME_TOOL_REPEATS = 3;
1331
+ while (iterations < MAX_TOOL_CALL_ITERATIONS) {
1332
+ iterations++;
1333
+ logger.debug(`[agent] ── Iteration ${iterations}/${MAX_TOOL_CALL_ITERATIONS} ── messages=${messages.length}`);
1334
+ // ── Sliding Window — compress old messages when context is genuinely full ─
1335
+ //
1336
+ // Previously this fired whenever non-system message count exceeded
1337
+ // 30, regardless of token usage. A single task with tool-use can
1338
+ // produce 30+ messages in 3-5 user turns (each turn = user message +
1339
+ // assistant messages + tool results). That made compression fire at
1340
+ // ~2% token usage, destabilising the conversation every few turns
1341
+ // and causing Ava to lose context mid-task.
1342
+ //
1343
+ // New rule: the window compresses only when BOTH conditions are
1344
+ // true — message count is very high AND estimated tokens cross a
1345
+ // meaningful threshold. Pure message count is no longer a trigger.
1346
+ // Token-based thresholds compress when there's a real reason to,
1347
+ // not on a schedule. The absolute token check below (at 70% of
1348
+ // context) is the primary gate; this one is a secondary safety net
1349
+ // for pathological cases with tons of tiny messages.
1350
+ const WINDOW_MAX = 120; // Only extreme message counts hit this path
1351
+ const WINDOW_KEEP = 24; // Keep more recent context when it does
1352
+ const nonSystem = messages.filter(m => m.role !== 'system');
1353
+ const estimatedTokensForWindow = this.estimateTokenCount(messages);
1354
+ const windowTokenFloor = Math.floor(this.model.contextWindow * 0.5);
1355
+ if (nonSystem.length > WINDOW_MAX && estimatedTokensForWindow > windowTokenFloor) {
1356
+ const systemMsgs = messages.filter(m => m.role === 'system');
1357
+ const pinnedIdxFull = findOriginalUserTaskIndex(messages);
1358
+ const pinnedMsg = pinnedIdxFull !== -1 ? messages[pinnedIdxFull] : null;
1359
+ const toCompress = nonSystem.slice(0, nonSystem.length - WINDOW_KEEP);
1360
+ const toKeep = nonSystem.slice(nonSystem.length - WINDOW_KEEP);
1361
+ // Reference-equality check — cast to Message[] because toKeep's type
1362
+ // is narrowed by the system filter and doesn't accept Message directly.
1363
+ const pinnedInKeep = pinnedMsg ? toKeep.indexOf(pinnedMsg) !== -1 : false;
1364
+ // Memory policy: conversation content does NOT get saved to memory
1365
+ // here. Compression is a working-state operation — the summary
1366
+ // belongs in the conversation history (persisted per-conversation
1367
+ // in ~/.ava/history/*.json), not in user or project memory.
1368
+ //
1369
+ // Previously this block called autoExtractAndSave on the compressed
1370
+ // messages AND saved the raw summary as a "[Session context]"
1371
+ // project memory entry. Both were category errors: memory should
1372
+ // be distilled, durable facts about the user or project — not
1373
+ // conversation transcripts that get re-injected on later turns and
1374
+ // create a self-referential feedback loop.
1375
+ //
1376
+ // If something in the compressed context was worth remembering,
1377
+ // the model already had the chance to call memory_save during the
1378
+ // turn that produced it. Ambient extraction from compressed logs
1379
+ // is not the mechanism for durable memory.
1380
+ // Rebuild messages: system (with compression note merged in) + pinned
1381
+ // original task (if not already in the kept window) + recent messages.
1382
+ // Merging the compression note into the first system message avoids
1383
+ // Qwen's "system must be at beginning" error.
1384
+ const fixedKeep = this.fixToolPairing(toKeep);
1385
+ const compressionNote = [
1386
+ `[${toCompress.length} earlier messages compressed out of your working context. Your active task is still in flight — continue from where you left off. Do NOT treat this as a new conversation.]`,
1387
+ 'The full transcript of those messages is still on record.',
1388
+ 'If the user references something from earlier — or you need an exact detail, decision, path or value — call conversation_recall to read it from the real transcript instead of guessing.',
1389
+ 'Do NOT say you don\'t have context — recall it first. Do NOT greet the user.',
1390
+ ].join(' ');
1391
+ // Session tasks re-injection — same pattern as compressContext()
1392
+ let slidingTaskBlock = null;
1393
+ try {
1394
+ const tm = this.toolContext.sharedState?.taskManager;
1395
+ if (tm && typeof tm.getSessionTasks === 'function') {
1396
+ const block = formatSessionTasksBlock(tm.getSessionTasks());
1397
+ if (block)
1398
+ slidingTaskBlock = { role: 'user', content: block };
1399
+ }
1400
+ }
1401
+ catch { /* non-critical */ }
1402
+ // Pinned original task: previously this re-injected the original
1403
+ // user message VERBATIM as a user-role message. The model saw
1404
+ // what looked like a freshly-sent user turn and responded to it
1405
+ // as if it were new input — the classic "acts on the initial
1406
+ // message again" bug after compression. Fix: fold the original
1407
+ // task text into the system prompt's compression note as a
1408
+ // reference ("the user's original ask was X"), never as a
1409
+ // replayed user turn. The model knows the task context without
1410
+ // interpreting the replay as a new request.
1411
+ let pinnedNote = '';
1412
+ if (pinnedMsg && !pinnedInKeep) {
1413
+ const pinnedText = getTextContent(pinnedMsg.content);
1414
+ if (pinnedText) {
1415
+ pinnedNote = `\n\n[Original request at session start] "${pinnedText.slice(0, 800)}" — this is context for what the user initially asked. You were already in the middle of working on this; continue from where you left off. Do NOT treat this as a new request.`;
1416
+ }
1417
+ }
1418
+ const tail = slidingTaskBlock ? [slidingTaskBlock, ...fixedKeep] : fixedKeep;
1419
+ const mergedNote = compressionNote + pinnedNote;
1420
+ if (systemMsgs.length > 0) {
1421
+ const primary = systemMsgs[0];
1422
+ const mergedSystem = { ...primary, content: (typeof primary.content === 'string' ? primary.content : '') + '\n\n' + mergedNote };
1423
+ messages = [mergedSystem, ...tail];
1424
+ }
1425
+ else {
1426
+ messages = [
1427
+ { role: 'system', content: mergedNote },
1428
+ ...tail,
1429
+ ];
1430
+ }
1431
+ // Notify UI about compression (uses 'info' event type)
1432
+ logger.debug(`[agent] Sliding window: compressed ${toCompress.length} messages, kept ${fixedKeep.length}`);
1433
+ }
1434
+ // Check for cancellation before each iteration
1435
+ if (signal?.aborted) {
1436
+ onEvent({ type: 'done', finalMessage: { role: 'assistant', content: null } });
1437
+ return finalHistory();
1438
+ }
1439
+ // Check for user interjections — messages the user sent mid-run.
1440
+ // Frame them neutrally: enough signal that this arrived WHILE Ava was
1441
+ // working (so she folds it into the current task instead of treating it
1442
+ // as a brand-new request) without the corrective tone of the old
1443
+ // "[User interjection]:" prefix, which primed her to read questions as
1444
+ // criticism and apologise instead of answering. The UI event still
1445
+ // carries the raw text — the frame is for the model only.
1446
+ while (this.pendingInterjections.length > 0) {
1447
+ const interjection = this.pendingInterjections.shift();
1448
+ messages = [
1449
+ ...messages,
1450
+ {
1451
+ role: 'user',
1452
+ content: `[The user added this while you were working — take it into account and carry on]: ${interjection}`,
1453
+ },
1454
+ ];
1455
+ onEvent({ type: 'interjection', content: interjection });
1456
+ }
1457
+ // Graceful pause — the user typed "wait"/"pause" mid-run. We're at a
1458
+ // clean step boundary (the previous step's tools have finished), so end
1459
+ // the turn here rather than aborting mid-step. Everything completed
1460
+ // stays in the conversation; the user's next message continues from
1461
+ // here. Distinct from the hard stop (abort signal), which is immediate.
1462
+ if (this.pauseRequested) {
1463
+ this.pauseRequested = false;
1464
+ logger.info('[agent] Graceful pause requested — halting at step boundary');
1465
+ onEvent({ type: 'done', finalMessage: { role: 'assistant', content: null } });
1466
+ return finalHistory();
1467
+ }
1468
+ iterations++;
1469
+ // Warn the model when approaching the iteration limit
1470
+ // Injected as a user-role message to avoid Qwen's "system must be at beginning" error
1471
+ const remaining = MAX_TOOL_CALL_ITERATIONS - iterations;
1472
+ if (!warningInjected && remaining <= ITERATION_WARNING_THRESHOLD) {
1473
+ warningInjected = true;
1474
+ messages = [
1475
+ ...messages,
1476
+ {
1477
+ role: 'user',
1478
+ content: `[System notice]: ${t('error.msg.iteration_warning', { remaining: String(remaining) })}`,
1479
+ },
1480
+ ];
1481
+ }
1482
+ // Trim old tool results to save tokens — but ONLY when context is
1483
+ // genuinely filling up. Trimming on raw message count (regardless of how
1484
+ // much window is free) crushed files the model still needed on multi-file
1485
+ // tasks: it would lose a file read a few turns ago, re-read it, lose
1486
+ // another, and loop forever without ever reaching an edit. Gate on the
1487
+ // token budget so we keep full tool results while there's plenty of room
1488
+ // (the common case) and only collapse them once we cross half the window.
1489
+ const trimThreshold = Math.floor(this.model.contextWindow * 0.5);
1490
+ if (this.estimateTokenCount(messages) > trimThreshold) {
1491
+ messages = this.trimOldToolResults(messages);
1492
+ }
1493
+ // Auto-compress at 70% of the model's context window, capped at
1494
+ // 400K tokens as an absolute ceiling.
1495
+ //
1496
+ // The 70% ratio is preserved for small-to-mid-sized context models
1497
+ // (128K → ~90K trigger, 256K → ~180K trigger) because the author
1498
+ // deliberately chose it to avoid compression thrash: each
1499
+ // compression pass is an LLM call that can destabilise a session
1500
+ // ("acts on the initial message again" regression). Firing too
1501
+ // eagerly is worse than firing late.
1502
+ //
1503
+ // The 400K ceiling fixes the 1M-context degenerate case — at 70%
1504
+ // of 1M every turn would send up to 700K tokens before anything
1505
+ // got summarised. On managed Qwen Plus ($0.20 / $1.20 per 1M),
1506
+ // that's ~$0.14 per turn of raw input cost, and 700K of context
1507
+ // slows every response substantially. Capping at 400K means one
1508
+ // earlier compression pass vs. carrying an extra 300K per turn
1509
+ // for 20+ turns.
1510
+ //
1511
+ // Math: trigger = min(contextWindow × 0.7, 400_000)
1512
+ const maxInputTokens = Math.min(Math.floor(this.model.contextWindow * 0.7), 400_000);
1513
+ const estimatedTotal = this.estimateTokenCount(messages);
1514
+ // Emit context usage so UIs can show a progress bar
1515
+ const contextPercent = Math.round((estimatedTotal / this.model.contextWindow) * 100);
1516
+ onEvent({
1517
+ type: 'context_usage',
1518
+ context: { used: estimatedTotal, limit: this.model.contextWindow, percent: contextPercent },
1519
+ });
1520
+ // Auto-compress only when we've crossed the 70% threshold AND the
1521
+ // conversation is long enough that compression has something to
1522
+ // work with (< 6 messages means there's nothing meaningful to
1523
+ // summarise — just skip).
1524
+ if (estimatedTotal > maxInputTokens && messages.length >= 6) {
1525
+ // Destructive transform — absorb any real events added since the
1526
+ // last snapshot point BEFORE the transform mutates messages, then
1527
+ // reset the offset so further additions are tracked from the new
1528
+ // post-compression length.
1529
+ absorbSinceLastSnapshot();
1530
+ const msgsBeforeCompress = messages.length;
1531
+ messages = await this.compressContext(messages, onEvent, signal);
1532
+ lastSnapshotOffset = messages.length;
1533
+ // ── Dataset event: context compression fired ────────────────────
1534
+ avaEvents.emit('context_compression', {
1535
+ operation: 'compress',
1536
+ messages_before: msgsBeforeCompress,
1537
+ messages_after: messages.length,
1538
+ tokens_before: estimatedTotal,
1539
+ token_budget: maxInputTokens,
1540
+ });
1541
+ }
1542
+ // Still over budget? Fall back to truncation.
1543
+ //
1544
+ // Previously this emitted a user-facing error telling them to
1545
+ // "Consider starting a new chat for best results" — which was both
1546
+ // misleading (compression is routine, not an error) and risky (if
1547
+ // the agent ever saw that wording in its own context, it could
1548
+ // interpret "start a new chat" as instruction and reset its
1549
+ // behaviour, which is exactly the "she acted like it was a new
1550
+ // chat" failure mode we're fixing).
1551
+ //
1552
+ // Now it emits a neutral info message that doesn't prompt the user
1553
+ // or the agent to abandon the session. The agent's active task
1554
+ // state is preserved via the pinned original user task, the
1555
+ // re-injected session tasks block, and the continuation-first
1556
+ // compression header elsewhere in this file.
1557
+ const preCount = messages.length;
1558
+ // Truncation is also destructive — same snapshot/reset pattern.
1559
+ absorbSinceLastSnapshot();
1560
+ messages = this.truncateMessages(messages, maxInputTokens);
1561
+ lastSnapshotOffset = messages.length;
1562
+ const dropped = preCount - messages.length;
1563
+ if (dropped > 0) {
1564
+ // ── Dataset event: fell back to truncation ──────────────────────
1565
+ avaEvents.emit('context_compression', {
1566
+ operation: 'truncate',
1567
+ messages_before: preCount,
1568
+ messages_after: messages.length,
1569
+ tokens_before: estimatedTotal,
1570
+ token_budget: maxInputTokens,
1571
+ });
1572
+ onEvent({
1573
+ type: 'error',
1574
+ error: Object.assign(new Error(`Context compressed: ${dropped} older messages summarised to memory. Continuing your current task.`), { code: 'context_compressed' }),
1575
+ });
1576
+ }
1577
+ // ── Sanitize messages for model compatibility ──────────────────────────
1578
+ const filteredMessages = !useNativeTools
1579
+ ? messages.filter((m) => m.role !== 'tool') // Drop any stray tool messages in text mode
1580
+ : messages;
1581
+ // Vision bridge — when the coordinator can't see images but a vision
1582
+ // provider is configured (e.g. Supernova/DeepSeek with Qwen Omni), describe
1583
+ // every image FIRST (async, cached per image) so the model gets the image
1584
+ // as text instead of a "switch model" nag. This is what lets DeepSeek "see".
1585
+ // No-op for vision-capable models. See agent/vision-bridge.ts.
1586
+ const bridgedMessages = await bridgeImagesForTextModel(filteredMessages, this.model, this.visionProvider, this.visionModel, this.visionDescriptionCache);
1587
+ let sanitizedMessages = bridgedMessages.map((m) => {
1588
+ let msg = m;
1589
+ // Strip empty tool_calls arrays from assistant messages. Qwen
1590
+ // rejects `tool_calls: []` with a 400 error — the field must be
1591
+ // either omitted or non-empty. Upstream mutations (mode blocking,
1592
+ // budget enforcement, text-parser fallbacks) can leave an empty
1593
+ // array on the message; this is the architectural guard at the
1594
+ // API boundary so any future code path that reintroduces the bug
1595
+ // gets caught here before it reaches the provider.
1596
+ if (msg.role === 'assistant') {
1597
+ const asst = msg;
1598
+ if (Array.isArray(asst.tool_calls) && asst.tool_calls.length === 0) {
1599
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
1600
+ const { tool_calls: _empty, ...rest } = asst;
1601
+ msg = rest;
1602
+ }
1603
+ }
1604
+ // Text-based tool mode: strip tool_calls from assistant messages
1605
+ // The model doesn't understand these fields — they're our internal bookkeeping
1606
+ if (!useNativeTools && msg.role === 'assistant' && msg.tool_calls) {
1607
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
1608
+ const { tool_calls: _tc, ...rest } = msg;
1609
+ msg = rest;
1610
+ }
1611
+ // Handle reasoning_content based on model capability:
1612
+ // - Thinking models (DeepSeek Reasoner, etc.): KEEP — required for multi-turn
1613
+ // - Non-thinking models: STRIP — providers reject it as input
1614
+ if (msg.role === 'assistant' && 'reasoning_content' in msg) {
1615
+ const aMsg = msg;
1616
+ if (this.model.supportsThinking) {
1617
+ if (aMsg.reasoning_content && !aMsg.content) {
1618
+ return { ...aMsg, content: '' };
1619
+ }
1620
+ return msg;
1621
+ }
1622
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
1623
+ const { reasoning_content: _rc, ...rest } = aMsg;
1624
+ return rest;
1625
+ }
1626
+ return msg;
1627
+ });
1628
+ // Ensure all messages have string content and strip ANSI escape codes
1629
+ // Qwen rejects content: null and ANSI codes with 400 Bad Request
1630
+ sanitizedMessages = sanitizedMessages.map(m => {
1631
+ if (m.content === null || m.content === undefined) {
1632
+ return { ...m, content: '' };
1633
+ }
1634
+ if (typeof m.content === 'string') {
1635
+ // Strip all ANSI escape sequences and control characters that APIs reject.
1636
+ // no-control-regex is off for this block on purpose: matching control
1637
+ // characters IS the job here, and removing them from the pattern would
1638
+ // stop the sanitiser doing anything.
1639
+ /* eslint-disable no-control-regex */
1640
+ const cleaned = m.content
1641
+ .replace(/\u001b\[[0-9;]*[a-zA-Z]/g, '') // Standard ANSI escape codes
1642
+ .replace(/\u001b\][^\u0007]*\u0007/g, '') // OSC sequences
1643
+ .replace(/[\u0000-\u0008\u000B\u000C\u000E-\u001F]/g, ''); // Control chars (keep \n \r \t)
1644
+ /* eslint-enable no-control-regex */
1645
+ if (cleaned !== m.content)
1646
+ return { ...m, content: cleaned };
1647
+ }
1648
+ return m;
1649
+ });
1650
+ // Age out old content before sending — biggest token lever in the
1651
+ // agent loop. Without this, every screenshot and every verbose tool
1652
+ // result stays in full fidelity for the rest of the session, costing
1653
+ // 20-50K tokens per image × turns remaining and 1-5K tokens per
1654
+ // stale tool result × turns remaining. The model's prior reasoning
1655
+ // about these is preserved in the assistant messages; the raw
1656
+ // payload almost never adds value after 2-3 turns.
1657
+ sanitizedMessages = this.ageHistoryContent(sanitizedMessages);
1658
+ // Fix orphaned tool messages before sending — prevents 400 errors
1659
+ sanitizedMessages = this.fixToolPairing(sanitizedMessages);
1660
+ // Guard against 413: check estimated body size and truncate if too large
1661
+ // Most APIs reject bodies over 4MB. Target 3MB to leave headroom.
1662
+ const MAX_BODY_BYTES = 3 * 1024 * 1024;
1663
+ let finalMessages = sanitizedMessages;
1664
+ const estimatedSize = JSON.stringify(sanitizedMessages).length;
1665
+ if (estimatedSize > MAX_BODY_BYTES) {
1666
+ logger.warn(`[agent] Request body too large (${(estimatedSize / 1024 / 1024).toFixed(1)}MB). Truncating tool results and old messages.`);
1667
+ // First pass: truncate large tool results (keep first 500 chars)
1668
+ finalMessages = finalMessages.map(m => {
1669
+ if (m.role === 'tool' && typeof m.content === 'string' && m.content.length > 500) {
1670
+ return { ...m, content: m.content.slice(0, 500) + '\n\n[Output truncated — original was ' + m.content.length + ' chars]' };
1671
+ }
1672
+ return m;
1673
+ });
1674
+ // Second pass: if still too large, drop oldest messages (keep system + last 20)
1675
+ if (JSON.stringify(finalMessages).length > MAX_BODY_BYTES) {
1676
+ const systemMsg = finalMessages.find(m => m.role === 'system');
1677
+ const nonSystem = finalMessages.filter(m => m.role !== 'system');
1678
+ const dropped = nonSystem.slice(0, -20);
1679
+ const kept = nonSystem.slice(-20);
1680
+ // Fix orphaned tool messages after truncation
1681
+ const fixedKept = this.fixToolPairing(kept);
1682
+ finalMessages = systemMsg ? [systemMsg, ...fixedKept] : fixedKept;
1683
+ logger.warn(`[agent] Aggressive truncation: kept system + last ${fixedKept.length} messages, dropped ${dropped.length}`);
1684
+ // Memory policy: dropped conversation context does NOT get saved
1685
+ // to memory. It lives in the conversation history file on disk
1686
+ // (per-conversation, persistent) and that's where it belongs.
1687
+ // Previously this saved a concatenated transcript of dropped
1688
+ // messages to 'global' memory under a 'session' category —
1689
+ // exactly the conversation-into-memory leak we are cutting.
1690
+ }
1691
+ }
1692
+ const request = {
1693
+ model: this.model.id,
1694
+ messages: finalMessages,
1695
+ tools: toolSchemas.length > 0 ? toolSchemas : undefined,
1696
+ tool_choice: toolSchemas.length > 0 ? 'auto' : undefined,
1697
+ stream: true,
1698
+ // Every iteration of this loop is another call serving the SAME user
1699
+ // turn. Tagging them all with one id is what lets a turn be costed as
1700
+ // an outcome rather than as N unrelated calls.
1701
+ turnId: this.runTurnId,
1702
+ };
1703
+ let assistantMessage;
1704
+ let promptTokens;
1705
+ let streamInterrupted;
1706
+ const estimatedInput = this.estimateTokenCount(messages);
1707
+ logger.debug(`[agent] Calling streamResponse (est. ${estimatedInput} input tokens, model context: ${this.model.contextWindow})`);
1708
+ try {
1709
+ const streamResult = await this.streamResponse(request, onEvent, signal);
1710
+ assistantMessage = streamResult.message;
1711
+ promptTokens = streamResult.promptTokens;
1712
+ streamInterrupted = streamResult.interrupted === true;
1713
+ logger.debug(`[agent] streamResponse returned: content=${assistantMessage.content?.length ?? 0} chars, tool_calls=${assistantMessage.tool_calls?.length ?? 0}, promptTokens=${promptTokens}${streamInterrupted ? ' (INTERRUPTED by injection)' : ''}`);
1714
+ }
1715
+ catch (error) {
1716
+ logger.error(`[agent] streamResponse THREW: ${error instanceof Error ? error.message : String(error)}`);
1717
+ // Surface the error through the event system so CLI/extension handle it consistently
1718
+ onEvent({ type: 'error', error: error instanceof Error ? error : new Error(String(error)) });
1719
+ // Always emit done so UI clears isStreaming/isThinking
1720
+ onEvent({ type: 'done', finalMessage: { role: 'assistant', content: '' } });
1721
+ return finalHistory();
1722
+ }
1723
+ // Mid-stream injection happened. We aborted the provider request
1724
+ // before tool_calls could start. Preserve any partial text in the
1725
+ // transcript (keeps UI/history honest — user saw it) but skip both
1726
+ // the text-based tool parser (partial <tool_call> blocks would
1727
+ // mis-parse) and the full tool-execution path. The outer loop will
1728
+ // drain pendingInterjections at the top of the next iteration and
1729
+ // send a fresh request to the model.
1730
+ if (streamInterrupted) {
1731
+ const hasText = typeof assistantMessage.content === 'string' && assistantMessage.content.trim().length > 0;
1732
+ if (hasText) {
1733
+ messages = [...messages, {
1734
+ role: 'assistant',
1735
+ content: assistantMessage.content,
1736
+ ...(assistantMessage.reasoning_content ? { reasoning_content: assistantMessage.reasoning_content } : {}),
1737
+ }];
1738
+ }
1739
+ continue;
1740
+ }
1741
+ // Text-based tool parsing: extract <tool_call> blocks from the model's text
1742
+ if (!useNativeTools && assistantMessage.content) {
1743
+ const { toolCalls: parsedCalls, cleanText } = parseToolCalls(assistantMessage.content);
1744
+ if (parsedCalls.length > 0) {
1745
+ logger.debug(`[agent] Parsed ${parsedCalls.length} tool calls from text output`);
1746
+ assistantMessage = {
1747
+ ...assistantMessage,
1748
+ content: cleanText || null,
1749
+ tool_calls: parsedCalls,
1750
+ };
1751
+ }
1752
+ }
1753
+ messages = [...messages, assistantMessage];
1754
+ // NOTE: Do NOT truncate here — tool results haven't been appended yet.
1755
+ // Truncation between assistant tool_calls and tool results breaks the
1756
+ // message ordering that models require. Truncation happens after tool
1757
+ // results are appended, at the top of the next loop iteration.
1758
+ //
1759
+ // The truncation call that used to sit here was disabled with
1760
+ // `if (false && ...)` rather than removed. Deleted now: the note above
1761
+ // is the part worth keeping, and dead code behind a constant false is
1762
+ // code that still has to compile, still gets read as if it might run,
1763
+ // and cannot be tested.
1764
+ // If cancelled during streaming, stop immediately
1765
+ if (signal?.aborted) {
1766
+ onEvent({ type: 'done', finalMessage: assistantMessage });
1767
+ return finalHistory();
1768
+ }
1769
+ // ─── A written tool call is still a tool call ──────────────────────
1770
+ // Some models emit <present_plan>{…}</present_plan> as TEXT instead of
1771
+ // calling the tool, even with native schemas offered. Left alone the user
1772
+ // gets raw JSON where a plan card belongs, and the turn closes cleanly
1773
+ // because the agent sees no tool_calls. Recovery is reliable where
1774
+ // instructing the model is not — the models that do this are the ones
1775
+ // least likely to follow an instruction about it.
1776
+ if ((!assistantMessage.tool_calls || assistantMessage.tool_calls.length === 0) &&
1777
+ typeof assistantMessage.content === 'string') {
1778
+ const offered = new Set(toolSchemas.map((t) => t.function.name));
1779
+ const recovered = recoverWrittenToolCalls(assistantMessage.content, offered);
1780
+ if (recovered.calls.length > 0) {
1781
+ logger.warn(`[agent] Recovered ${recovered.calls.length} written tool call(s) from text: ` +
1782
+ recovered.calls.map((c) => c.function.name).join(', '));
1783
+ assistantMessage.tool_calls = recovered.calls;
1784
+ assistantMessage.content = recovered.text;
1785
+ }
1786
+ }
1787
+ if (!assistantMessage.tool_calls || assistantMessage.tool_calls.length === 0) {
1788
+ logger.debug(`[agent] No tool_calls in response. content=${(assistantMessage.content ?? '').length} chars, reasoning=${(assistantMessage.reasoning_content ?? '').length} chars`);
1789
+ // ─── Pre-closure verify guard (universal) ─────────────────────
1790
+ // If the trajectory has unverified file edits, run verify_change
1791
+ // before allowing the turn to end. On pass, files move to
1792
+ // verifiedFiles and closure proceeds. On fail, inject the
1793
+ // failure report (or a fresh-eyes review if we've looped on
1794
+ // the same root cause) as user-role context and re-enter the
1795
+ // loop. Bounded to MAX_CLOSURE_VERIFY cycles per run via
1796
+ // closureVerifyCount — re-verifies recovery fixes, still guaranteed
1797
+ // to exit so an unfixable verify can't trap a turn.
1798
+ //
1799
+ // Lives in the universal agent loop, NOT AutoCoordinator, so
1800
+ // single-model BYOK chats get the same enforcement orchestrated
1801
+ // modes have always had.
1802
+ const trajForVerify = getTrajectory();
1803
+ const pendingFiles = trajForVerify ? pendingFilesAtClosure(trajForVerify) : null;
1804
+ if (this.loopPreventionEnabled &&
1805
+ pendingFiles &&
1806
+ pendingFiles.length > 0 &&
1807
+ closureVerifyCount < MAX_CLOSURE_VERIFY &&
1808
+ !signal?.aborted &&
1809
+ trajForVerify) {
1810
+ closureVerifyCount++;
1811
+ logger.debug(`[agent] Pre-closure verify on ${pendingFiles.length} pending file(s)`);
1812
+ onEvent({ type: 'verify_started', files: pendingFiles });
1813
+ const verifyResult = await runPendingVerify(trajForVerify, this.toolContext);
1814
+ if (!verifyResult.passed) {
1815
+ onEvent({ type: 'verify_failed', files: verifyResult.files, output: verifyResult.output });
1816
+ // Record signature + check loop threshold; fall through to
1817
+ // a normal nudge or, if we've been spinning, a fresh-eyes
1818
+ // independent second opinion.
1819
+ const sig = signatureForFailure(verifyResult.output, verifyResult.files);
1820
+ recordFailure(trajForVerify, sig, 'verify');
1821
+ let nudgeContent;
1822
+ if (shouldEscalateFreshEyes(trajForVerify, sig)) {
1823
+ markFreshEyesEscalated(trajForVerify);
1824
+ logger.info(`[agent] Fresh-eyes escalation triggered — signature ${sig.slice(0, 60)}... has recurred`);
1825
+ onEvent({ type: 'fresh_eyes_started', signature: sig.slice(0, 60) });
1826
+ const firstUserMsg = messages.find((m) => m.role === 'user');
1827
+ const originalTask = typeof firstUserMsg?.content === 'string' ? firstUserMsg.content : '';
1828
+ const review = await runFreshEyesReview({
1829
+ provider: this.provider,
1830
+ modelId: this.model.id,
1831
+ originalTask,
1832
+ files: verifyResult.files,
1833
+ cwd: this.toolContext.cwd,
1834
+ failureSummary: describeFailureLoop(trajForVerify),
1835
+ lastFailureReport: verifyResult.output,
1836
+ signal,
1837
+ });
1838
+ onEvent({ type: 'fresh_eyes_complete' });
1839
+ // Credit-fairness signal — by the time fresh-eyes has fired,
1840
+ // the user has paid for ≥3 same-signature failures + the
1841
+ // fresh-eyes call itself. Flag this turn as refund-eligible
1842
+ // so the backend can decide whether to credit the user
1843
+ // back. Token estimate: fresh-eyes max_tokens (800) plus
1844
+ // the prompt budget it builds (capped at ~10K input via
1845
+ // file/report/task budgets in fresh-eyes.ts). Conservative
1846
+ // 11_000 is the worst-case ceiling, not a measured spend —
1847
+ // backend should still cross-reference its own usage rows
1848
+ // for the authoritative number.
1849
+ onEvent({
1850
+ type: 'loop_refund_eligible',
1851
+ signature: sig.slice(0, 60),
1852
+ tokensInRecovery: 11_000,
1853
+ reason: 'fresh-eyes review fired — same-signature failure recurred 3+ times',
1854
+ });
1855
+ nudgeContent = buildFreshEyesContext(review);
1856
+ }
1857
+ else {
1858
+ nudgeContent = buildVerifyFailureNudge(verifyResult.output, verifyResult.files);
1859
+ }
1860
+ // Drop the closure attempt and re-prompt with the failure
1861
+ // context — gives the model real diagnostic info to act on
1862
+ // instead of letting it declare done with broken code.
1863
+ messages = messages.slice(0, -1);
1864
+ messages = [...messages, { role: 'user', content: nudgeContent }];
1865
+ continue;
1866
+ }
1867
+ onEvent({ type: 'verify_passed', files: verifyResult.files });
1868
+ // Pass — fall through to existing closure logic.
1869
+ }
1870
+ // ─── Closure fallback ─────────────────────────────────────────
1871
+ // Detect two failure modes where the turn terminates without
1872
+ // actually doing visible work:
1873
+ //
1874
+ // 1. Empty close — the model finished cleanly but produced zero
1875
+ // visible content. User sees silence after tool calls.
1876
+ //
1877
+ // 2. Continuation stall — the model produced text like "Let me
1878
+ // rewrite the sidebar" but terminated with no tool_calls.
1879
+ // It narrated intent but never acted. This is arguably worse
1880
+ // than an empty close because the user sees a promise that
1881
+ // never gets fulfilled.
1882
+ //
1883
+ // Both cases share the same fix: drop the stalled message, inject
1884
+ // a forcing nudge, re-enter the loop for one more iteration.
1885
+ // Guarded by closureFallbackAttempted so we never loop more than
1886
+ // once per run. If the nudged response is ALSO stalled, fall
1887
+ // through to the hardcoded "Done" substitution.
1888
+ const contentText = typeof assistantMessage.content === 'string'
1889
+ ? assistantMessage.content.trim()
1890
+ : '';
1891
+ const isEmptyClose = contentText.length === 0;
1892
+ const isContinuationStall = !isEmptyClose && looksLikeContinuationStall(contentText);
1893
+ // Count tool calls in this run so far (for drift detection)
1894
+ const runToolCallCount = messages.filter(m => m.role === 'assistant' && m.tool_calls?.length > 0)
1895
+ .reduce((sum, m) => sum + (m.tool_calls?.length ?? 0), 0);
1896
+ const isPostToolDrift = !isEmptyClose && !isContinuationStall && looksLikePostToolDrift(contentText, runToolCallCount);
1897
+ if ((isEmptyClose || isContinuationStall || isPostToolDrift) && !closureFallbackAttempted) {
1898
+ closureFallbackAttempted = true;
1899
+ const reason = isEmptyClose ? 'empty final message'
1900
+ : isContinuationStall ? 'continuation stall (narrated intent without acting)'
1901
+ : 'post-tool drift (greeting/social response after tool usage)';
1902
+ logger.debug(`[agent] Closure fallback: ${reason}, re-prompting`);
1903
+ // ── Dataset event: stall detected + nudge will fire ─────────
1904
+ // Stash the stall event_id on the trajectory so the run
1905
+ // wrapper's finally can emit continuation_nudge_fired with
1906
+ // an accurate `recovered` flag once we know whether the
1907
+ // nudge worked.
1908
+ const stallPattern = isEmptyClose ? 'empty-close'
1909
+ : isContinuationStall ? 'continuation-narration'
1910
+ : 'post-tool-drift';
1911
+ const stallEventId = avaEvents.emit('continuation_stall_detected', {
1912
+ response_summary: `${stallPattern}, ${contentText.length}ch`,
1913
+ stall_pattern: stallPattern,
1914
+ });
1915
+ const stallTraj = getTrajectory();
1916
+ if (stallTraj) {
1917
+ stallTraj.pendingStallEventId = stallEventId;
1918
+ // Optimistic default — the wrapper's finally flips this to
1919
+ // false if we hit the fallback-exhausted branch below.
1920
+ stallTraj.nudgeRecovered = true;
1921
+ }
1922
+ // Drop the stalled assistant message from history
1923
+ messages = messages.slice(0, -1);
1924
+ // Inject the appropriate forcing nudge
1925
+ const nudgeContent = isEmptyClose
1926
+ ? '[Closure check — your previous response was empty. The user needs visible confirmation that you finished. Write ONE short sentence summarising what you just did in this turn. Example: "Done — sidebar.tsx updated with the new palette." or "Fixed the missing habitId arg on line 71 of App.tsx." No tool calls. Just one sentence of text. This is the minimum required to close out a turn.]'
1927
+ : isPostToolDrift
1928
+ ? `[Context drift detected — you just used ${runToolCallCount} tools (reading files, searching, etc.) but then produced a greeting/social response instead of summarising your findings. You were in the middle of a task. The user did NOT change the subject — your attention drifted under the weight of all those tool results. Go back to the ORIGINAL task. Summarise what you found in the files you just read, present your plan, or continue working. Never produce a greeting after research.]`
1929
+ : `[Continuation check — you said "${contentText.slice(0, 120)}${contentText.length > 120 ? '…' : ''}" but then stopped without making any tool calls. You NARRATED intent but never acted on it. The user sees a promise that never got fulfilled — the worst possible UX. Do the work NOW in this response: make the actual tool calls to accomplish what you said you would. If the work genuinely can't be done, explain clearly why ("I can't X because Y"). Silence or another narration loop is not acceptable — either act or explain, no middle ground.]`;
1930
+ messages = [
1931
+ ...messages,
1932
+ { role: 'user', content: nudgeContent },
1933
+ ];
1934
+ // Loop back for one more streaming call — the nudge will force
1935
+ // either tool calls or a clear explanation. Normal flow resumes
1936
+ // from there.
1937
+ continue;
1938
+ }
1939
+ // If we already tried the closure fallback and STILL got a stall,
1940
+ // substitute a hardcoded "Done." so the user sees something rather
1941
+ // than a blank turn or a broken promise. This is belt-and-braces —
1942
+ // the prompt rule should catch most cases, the fallback nudge
1943
+ // catches more, and this final substitution catches the remaining
1944
+ // edge cases where the model is genuinely broken on closure.
1945
+ if ((isEmptyClose || isContinuationStall) && closureFallbackAttempted) {
1946
+ logger.warn('[agent] Closure fallback exhausted — substituting hardcoded "Done."');
1947
+ // Mark the nudge as failed for the upcoming nudge_fired emit.
1948
+ const exhaustedTraj = getTrajectory();
1949
+ if (exhaustedTraj)
1950
+ exhaustedTraj.nudgeRecovered = false;
1951
+ const substitute = isContinuationStall
1952
+ ? contentText + ' [Agent stalled — closure fallback substituted this message.]'
1953
+ : 'Done.';
1954
+ assistantMessage = {
1955
+ ...assistantMessage,
1956
+ content: substitute,
1957
+ };
1958
+ messages = [...messages.slice(0, -1), assistantMessage];
1959
+ }
1960
+ // Surface empty responses — model returned nothing visible to the user
1961
+ // (kept for the edge case where both content AND reasoning are empty
1962
+ // even after the closure fallback — genuinely broken model output)
1963
+ if (!assistantMessage.content && !assistantMessage.reasoning_content) {
1964
+ onEvent({
1965
+ type: 'error',
1966
+ error: new Error(t('error.msg.empty_response')),
1967
+ });
1968
+ }
1969
+ // ─── Honesty gate: verify-or-restate (active, every-model) ─────
1970
+ // A high-stakes completion/security claim with no verifying tool
1971
+ // behind it doesn't get to close on a guess. Re-prompt once to
1972
+ // verify (call the right tool) or restate without the claim —
1973
+ // mirrors the pre-closure file-verify guard above. Bounded to one
1974
+ // attempt per run; if it still can't back it, the claims-auditor's
1975
+ // deterministic caveat floor has already annotated the reply.
1976
+ const honestyAudit = this.lastAudit;
1977
+ if (this.loopPreventionEnabled &&
1978
+ !this.honestyVerifyAttempted &&
1979
+ honestyAudit?.flagged &&
1980
+ (honestyAudit.tier === 'high' || honestyAudit.tier === 'critical') &&
1981
+ !signal?.aborted &&
1982
+ typeof assistantMessage.content === 'string' &&
1983
+ assistantMessage.content.trim().length > 0) {
1984
+ this.honestyVerifyAttempted = true;
1985
+ logger.debug(`[agent] Honesty gate: unbacked ${honestyAudit.tier} claim — re-prompting to verify or restate`);
1986
+ const nudge = this.buildHonestyVerifyNudge(honestyAudit);
1987
+ messages = messages.slice(0, -1);
1988
+ messages = [...messages, { role: 'user', content: nudge }];
1989
+ continue;
1990
+ }
1991
+ // Memory extraction — runs post-turn, extracts genuinely durable
1992
+ // user/project facts (name, preferences, decisions, architecture).
1993
+ // Bounded by the Memory Agent's regex + single LLM call.
1994
+ this.extractMemoriesFromRun(messages, runContext);
1995
+ // Ambient hot-path writers DISABLED.
1996
+ // The following used to fire on every turn, each reading the
1997
+ // conversation and deriving persistent state from it:
1998
+ // captureInteraction(messages) — dataset capture
1999
+ // this.feedProceduralObserver(...) — v3 procedural learning
2000
+ // this.saveGraphState(runContext) — v3 graph persistence
2001
+ //
2002
+ // Collectively they were saving conversation-shaped content into
2003
+ // memory, creating a feedback loop where earlier turns' text got
2004
+ // re-injected on later turns and re-saved. They also ran 3+
2005
+ // concurrent writers against the shared conversation state,
2006
+ // which is the likely source of the empty-tool-call corruption
2007
+ // we've been seeing in longer sessions.
2008
+ //
2009
+ // These capabilities are not deleted — they belong in a
2010
+ // session-end or scheduled background job that reads memory
2011
+ // (distilled facts only), not the live conversation. Wiring
2012
+ // that up is follow-up work; for now, the hot path stays clean.
2013
+ onEvent({ type: 'done', finalMessage: assistantMessage });
2014
+ return finalHistory();
2015
+ }
2016
+ logger.debug(`[agent] Got ${assistantMessage.tool_calls.length} tool_calls: ${assistantMessage.tool_calls.map((tc) => tc.function.name).join(', ')}`);
2017
+ // ── Repeated tool-call detection ───────────────────────────────────────
2018
+ // If the model calls the same tool with the same arguments 3+ times, break the loop.
2019
+ // Different arguments = different call = not a loop (e.g. list_directory on different paths).
2020
+ const currentToolSig = assistantMessage.tool_calls.map((tc) => `${tc.function.name}:${tc.function.arguments}`).sort().join(',');
2021
+ if (currentToolSig === lastToolName) {
2022
+ repeatCount++;
2023
+ if (repeatCount >= MAX_SAME_TOOL_REPEATS) {
2024
+ logger.warn(`[agent] HARD STOP: ${currentToolSig} called ${repeatCount + 1} times consecutively`);
2025
+ const stopMsg = `Stopped: ${currentToolSig} was called ${repeatCount + 1} times in a row and kept failing. Try a different approach or start a new chat.`;
2026
+ onEvent({
2027
+ type: 'error',
2028
+ error: Object.assign(new Error(stopMsg), { code: 'tool_loop_stopped' }),
2029
+ });
2030
+ onEvent({
2031
+ type: 'done',
2032
+ finalMessage: { role: 'assistant', content: stopMsg },
2033
+ });
2034
+ const trajLoopStop = getTrajectory();
2035
+ if (trajLoopStop)
2036
+ trajLoopStop.outcome = 'hit_loop_limit';
2037
+ return finalHistory();
2038
+ }
2039
+ }
2040
+ else {
2041
+ lastToolName = currentToolSig;
2042
+ repeatCount = 0;
2043
+ }
2044
+ // ── Mode enforcement: block tools not allowed in the active mode ────
2045
+ // ALWAYS_ALLOWED_TOOLS is honoured here as well as in the schema filter
2046
+ // above. The two must agree: offering a schema and then blocking the call
2047
+ // gives the model a tool it can see and cannot use, which reads to the
2048
+ // user as Ava being broken rather than restricted.
2049
+ if (modeAllowed) {
2050
+ const isAllowed = (name) => modeAllowed.has(name) || ALWAYS_ALLOWED_TOOLS.has(name);
2051
+ const blocked = assistantMessage.tool_calls.filter((tc) => !isAllowed(tc.function.name));
2052
+ if (blocked.length > 0) {
2053
+ const blockedNames = blocked.map((tc) => tc.function.name).join(', ');
2054
+ logger.warn(`[agent] Mode ${detectedMode} blocked tools: ${blockedNames}`);
2055
+ // assistantMessage is already in `messages` (pushed unconditionally
2056
+ // earlier in the loop). The original code pushed it again inside
2057
+ // the for-loop, producing duplicate assistant turns in history
2058
+ // (one extra copy per blocked tool). Just push the tool results.
2059
+ for (const tc of blocked) {
2060
+ messages.push({
2061
+ role: 'tool',
2062
+ content: `Tool "${tc.function.name}" is not available in ${detectedMode} mode. This mode is read-only — use work mode (>>) to make changes.`,
2063
+ tool_call_id: tc.id,
2064
+ });
2065
+ }
2066
+ // Remove blocked calls, keep allowed ones.
2067
+ assistantMessage.tool_calls = assistantMessage.tool_calls.filter((tc) => isAllowed(tc.function.name));
2068
+ if (assistantMessage.tool_calls.length === 0) {
2069
+ // Delete the field entirely — Qwen rejects `tool_calls: []`.
2070
+ // Since assistantMessage is a reference already in `messages`,
2071
+ // the deletion propagates to the history.
2072
+ delete assistantMessage.tool_calls;
2073
+ continue;
2074
+ }
2075
+ }
2076
+ }
2077
+ // ── Hard exploration budget enforcement ──────────────────────────────
2078
+ // Soft nudge (maybeExplorationBudgetNudge) fires once as guidance,
2079
+ // but the model can ignore it and keep reading. This block is the
2080
+ // architectural floor: if reads-before-first-write exceed 2× the
2081
+ // task-complexity budget, read-only calls are refused at the agent
2082
+ // loop BEFORE they execute. The model receives a tool result telling
2083
+ // it exactly why and what to do next. This prevents the "100 reads,
2084
+ // still no code" failure mode that burns hundreds of thousands of
2085
+ // tokens with nothing to show.
2086
+ if (!this.hasWrittenInThisRun) {
2087
+ const hardBudget = COMPLEXITY_BUDGETS[this.currentTaskComplexity];
2088
+ const hardCap = hardBudget.readCapBeforeFirstWrite * 2;
2089
+ const projectedReads = this.readCountBeforeFirstWrite
2090
+ + assistantMessage.tool_calls.filter((tc) => this.isReadOnlyToolCall(tc.function.name)).length;
2091
+ if (projectedReads > hardCap) {
2092
+ const blockedReads = assistantMessage.tool_calls.filter((tc) => this.isReadOnlyToolCall(tc.function.name));
2093
+ if (blockedReads.length > 0) {
2094
+ logger.warn(`[agent] HARD BUDGET BLOCK: ${this.readCountBeforeFirstWrite} reads already done, ${blockedReads.length} more would exceed ${hardCap} cap for ${this.currentTaskComplexity} task`);
2095
+ // Note: assistantMessage is already in `messages` (pushed
2096
+ // unconditionally earlier in the loop). Do not push it again
2097
+ // — that would produce a duplicate assistant turn.
2098
+ const refusalBody = [
2099
+ `Read budget hard-limit exceeded.`,
2100
+ ``,
2101
+ `You've made ${this.readCountBeforeFirstWrite} read-only tool calls without a single write on a ${this.currentTaskComplexity} task (hard cap: ${hardCap}).`,
2102
+ ``,
2103
+ `Further reads are blocked until you either:`,
2104
+ ` 1. Commit to a write — pick the most likely correct change and make it. You have enough context.`,
2105
+ ` 2. Explicitly re-scope — if this task is genuinely architectural, say so in your next response ("this is bigger than it looked because...") and the budget resets.`,
2106
+ ``,
2107
+ `Stalling on context-gathering is the failure mode. Act or explain.`,
2108
+ ].join('\n');
2109
+ for (const tc of blockedReads) {
2110
+ messages.push({
2111
+ role: 'tool',
2112
+ content: refusalBody,
2113
+ tool_call_id: tc.id,
2114
+ });
2115
+ }
2116
+ // Remove blocked reads, keep any non-read tool calls (writes, etc.)
2117
+ assistantMessage.tool_calls = assistantMessage.tool_calls.filter((tc) => !blockedReads.some(b => b.id === tc.id));
2118
+ if (assistantMessage.tool_calls.length === 0) {
2119
+ // Delete the field entirely — Qwen rejects `tool_calls: []`.
2120
+ delete assistantMessage.tool_calls;
2121
+ continue;
2122
+ }
2123
+ }
2124
+ }
2125
+ }
2126
+ // ── Parallel tool execution ──────────────────────────────────────────
2127
+ // Partition tool calls: confirmation-required run sequentially first,
2128
+ // auto-approved tools run in parallel after for speed.
2129
+ const confirmCalls = [];
2130
+ const autoCalls = [];
2131
+ for (const tc of assistantMessage.tool_calls) {
2132
+ const tool = this.toolRegistry.getTool(tc.function.name);
2133
+ if (tool && this.toolRegistry.needsConfirmation(tool)) {
2134
+ confirmCalls.push(tc);
2135
+ }
2136
+ else {
2137
+ autoCalls.push(tc);
2138
+ }
2139
+ }
2140
+ // Phase 1: Confirmation-required tools (sequential — user must approve each)
2141
+ for (const toolCall of confirmCalls) {
2142
+ if (signal?.aborted) {
2143
+ onEvent({ type: 'done', finalMessage: assistantMessage });
2144
+ return finalHistory();
2145
+ }
2146
+ // Auto-checkpoint before write/dangerous tools
2147
+ const toolDef = this.toolRegistry.getTool(toolCall.function.name);
2148
+ if (toolDef && (toolDef.riskLevel === 'write' || toolDef.riskLevel === 'dangerous')) {
2149
+ const cp = runContext.sharedState?.checkpointManager;
2150
+ if (cp && !cp.hasActiveCheckpoint()) {
2151
+ try {
2152
+ await cp.createCheckpoint();
2153
+ }
2154
+ catch { /* best-effort */ }
2155
+ }
2156
+ }
2157
+ messages = await this.executeToolCall(toolCall, runContext, onEvent, messages, useNativeTools);
2158
+ }
2159
+ // Phase 2: Auto-approved tools (parallel via Promise.allSettled)
2160
+ if (autoCalls.length > 0) {
2161
+ if (signal?.aborted) {
2162
+ onEvent({ type: 'done', finalMessage: assistantMessage });
2163
+ return finalHistory();
2164
+ }
2165
+ // Auto-checkpoint if any auto-approved tool is write/dangerous
2166
+ const hasRiskyAuto = autoCalls.some(tc => {
2167
+ const td = this.toolRegistry.getTool(tc.function.name);
2168
+ return td && (td.riskLevel === 'write' || td.riskLevel === 'dangerous');
2169
+ });
2170
+ if (hasRiskyAuto) {
2171
+ const cp = runContext.sharedState?.checkpointManager;
2172
+ if (cp && !cp.hasActiveCheckpoint()) {
2173
+ try {
2174
+ await cp.createCheckpoint();
2175
+ }
2176
+ catch { /* best-effort */ }
2177
+ }
2178
+ }
2179
+ // Fire all start events
2180
+ for (const tc of autoCalls) {
2181
+ onEvent({ type: 'tool_call_start', toolCall: tc });
2182
+ }
2183
+ // Execute all in parallel
2184
+ const results = await Promise.allSettled(autoCalls.map(async (tc) => {
2185
+ let parsedArgs;
2186
+ try {
2187
+ parsedArgs = JSON.parse(tc.function.arguments);
2188
+ }
2189
+ catch {
2190
+ parsedArgs = {};
2191
+ }
2192
+ const ctx = {
2193
+ ...runContext,
2194
+ // Thread the model's tool_call ID so any confirmation handler
2195
+ // (auto tools should never trigger one, but this is defensive)
2196
+ // can match cards to the exact tool call.
2197
+ toolCallId: tc.id,
2198
+ onOutput: (data) => {
2199
+ onEvent({ type: 'tool_call_partial', toolCallId: tc.id, data });
2200
+ },
2201
+ };
2202
+ return this.executeToolWithCapture(tc.function.name, parsedArgs, ctx);
2203
+ }));
2204
+ // Append results in order (API requires tool messages match tool_call order)
2205
+ for (let i = 0; i < autoCalls.length; i++) {
2206
+ const toolCall = autoCalls[i];
2207
+ const settled = results[i];
2208
+ const result = settled.status === 'fulfilled'
2209
+ ? settled.value
2210
+ : { success: false, output: `Tool failed: ${settled.reason}`, metadata: undefined };
2211
+ onEvent({
2212
+ type: 'tool_call_end',
2213
+ toolCall,
2214
+ result: result.output,
2215
+ success: result.success,
2216
+ metadata: result.metadata,
2217
+ });
2218
+ if (useNativeTools) {
2219
+ messages = [
2220
+ ...messages,
2221
+ {
2222
+ role: 'tool',
2223
+ tool_call_id: toolCall.id,
2224
+ content: result.output,
2225
+ },
2226
+ ];
2227
+ }
2228
+ else {
2229
+ // Text-based mode: send tool results as user messages
2230
+ messages = [
2231
+ ...messages,
2232
+ {
2233
+ role: 'user',
2234
+ content: formatToolResult(toolCall.function.name, result.output, result.success),
2235
+ },
2236
+ ];
2237
+ }
2238
+ // Vision pipeline — downsample before embedding to cap per-image
2239
+ // token cost. Full-res screenshots burn 20-50K tokens each and
2240
+ // get re-sent on every subsequent turn.
2241
+ if (result.metadata?.base64_image) {
2242
+ const rawBase64 = result.metadata.base64_image;
2243
+ const resizedBase64 = downsampleScreenshotBase64(rawBase64);
2244
+ messages = [
2245
+ ...messages,
2246
+ {
2247
+ role: 'user',
2248
+ content: [
2249
+ { type: 'text', text: `[Image captured by ${toolCall.function.name}]` },
2250
+ { type: 'image_url', image_url: {
2251
+ url: `data:${result.metadata.mime_type || 'image/png'};base64,${resizedBase64}`,
2252
+ } },
2253
+ ],
2254
+ },
2255
+ ];
2256
+ }
2257
+ }
2258
+ // ─── Dynamic design context re-injection ─────────────────────────
2259
+ // If any tool call in this batch wrote or edited a UI file, refresh
2260
+ // the Decisions/design context into the message history so it's in
2261
+ // attention for the NEXT turn — not buried behind whatever error
2262
+ // recovery or other noise has accumulated. One injection per batch,
2263
+ // even if multiple UI files were touched. Throttled by turn count
2264
+ // and file mtime cache so we don't re-read the same files 20 times.
2265
+ const uiBatchPath = this.findUIFilePathInBatch(autoCalls);
2266
+ if (uiBatchPath) {
2267
+ this.designReinjectionTurn++;
2268
+ const reinject = await maybeBuildDesignReinjection(runContext.cwd, uiBatchPath, {
2269
+ currentTurn: this.designReinjectionTurn,
2270
+ lastInjectedTurn: this.designReinjectionLastTurn,
2271
+ lastMtimes: this.designReinjectionLastMtimes,
2272
+ });
2273
+ if (reinject) {
2274
+ messages = [
2275
+ ...messages,
2276
+ { role: 'user', content: reinject.content },
2277
+ ];
2278
+ this.designReinjectionLastTurn = this.designReinjectionTurn;
2279
+ this.designReinjectionLastMtimes = reinject.updatedMtimes;
2280
+ }
2281
+ }
2282
+ // ─── Exploration budget nudge ──────────────────────────────────
2283
+ // Count read-only tool calls in this batch. If the agent has done
2284
+ // too much exploration without committing to a write, inject a
2285
+ // soft nudge telling her to commit or justify. Never hard-stops.
2286
+ const nudge = this.maybeExplorationBudgetNudge(autoCalls);
2287
+ if (nudge) {
2288
+ messages = [
2289
+ ...messages,
2290
+ { role: 'user', content: nudge },
2291
+ ];
2292
+ }
2293
+ }
2294
+ }
2295
+ const iterError = new Error(t('error.msg.iteration_limit', { limit: String(MAX_TOOL_CALL_ITERATIONS) }));
2296
+ iterError.code = 'iterations_exceeded';
2297
+ onEvent({ type: 'error', error: iterError });
2298
+ // Always emit done so the UI clears isStreaming
2299
+ onEvent({ type: 'done', finalMessage: { role: 'assistant', content: 'Stopped: tool call iteration limit reached.' } });
2300
+ // Extract memories on iteration limit — bounded extraction only.
2301
+ // Dataset capture, procedural observer, graph state save: disabled
2302
+ // from the hot path (see the clean-exit branch above for the full
2303
+ // rationale).
2304
+ this.extractMemoriesFromRun(messages, runContext);
2305
+ return finalHistory();
2306
+ }
2307
+ /**
2308
+ * Extract and save memories from a completed run.
2309
+ * Fire-and-forget — never blocks the response.
2310
+ * Errors are logged at debug level so they don't spam the UI but are visible for debugging.
2311
+ */
2312
+ extractMemoriesFromRun(messages, runContext) {
2313
+ const ma = runContext.sharedState?.memoryAgent;
2314
+ const mm = runContext.sharedState?.memoryManager;
2315
+ if (ma) {
2316
+ // Memory Agent: single extraction call (regex + LLM reflection)
2317
+ logger.debug('[memory] Running Memory Agent extraction');
2318
+ ma.extractAndSave(messages)
2319
+ .then(saved => {
2320
+ if (saved > 0)
2321
+ logger.info(`[memory] Memory Agent saved ${saved} ${saved === 1 ? 'memory' : 'memories'}`);
2322
+ else
2323
+ logger.debug('[memory] Memory Agent: 0 memories extracted from this turn');
2324
+ })
2325
+ .catch(err => logger.warn(`[memory] Memory Agent extraction failed: ${err instanceof Error ? err.message : String(err)}`));
2326
+ }
2327
+ else if (mm) {
2328
+ // Legacy fallback (Memory Agent unavailable): regex extraction only.
2329
+ // Previously this ran three additional passes — reflectAndSave
2330
+ // (LLM reflection), trackAndLearn (pattern tracking), and every
2331
+ // 6 user turns analyseAndSave (insights consolidation). Each was a
2332
+ // concurrent writer reading the conversation and deriving memory
2333
+ // from it. Together they (a) quadrupled the per-turn LLM spend on
2334
+ // ambient memory work and (b) produced conversation-shaped memory
2335
+ // entries that fed back into later turns.
2336
+ //
2337
+ // Keeping only autoExtractAndSave here: it's regex-based, bounded,
2338
+ // and the narrowest path. If its heuristics still save conversation
2339
+ // snippets rather than durable facts, that's a follow-up tightening.
2340
+ logger.debug('[memory] Running legacy memory extraction (regex only)');
2341
+ autoExtractAndSave(messages, mm)
2342
+ .then(saved => {
2343
+ if (saved > 0)
2344
+ logger.info(`[memory] Auto-extract saved ${saved} ${saved === 1 ? 'memory' : 'memories'}`);
2345
+ else
2346
+ logger.debug('[memory] Auto-extract: 0 memories from regex patterns');
2347
+ })
2348
+ .catch(err => logger.warn(`[memory] Auto-extract failed: ${err instanceof Error ? err.message : String(err)}`));
2349
+ }
2350
+ else {
2351
+ logger.debug('[memory] No memoryManager in sharedState — skipping extraction. Is memory wired correctly?');
2352
+ }
2353
+ }
2354
+ async streamResponse(request, onEvent, signal) {
2355
+ onEvent({ type: 'stream_start' });
2356
+ let content = '';
2357
+ let reasoningContent = '';
2358
+ let usage;
2359
+ // Holds back a written tool call while it streams. Built from the tools
2360
+ // actually offered this turn, so it can only ever hide something that
2361
+ // recoverWrittenToolCalls would go on to lift out of the finished reply.
2362
+ const writtenCallFilter = new WrittenCallStreamFilter(new Set((request.tools ?? []).map((t) => t.function.name)));
2363
+ const toolCallsAccumulator = new Map();
2364
+ // Local controller linked to the parent signal. Lets us abort the
2365
+ // streaming request from inside the loop (on mid-stream user
2366
+ // injection) without touching the outer agent signal — the outer
2367
+ // run isn't cancelled, just this single streamResponse call.
2368
+ const localController = new AbortController();
2369
+ const forwardAbort = () => localController.abort();
2370
+ if (signal?.aborted) {
2371
+ localController.abort();
2372
+ }
2373
+ else {
2374
+ signal?.addEventListener('abort', forwardAbort);
2375
+ }
2376
+ // Flag set when we abort due to a mid-stream injection so the
2377
+ // caller knows to loop without attempting tool execution on a
2378
+ // partial response.
2379
+ let interruptedByInjection = false;
2380
+ try {
2381
+ for await (const chunk of this.provider.createStreamingCompletion(request, localController.signal)) {
2382
+ if (chunk.usage) {
2383
+ usage = chunk.usage;
2384
+ }
2385
+ const delta = chunk.choices[0]?.delta;
2386
+ if (!delta)
2387
+ continue;
2388
+ // Thinking/reasoning content (DeepSeek R1, GLM, Kimi, Mistral Magistral)
2389
+ const thinking = delta.reasoning_content ?? delta.reasoning;
2390
+ if (thinking) {
2391
+ // Some models leak literal <think>/</think> markers into the reasoning
2392
+ // field — strip them so they never render as raw text in the thought bubble.
2393
+ const cleaned = thinking.replace(/<\/?think>/g, '');
2394
+ if (cleaned) {
2395
+ reasoningContent += cleaned;
2396
+ onEvent({ type: 'thinking_delta', content: cleaned });
2397
+ }
2398
+ }
2399
+ if (delta.content) {
2400
+ // COERCE TO TEXT FIRST. delta.content was assumed to be a string and
2401
+ // concatenated straight onto the reply — so a provider that streams
2402
+ // it as a content-part object (or an array of them) produced one
2403
+ // "[object Object]" per chunk, and the user got a wall of them where
2404
+ // the answer should be. Seen live 2026-08-18 on a self_inspect turn.
2405
+ //
2406
+ // Nothing downstream can recover from it either: by the time it is
2407
+ // in `content` the real text is gone, so the transcript, the history
2408
+ // file and the next request all carry the same rubbish.
2409
+ //
2410
+ // getTextContent handles both string and ContentPart[]; the object
2411
+ // case is a single part, so it is wrapped before extraction.
2412
+ const rawDelta = delta.content;
2413
+ let visibleContent = typeof rawDelta === 'string'
2414
+ ? rawDelta
2415
+ : getTextContent((Array.isArray(rawDelta) ? rawDelta : [rawDelta]));
2416
+ if (!visibleContent)
2417
+ continue;
2418
+ if (visibleContent.includes('<think>') || visibleContent.includes('</think>') || this._inThinkTag) {
2419
+ // Track if we're inside a think tag across chunks
2420
+ const parts = visibleContent.split(/(<\/?think>)/);
2421
+ let visible = '';
2422
+ for (const part of parts) {
2423
+ if (part === '<think>') {
2424
+ this._inThinkTag = true;
2425
+ continue;
2426
+ }
2427
+ if (part === '</think>') {
2428
+ this._inThinkTag = false;
2429
+ continue;
2430
+ }
2431
+ if (this._inThinkTag) {
2432
+ reasoningContent += part;
2433
+ onEvent({ type: 'thinking_delta', content: part });
2434
+ }
2435
+ else {
2436
+ visible += part;
2437
+ }
2438
+ }
2439
+ visibleContent = visible;
2440
+ }
2441
+ if (visibleContent) {
2442
+ // Raw content accumulates in full — recoverWrittenToolCalls reads
2443
+ // it after the stream. Only the VIEW is filtered, so a written call
2444
+ // never reaches the screen on its way to becoming a real one.
2445
+ content += visibleContent;
2446
+ const showable = writtenCallFilter.push(visibleContent);
2447
+ if (showable)
2448
+ onEvent({ type: 'stream_delta', content: showable });
2449
+ }
2450
+ }
2451
+ if (delta.tool_calls) {
2452
+ if (toolCallsAccumulator.size === 0) {
2453
+ logger.debug('[agent] First tool_call delta received in stream');
2454
+ }
2455
+ for (const tcDelta of delta.tool_calls) {
2456
+ if (!toolCallsAccumulator.has(tcDelta.index)) {
2457
+ toolCallsAccumulator.set(tcDelta.index, {
2458
+ id: tcDelta.id ?? '',
2459
+ type: 'function',
2460
+ function: { name: '', arguments: '' },
2461
+ });
2462
+ }
2463
+ const acc = toolCallsAccumulator.get(tcDelta.index);
2464
+ if (tcDelta.id)
2465
+ acc.id = tcDelta.id;
2466
+ if (tcDelta.function?.name)
2467
+ acc.function.name += tcDelta.function.name;
2468
+ if (tcDelta.function?.arguments)
2469
+ acc.function.arguments += tcDelta.function.arguments;
2470
+ }
2471
+ }
2472
+ // Mid-stream injection check. If the user sent a message while the
2473
+ // model was streaming, abort this stream and let the outer loop
2474
+ // pick up the interjection on its next iteration (where
2475
+ // pendingInterjections gets drained at the top of the loop).
2476
+ //
2477
+ // Interrupting mid-tool-call would leave dangling tool_calls in
2478
+ // history with no matching tool results — most providers 400 on
2479
+ // that. Only interrupt while we're in the text/thinking phase,
2480
+ // before any tool_calls have started accumulating.
2481
+ if (this.pendingInterjections.length > 0 && toolCallsAccumulator.size === 0) {
2482
+ logger.debug('[agent] Mid-stream injection arrived — aborting stream, interjection will fire next iteration');
2483
+ interruptedByInjection = true;
2484
+ localController.abort();
2485
+ break;
2486
+ }
2487
+ }
2488
+ }
2489
+ catch (error) {
2490
+ // If we aborted locally because of an injection, fall through to the
2491
+ // normal return path with interruptedByInjection=true. The outer
2492
+ // loop handles the partial message without running tools.
2493
+ if (interruptedByInjection) {
2494
+ // intentional: swallow the abort error, continue to the return below
2495
+ }
2496
+ else if (content || reasoningContent || toolCallsAccumulator.size > 0) {
2497
+ // Preserve partial content AND accumulated tool calls if we collected any before the error
2498
+ const partialToolCalls = toolCallsAccumulator.size > 0
2499
+ ? Array.from(toolCallsAccumulator.values()).filter(tc => tc.id && tc.function.name)
2500
+ : [];
2501
+ const partialMessage = {
2502
+ role: 'assistant',
2503
+ content: content || null,
2504
+ ...(reasoningContent ? { reasoning_content: reasoningContent } : {}),
2505
+ ...(partialToolCalls.length > 0 ? { tool_calls: partialToolCalls } : {}),
2506
+ };
2507
+ // The turn is failing, so nothing downstream will lift a written call
2508
+ // out of it. Release whatever the filter was holding rather than lose
2509
+ // it — a half-typed tag on screen beats a silently truncated reply.
2510
+ const pending = writtenCallFilter.flush();
2511
+ if (pending)
2512
+ onEvent({ type: 'stream_delta', content: pending });
2513
+ onEvent({ type: 'stream_end', message: partialMessage });
2514
+ throw error;
2515
+ }
2516
+ else {
2517
+ throw error;
2518
+ }
2519
+ }
2520
+ finally {
2521
+ signal?.removeEventListener('abort', forwardAbort);
2522
+ }
2523
+ // Anything still held that never became a call is ordinary text and is
2524
+ // owed to the reader. A confirmed call is dropped here on purpose: the
2525
+ // recovery lifts it from `content`, so re-emitting it would put back the
2526
+ // exact block this filter exists to withhold.
2527
+ const heldText = writtenCallFilter.flush();
2528
+ if (heldText)
2529
+ onEvent({ type: 'stream_delta', content: heldText });
2530
+ const toolCalls = toolCallsAccumulator.size > 0 ? Array.from(toolCallsAccumulator.values()) : [];
2531
+ // DeepSeek Reasoner rule: "If reasoning_content is set, content must not be empty."
2532
+ // When the model returns reasoning + tool_calls but no text, content would be null —
2533
+ // which causes a 400 on the next request if reasoning_content is also present.
2534
+ let finalContent = (!content && reasoningContent) ? '' : (content || null);
2535
+ // Honesty gate (soft): on the user-facing answer turn (no tool calls),
2536
+ // flag a state-claim ("done" / "it works" / "it's live") that ran no
2537
+ // verifying tool this run, and append a visible caveat so the unverified
2538
+ // claim doesn't stand as fact. Soft by design — annotates, never blocks.
2539
+ if (toolCalls.length === 0 && typeof finalContent === 'string' && finalContent.trim()) {
2540
+ const audit = auditClaims({ text: finalContent, toolsUsed: this.runToolEvidence });
2541
+ this.lastAudit = audit;
2542
+ if (audit.flagged) {
2543
+ // Record for the verification_evidence dataset event (shape-only:
2544
+ // a boolean, never the claim text). Captured even when there's no
2545
+ // caveat string, so the signal reflects every flagged claim.
2546
+ this.runClaimFlagged = true;
2547
+ }
2548
+ // Active honesty gate: a high/critical claim with no verifying tool is
2549
+ // about to get one verify-or-restate re-prompt in runInner — so DON'T
2550
+ // append the caveat yet in that case. Append it now for soft claims,
2551
+ // once the re-prompt is already spent, or when loop prevention is off:
2552
+ // that's the deterministic floor.
2553
+ const willReRun = this.loopPreventionEnabled &&
2554
+ !this.honestyVerifyAttempted &&
2555
+ (audit.tier === 'high' || audit.tier === 'critical');
2556
+ if (audit.flagged && audit.caveat && !willReRun) {
2557
+ const caveatText = `\n\n${audit.caveat}`;
2558
+ onEvent({ type: 'stream_delta', content: caveatText });
2559
+ finalContent = finalContent + caveatText;
2560
+ }
2561
+ }
2562
+ const message = {
2563
+ role: 'assistant',
2564
+ content: finalContent,
2565
+ ...(reasoningContent ? { reasoning_content: reasoningContent } : {}),
2566
+ ...(toolCalls.length > 0 ? { tool_calls: toolCalls } : {}),
2567
+ };
2568
+ onEvent({ type: 'stream_end', message });
2569
+ if (usage) {
2570
+ let cost;
2571
+ if (this.model.pricing) {
2572
+ cost =
2573
+ (usage.prompt_tokens / 1_000_000) * this.model.pricing.inputPerMillion +
2574
+ (usage.completion_tokens / 1_000_000) * this.model.pricing.outputPerMillion;
2575
+ }
2576
+ onEvent({ type: 'usage', usage, cost });
2577
+ }
2578
+ // Meter the call — one chat_turn charge per agent iteration. A multi-
2579
+ // iteration agent (tool loop) emits one per loop, which is the intended
2580
+ // granularity. Cache-hit detection uses cached_tokens ratio: if >50% of
2581
+ // prompt was cached, treat as a cache hit for discount purposes.
2582
+ // No cast needed since TokenUsage declares the cache fields it always
2583
+ // carried at runtime. The `as unknown as` this replaced was the tell:
2584
+ // the shape was known to be wider than the type all along.
2585
+ const rawUsage = extractUsage(usage);
2586
+ const cacheHit = rawUsage?.cached != null && rawUsage.input > 0 && rawUsage.cached / rawUsage.input > 0.5;
2587
+ chargeCredits('chat_turn', {
2588
+ model: this.model.id,
2589
+ rawTokens: rawUsage,
2590
+ cacheHit,
2591
+ });
2592
+ return {
2593
+ message,
2594
+ promptTokens: usage?.prompt_tokens ?? 0,
2595
+ ...(interruptedByInjection ? { interrupted: true } : {}),
2596
+ };
2597
+ }
2598
+ // ── Single tool call execution (used by sequential confirmation phase) ──
2599
+ /**
2600
+ * Run a tool through the registry while emitting `tool_choice` /
2601
+ * `tool_result` dataset events around the call. Returns the same
2602
+ * shape `toolRegistry.execute` does — never throws (registry
2603
+ * exceptions are converted to a failure result, mirroring the
2604
+ * existing executeToolCall behaviour). Both the sequential and
2605
+ * parallel tool paths in `runInner` go through this helper so the
2606
+ * dataset trajectory captures every tool invocation in order.
2607
+ */
2608
+ async executeToolWithCapture(toolName, args, ctx) {
2609
+ const traj = getTrajectory();
2610
+ const prevTools = traj?.toolsSoFar ? [...traj.toolsSoFar] : [];
2611
+ // ── Recovery-action emit ────────────────────────────────────────────
2612
+ // If the previous tool in this trajectory failed, the upcoming choice
2613
+ // is implicitly Ava's recovery move. Emit recovery_action linking
2614
+ // back to the specific tool_error before the new choice fires so the
2615
+ // ordering in the dataset reflects cause → response.
2616
+ const recoveringFromErrorId = traj?.pendingErrorEventId;
2617
+ if (recoveringFromErrorId) {
2618
+ const lastTool = prevTools[prevTools.length - 1];
2619
+ const recoveryKind = lastTool === toolName ? 'retry_same' : 'switch_tool';
2620
+ avaEvents.emit('recovery_action', {
2621
+ tool_error_event_id: recoveringFromErrorId,
2622
+ recovery_kind: recoveryKind,
2623
+ next_tool: toolName,
2624
+ });
2625
+ if (traj)
2626
+ traj.pendingErrorEventId = undefined;
2627
+ }
2628
+ const choiceEventId = avaEvents.emit('tool_choice', {
2629
+ tool_name: toolName,
2630
+ args_summary: summarizeToolArgs(toolName, args),
2631
+ // Process category for WHY this tool was reached for — deterministic
2632
+ // from the tool + whether we're recovering from a prior failure.
2633
+ // A label, never the model's raw chain-of-thought.
2634
+ reasoning_summary: categorizeToolPurpose(toolName, { recovering: !!recoveringFromErrorId }),
2635
+ prev_tools_in_trajectory: prevTools,
2636
+ });
2637
+ if (traj?.toolsSoFar)
2638
+ traj.toolsSoFar.push(toolName);
2639
+ const start = Date.now();
2640
+ let result;
2641
+ try {
2642
+ result = await this.toolRegistry.execute(toolName, args, ctx);
2643
+ }
2644
+ catch (err) {
2645
+ result = {
2646
+ output: `Tool error: ${err instanceof Error ? err.message : String(err)}`,
2647
+ success: false,
2648
+ };
2649
+ }
2650
+ const resultEventId = avaEvents.emit('tool_result', {
2651
+ tool_name: toolName,
2652
+ tool_choice_event_id: choiceEventId,
2653
+ success: result.success,
2654
+ result_summary: summarizeToolResult(result.output, result.success),
2655
+ duration_ms: Date.now() - start,
2656
+ error_summary: result.success ? undefined : result.output.slice(0, 200),
2657
+ });
2658
+ // Record for the soft honesty gate: did a verifying tool succeed this run?
2659
+ this.runToolEvidence.push({ name: toolName, ok: result.success });
2660
+ // ── Tool-error + guidance emits ─────────────────────────────────────
2661
+ // On failure, emit tool_error with the matched pattern key (if any),
2662
+ // followed by error_guidance_applied if the pattern library produced
2663
+ // user-facing fix advice. Stash the tool_error event_id on the
2664
+ // trajectory so the next tool_choice can attach a recovery_action.
2665
+ if (!result.success) {
2666
+ const matched = matchToolError(result.output);
2667
+ const errorEventId = avaEvents.emit('tool_error', {
2668
+ tool_name: toolName,
2669
+ tool_result_event_id: resultEventId,
2670
+ error_pattern_match: matched?.pattern_key,
2671
+ error_summary: result.output.slice(0, 200),
2672
+ });
2673
+ if (matched) {
2674
+ avaEvents.emit('error_guidance_applied', {
2675
+ tool_error_event_id: errorEventId,
2676
+ pattern: matched.pattern_key,
2677
+ // Guidance text is bounded and contains no user data — it's
2678
+ // canonical advice strings from the pattern library — so
2679
+ // capturing a short summary is safe.
2680
+ guidance_summary: matched.guidance.slice(0, 200),
2681
+ });
2682
+ }
2683
+ if (traj)
2684
+ traj.pendingErrorEventId = errorEventId;
2685
+ }
2686
+ // ── Post-edit verify tracking ─────────────────────────────────────
2687
+ // Universal hook: every successful file_write / file_edit registers
2688
+ // its target on the trajectory's pendingVerifyFiles. Read by the
2689
+ // closure-time verify guard in runInner so unverified edits can't
2690
+ // sneak past a turn-end. Lives here (not in AutoCoordinator) so
2691
+ // every routing mode — Maestro / Supernova / Aurora / direct BYOK —
2692
+ // gets the same enforcement.
2693
+ if (traj) {
2694
+ recordEditFromTool(traj, toolName, args, result.success);
2695
+ }
2696
+ return result;
2697
+ }
2698
+ async executeToolCall(toolCall, runContext, onEvent, messages, useNativeTools = true) {
2699
+ onEvent({ type: 'tool_call_start', toolCall });
2700
+ let parsedArgs;
2701
+ try {
2702
+ parsedArgs = JSON.parse(toolCall.function.arguments);
2703
+ }
2704
+ catch {
2705
+ parsedArgs = {};
2706
+ }
2707
+ const toolRunContext = {
2708
+ ...runContext,
2709
+ // Thread the model's tool_call ID through so the confirmation handler
2710
+ // can forward it to the UI for exact-match card attachment.
2711
+ toolCallId: toolCall.id,
2712
+ onOutput: (data) => {
2713
+ onEvent({ type: 'tool_call_partial', toolCallId: toolCall.id, data });
2714
+ },
2715
+ };
2716
+ const result = await this.executeToolWithCapture(toolCall.function.name, parsedArgs, toolRunContext);
2717
+ onEvent({
2718
+ type: 'tool_call_end',
2719
+ toolCall,
2720
+ result: result.output,
2721
+ success: result.success,
2722
+ metadata: result.metadata,
2723
+ });
2724
+ // ── Same call, same failure, again ──────────────────────────────────
2725
+ //
2726
+ // Two identical failures is evidence. The third attempt is told to stop and
2727
+ // ask, because nothing about the call has changed and nothing about the
2728
+ // result will. The message replaces the tool output rather than joining it:
2729
+ // repeating the same error text a third time is what convinced the model to
2730
+ // try a fourth.
2731
+ const failKey = `${toolCall.function.name}:${toolCall.function.arguments}`;
2732
+ let output = result.output;
2733
+ if (!result.success) {
2734
+ const n = (this.repeatedToolFailures.get(failKey) ?? 0) + 1;
2735
+ this.repeatedToolFailures.set(failKey, n);
2736
+ if (n >= 3) {
2737
+ output = `${result.output}
2738
+
2739
+ [This is attempt ${n} of \`${toolCall.function.name}\` with identical arguments, and it has failed the same way every time. Do NOT call it again with these arguments — nothing has changed, so nothing will. Tell the user plainly what failed and what the error said, and either change the approach or ask them how they want to proceed.]`;
2740
+ }
2741
+ }
2742
+ else {
2743
+ this.repeatedToolFailures.delete(failKey);
2744
+ }
2745
+ if (useNativeTools) {
2746
+ messages = [
2747
+ ...messages,
2748
+ {
2749
+ role: 'tool',
2750
+ tool_call_id: toolCall.id,
2751
+ content: output,
2752
+ },
2753
+ ];
2754
+ }
2755
+ else {
2756
+ // Text-based mode: send tool results as user messages
2757
+ messages = [
2758
+ ...messages,
2759
+ {
2760
+ role: 'user',
2761
+ content: formatToolResult(toolCall.function.name, output, result.success),
2762
+ },
2763
+ ];
2764
+ }
2765
+ // Vision pipeline
2766
+ if (result.metadata?.base64_image) {
2767
+ messages = [
2768
+ ...messages,
2769
+ {
2770
+ role: 'user',
2771
+ content: [
2772
+ { type: 'text', text: `[Image captured by ${toolCall.function.name}]` },
2773
+ { type: 'image_url', image_url: {
2774
+ url: `data:${result.metadata.mime_type || 'image/png'};base64,${result.metadata.base64_image}`,
2775
+ } },
2776
+ ],
2777
+ },
2778
+ ];
2779
+ }
2780
+ // Dynamic design context re-injection — same treatment as the parallel
2781
+ // batch path. Throttled by turn count and mtime cache.
2782
+ const uiPath = this.findUIFilePathInBatch([toolCall]);
2783
+ if (uiPath) {
2784
+ this.designReinjectionTurn++;
2785
+ const reinject = await maybeBuildDesignReinjection(runContext.cwd, uiPath, {
2786
+ currentTurn: this.designReinjectionTurn,
2787
+ lastInjectedTurn: this.designReinjectionLastTurn,
2788
+ lastMtimes: this.designReinjectionLastMtimes,
2789
+ });
2790
+ if (reinject) {
2791
+ messages = [
2792
+ ...messages,
2793
+ { role: 'user', content: reinject.content },
2794
+ ];
2795
+ this.designReinjectionLastTurn = this.designReinjectionTurn;
2796
+ this.designReinjectionLastMtimes = reinject.updatedMtimes;
2797
+ }
2798
+ }
2799
+ // Exploration budget nudge — same as parallel path
2800
+ const seqNudge = this.maybeExplorationBudgetNudge([toolCall]);
2801
+ if (seqNudge) {
2802
+ messages = [
2803
+ ...messages,
2804
+ { role: 'user', content: seqNudge },
2805
+ ];
2806
+ }
2807
+ return messages;
2808
+ }
2809
+ /**
2810
+ * Scan a batch of tool calls for a UI file write/edit and return the first
2811
+ * matching file path. Returns undefined if no UI file was touched.
2812
+ * Used by the design context re-injection hook to decide whether to refresh
2813
+ * the Decisions/design/* content into the next LLM turn.
2814
+ */
2815
+ findUIFilePathInBatch(toolCalls) {
2816
+ for (const tc of toolCalls) {
2817
+ if (tc.function.name !== 'file_write' && tc.function.name !== 'file_edit')
2818
+ continue;
2819
+ try {
2820
+ const args = JSON.parse(tc.function.arguments);
2821
+ const filePath = (args.file_path ?? args.path);
2822
+ if (filePath && isUIFilePathLocal(filePath))
2823
+ return filePath;
2824
+ }
2825
+ catch { /* malformed args — skip */ }
2826
+ }
2827
+ return undefined;
2828
+ }
2829
+ // ─── Task classification + exploration budget helpers ──────────────────
2830
+ /**
2831
+ * Walk the message array backwards to find the most recent user-role
2832
+ * message that represents a real user request (not a meta injection like
2833
+ * a memory brief or compression summary). Returns the text content, or
2834
+ * null if nothing qualifies.
2835
+ */
2836
+ findLatestNonMetaUserMessage(messages) {
2837
+ for (let i = messages.length - 1; i >= 0; i--) {
2838
+ const m = messages[i];
2839
+ if (m.role !== 'user')
2840
+ continue;
2841
+ const text = getTextContent(m.content);
2842
+ if (!text.trim())
2843
+ continue;
2844
+ if (isMetaPrefix(text))
2845
+ continue;
2846
+ return text;
2847
+ }
2848
+ return null;
2849
+ }
2850
+ /**
2851
+ * Append text to the first system-role message's content, or prepend a
2852
+ * new system message if none exists. Used to merge the directness hint
2853
+ * into the session prompt without creating a separate system message
2854
+ * (which would break Qwen's "system must be at beginning" rule).
2855
+ */
2856
+ appendToSystemMessage(messages, text) {
2857
+ if (messages.length > 0 && messages[0].role === 'system') {
2858
+ const existing = typeof messages[0].content === 'string' ? messages[0].content : '';
2859
+ return [
2860
+ { ...messages[0], content: existing + text },
2861
+ ...messages.slice(1),
2862
+ ];
2863
+ }
2864
+ // No system message — prepend one
2865
+ return [
2866
+ { role: 'system', content: text.trimStart() },
2867
+ ...messages,
2868
+ ];
2869
+ }
2870
+ /**
2871
+ * Classify a tool call as a read-only exploration call (file_read, glob,
2872
+ * grep, list_directory, find_symbol, project_index) vs a write/action
2873
+ * call (file_write, file_edit, bash, git_*, etc). Used by the exploration
2874
+ * budget tracker to count "reads before first write" for each run.
2875
+ */
2876
+ isReadOnlyToolCall(name) {
2877
+ return (name === 'file_read' ||
2878
+ name === 'glob' ||
2879
+ name === 'grep' ||
2880
+ name === 'list_directory' ||
2881
+ name === 'find_symbol' ||
2882
+ name === 'project_index' ||
2883
+ name === 'git_status' ||
2884
+ name === 'git_diff' ||
2885
+ name === 'docs_lookup');
2886
+ }
2887
+ isWriteCapableToolCall(name) {
2888
+ return (name === 'file_write' ||
2889
+ name === 'file_edit' ||
2890
+ name === 'bash' ||
2891
+ name === 'git_commit' ||
2892
+ name === 'git_create_pr');
2893
+ }
2894
+ /**
2895
+ * After each batch of tool calls, update the exploration budget state
2896
+ * and return a nudge message if the budget has been exceeded. The nudge
2897
+ * is a soft signal — it's injected into the next LLM call's context
2898
+ * telling the agent "you're stalling, commit to a direction." It never
2899
+ * hard-stops the run; graceful escalation is the design intent.
2900
+ *
2901
+ * Returns null if no nudge is needed, or the nudge message body if the
2902
+ * caller should inject it before the next turn.
2903
+ */
2904
+ maybeExplorationBudgetNudge(toolCalls) {
2905
+ // Count this batch's reads + detect any writes
2906
+ let batchReads = 0;
2907
+ let batchHadWrite = false;
2908
+ for (const tc of toolCalls) {
2909
+ if (this.isReadOnlyToolCall(tc.function.name))
2910
+ batchReads++;
2911
+ if (this.isWriteCapableToolCall(tc.function.name))
2912
+ batchHadWrite = true;
2913
+ }
2914
+ // If she wrote at all, mark the run as "past the exploration phase"
2915
+ // and stop counting. The budget is specifically about read-before-write.
2916
+ if (batchHadWrite || this.hasWrittenInThisRun) {
2917
+ this.hasWrittenInThisRun = true;
2918
+ return null;
2919
+ }
2920
+ this.readCountBeforeFirstWrite += batchReads;
2921
+ // Don't re-fire the nudge once it's fired — one soft signal per run
2922
+ if (this.explorationNudgeFired)
2923
+ return null;
2924
+ const budget = COMPLEXITY_BUDGETS[this.currentTaskComplexity];
2925
+ if (this.readCountBeforeFirstWrite < budget.readCapBeforeFirstWrite)
2926
+ return null;
2927
+ this.explorationNudgeFired = true;
2928
+ logger.debug(`[agent] Exploration budget nudge: ${this.readCountBeforeFirstWrite} reads before first write (cap ${budget.readCapBeforeFirstWrite}) for ${this.currentTaskComplexity} task`);
2929
+ return [
2930
+ `[Exploration budget check — ${this.readCountBeforeFirstWrite} read-only tool calls and zero writes so far on a ${this.currentTaskComplexity} task.]`,
2931
+ '',
2932
+ `You're past the comfortable exploration window for this task size. Two honest options:`,
2933
+ ` 1. You have enough context now — commit to a direction and make the change. Pick the most likely correct path and execute. You can always iterate.`,
2934
+ ` 2. The task is actually bigger than it looked at first — say so clearly in your next response ("this looked focused but it needs broader changes because..."), then continue exploring with justification.`,
2935
+ '',
2936
+ `What you MUST NOT do: keep reading files silently. Either commit, or explain why you need more context. Stalling is the one unacceptable outcome.`,
2937
+ ].join('\n');
2938
+ }
2939
+ // v3 graph integration (feedProceduralObserver / saveGraphState) was
2940
+ // removed from the hot path in the memory-cleanup sweep. Those features
2941
+ // read the live conversation and wrote derived state on every turn —
2942
+ // exactly the conversation→memory leak we are cutting. If procedural
2943
+ // learning and graph persistence come back, they belong in a scheduled
2944
+ // background job or an end-of-session hook, reading already-distilled
2945
+ // memory entries, not the live conversation.
2946
+ // ── Context usage ────────────────────────────────────────────────────────
2947
+ /** Get current context usage for a set of messages. */
2948
+ getContextUsage(messages) {
2949
+ const used = this.estimateTokenCount(messages);
2950
+ const limit = this.model.contextWindow;
2951
+ return { used, limit, percent: Math.round((used / limit) * 100) };
2952
+ }
2953
+ /** Manually compress context — triggered by user clicking the context bar. */
2954
+ async manualCompress(messages, onEvent, signal) {
2955
+ return this.compressContext(messages, onEvent, signal);
2956
+ }
2957
+ // ── Context compression ──────────────────────────────────────────────────
2958
+ /**
2959
+ * Compress conversation context by summarizing older messages.
2960
+ * Keeps the system prompt and last 8 messages (4 user-assistant exchanges)
2961
+ * verbatim, summarizes everything in between using the model.
2962
+ * Falls back silently if the compression API call fails.
2963
+ */
2964
+ async compressContext(messages, onEvent, signal) {
2965
+ onEvent({ type: 'context_compression_start' });
2966
+ const systemMsg = messages[0]?.role === 'system' ? messages[0] : null;
2967
+ const rest = systemMsg ? messages.slice(1) : [...messages];
2968
+ // Keep last 8 messages verbatim (4 exchange pairs) for better continuity
2969
+ const KEEP_RECENT = 8;
2970
+ if (rest.length <= KEEP_RECENT) {
2971
+ onEvent({ type: 'context_compression_end', originalTokens: 0, compressedTokens: 0 });
2972
+ return messages;
2973
+ }
2974
+ // ── Preserve the pinned original user task ─────────────────────────
2975
+ // The first real user message (not a meta injection like a memory brief
2976
+ // or compression summary) is the root intent of the whole session. It
2977
+ // must survive every compression pass or the post-compression agent
2978
+ // loses its sense of "what am I doing here" and fresh-greets the user.
2979
+ //
2980
+ // We find it by walking the pre-slice messages, and if it falls in the
2981
+ // compress zone (not already in the recent window), we pin it to be
2982
+ // re-added after the summary.
2983
+ const pinnedIdxInMessages = findOriginalUserTaskIndex(messages);
2984
+ const pinnedMessage = pinnedIdxInMessages !== -1 ? messages[pinnedIdxInMessages] : null;
2985
+ const pinnedIsInRecentWindow = pinnedIdxInMessages !== -1
2986
+ && pinnedIdxInMessages >= messages.length - KEEP_RECENT;
2987
+ const toCompress = rest.slice(0, -KEEP_RECENT);
2988
+ const toKeep = rest.slice(-KEEP_RECENT);
2989
+ // Build the text to summarize. Tool-role bodies are already trimmed to
2990
+ // ~200 chars by trimOldToolResults before we get here (token economy), so
2991
+ // the summariser sees each tool result's head, not raw JSON. Full tool
2992
+ // outputs remain in the persisted transcript and are retrievable via
2993
+ // conversation_recall — the backstop, not the summary, is the place for
2994
+ // exact tool detail.
2995
+ const transcript = toCompress
2996
+ .map((m) => {
2997
+ const text = getTextContent(m.content);
2998
+ return `[${m.role}]: ${text || '(no text)'}`;
2999
+ })
3000
+ .join('\n');
3001
+ const compressionPrompt = `You are a conversation summarizer preparing a handoff for an AI agent that will continue the work. The agent will have zero memory of this transcript except for what you produce, so your summary must be structured and decision-focused, not narrative.
3002
+
3003
+ Produce your output in EXACTLY this format:
3004
+
3005
+ CURRENT_TASK: <one sentence describing what the agent was actively working on at the end of the transcript. This is the single most important field — the agent uses it to decide what to do next. If multiple tasks were interleaved, pick the one that was most recently in flight.>
3006
+
3007
+ LAST_STEP: <one sentence describing the most recent concrete action the agent completed. Example: "Wrote src/components/HabitTracker.tsx with Tauri invoke calls for get_habit_logs."</ >
3008
+
3009
+ NEXT_STEP: <one sentence describing what the agent should do next to continue the task. Example: "Fix the missing habitId argument being passed to get_habit_logs in App.tsx." >
3010
+
3011
+ BLOCKERS: <any active blockers the agent needs to know about. Write "none" if there are none.>
3012
+
3013
+ SUMMARY:
3014
+ <Free-form bullet-point summary of everything else worth preserving: key decisions, file paths, function names, tool results, errors and how they were resolved, technical context. Be thorough but concise. Do NOT repeat what you put in the structured fields above.>
3015
+
3016
+ Rules:
3017
+ - Every field above is MANDATORY. If you can't extract a value for one, write "unclear" but never omit the field.
3018
+ - No pleasantries, no meta-commentary, no "Here's the summary" preamble.
3019
+ - Use plain text in the structured fields — no markdown, no bullet points, no multi-line values.
3020
+ - Keep the CURRENT_TASK, LAST_STEP, NEXT_STEP fields to a single sentence each.
3021
+
3022
+ TRANSCRIPT:
3023
+ ${transcript}`;
3024
+ // Scale the summary budget to how much is being compressed. A flat cap
3025
+ // under-summarises a large zone — 1500 tokens for a 300K-token compress
3026
+ // zone loses real fidelity. Proportional (~8%) keeps detail where there's
3027
+ // a lot to keep, with a 1500 floor (the old default, fine for small zones)
3028
+ // and a 4000 ceiling so the summary can't itself bloat the context it's
3029
+ // meant to shrink.
3030
+ const compressedTokens = this.estimateTokenCount(toCompress);
3031
+ const summaryBudget = Math.min(4000, Math.max(1500, Math.floor(compressedTokens / 12)));
3032
+ try {
3033
+ const response = await this.provider.createCompletion({
3034
+ model: this.model.id,
3035
+ messages: [
3036
+ { role: 'system', content: 'You are a precise conversation summarizer.' },
3037
+ { role: 'user', content: compressionPrompt },
3038
+ ],
3039
+ // Compression is a real cost the turn incurred, so it belongs to the
3040
+ // turn. Leaving it untagged would quietly understate what a long
3041
+ // conversation actually costs to answer.
3042
+ turnId: this.runTurnId,
3043
+ max_tokens: summaryBudget,
3044
+ temperature: 0.2,
3045
+ }, signal);
3046
+ // Meter the compression call. It's a heavy-model completion the same
3047
+ // shape as a chat turn, just summarising rather than answering a user.
3048
+ chargeCredits('chat_turn', {
3049
+ model: this.model.id,
3050
+ rawTokens: extractUsage(response.usage),
3051
+ });
3052
+ const summary = response.choices?.[0]?.message?.content || '';
3053
+ if (!summary)
3054
+ throw new Error('Empty compression response');
3055
+ // Memory policy: see notes at the earlier compression site. The
3056
+ // compression summary lives in the conversation history (persisted
3057
+ // per-conversation) — it does NOT get pushed into user or project
3058
+ // memory. Previously this block ran reflectAndSave on the compressed
3059
+ // messages and dumped the raw summary as a project memory entry.
3060
+ // Both paths created a conversation→memory feedback loop where
3061
+ // earlier turns' text got re-injected via memory on later turns,
3062
+ // shaping new responses, which then got saved again. Memory should
3063
+ // be durable user/project facts the model (or user) explicitly
3064
+ // chose to persist — not a rolling transcript of the conversation.
3065
+ // ── Build the continuation-first summary message ────────────────
3066
+ // Extract structured CURRENT_TASK / LAST_STEP / NEXT_STEP / BLOCKERS
3067
+ // fields from the summariser's output. The summariser prompt asks
3068
+ // for these explicitly but LLMs paraphrase — the parser is lenient.
3069
+ const structured = extractStructuredFields(summary);
3070
+ const continuationHeader = buildCompressionContinuationHeader(summary, structured);
3071
+ const summaryMessage = {
3072
+ role: 'user',
3073
+ content: continuationHeader,
3074
+ };
3075
+ // ── Build the session-tasks re-injection block ──────────────────
3076
+ // If the TaskManager has active session tasks, format them as a
3077
+ // continuation-focused block for direct injection into the
3078
+ // post-compression context. This is the single biggest signal that
3079
+ // stops the agent from treating compression as a fresh chat.
3080
+ let sessionTasksMessage = null;
3081
+ try {
3082
+ const tm = this.toolContext.sharedState?.taskManager;
3083
+ if (tm && typeof tm.getSessionTasks === 'function') {
3084
+ const tasks = tm.getSessionTasks();
3085
+ const block = formatSessionTasksBlock(tasks);
3086
+ if (block) {
3087
+ sessionTasksMessage = { role: 'user', content: block };
3088
+ }
3089
+ }
3090
+ }
3091
+ catch {
3092
+ /* non-critical — proceed without the task block */
3093
+ }
3094
+ // ── Fold original task into the system prompt instead of replaying ──
3095
+ // Previously this prepended the original user message verbatim as
3096
+ // a user-role message — which the model then treated as a freshly
3097
+ // sent user turn ("acts on the initial message again" bug). Fix:
3098
+ // merge the original task text into the system message as a
3099
+ // reference note. The model sees what the task was without
3100
+ // interpreting its replay as a new request.
3101
+ let pinnedNote = '';
3102
+ if (pinnedMessage && !pinnedIsInRecentWindow) {
3103
+ const pinnedText = getTextContent(pinnedMessage.content);
3104
+ if (pinnedText) {
3105
+ pinnedNote = `\n\n[Original request at session start] "${pinnedText.slice(0, 800)}" — context for what the user initially asked. You were already in the middle of working on this; continue from where you left off. Do NOT treat this as a new request.`;
3106
+ }
3107
+ }
3108
+ const fixedTail = this.fixToolPairing(toKeep);
3109
+ // Assembly order — simplified to remove the replayed original user
3110
+ // message. The continuation header (summaryMessage) now carries
3111
+ // CURRENT_TASK / LAST_STEP / NEXT_STEP, and the system prompt
3112
+ // carries the original-request note. The model reads top to bottom
3113
+ // and gets the full context without seeing what looks like a new
3114
+ // user turn.
3115
+ const middle = [summaryMessage];
3116
+ // Keep the user's own turns from the compress zone verbatim — the
3117
+ // summariser paraphrases them, and their exact words are the truest
3118
+ // record of intent. Framed as historical reference, not new requests.
3119
+ const verbatimUserTurns = buildVerbatimUserTurnsBlock(toCompress);
3120
+ if (verbatimUserTurns)
3121
+ middle.push(verbatimUserTurns);
3122
+ if (sessionTasksMessage)
3123
+ middle.push(sessionTasksMessage);
3124
+ const enrichedSystem = systemMsg
3125
+ ? { ...systemMsg, content: (typeof systemMsg.content === 'string' ? systemMsg.content : '') + pinnedNote }
3126
+ : (pinnedNote ? { role: 'system', content: pinnedNote.trimStart() } : null);
3127
+ const result = enrichedSystem
3128
+ ? [enrichedSystem, ...middle, ...fixedTail]
3129
+ : [...middle, ...fixedTail];
3130
+ const originalTokens = this.estimateTokenCount(messages);
3131
+ const compressedTokens = this.estimateTokenCount(result);
3132
+ onEvent({ type: 'context_compression_end', originalTokens, compressedTokens });
3133
+ // Emit updated context usage so UI bars refresh after compression
3134
+ const newPercent = Math.round((compressedTokens / this.model.contextWindow) * 100);
3135
+ onEvent({
3136
+ type: 'context_usage',
3137
+ context: { used: compressedTokens, limit: this.model.contextWindow, percent: newPercent },
3138
+ });
3139
+ return result;
3140
+ }
3141
+ catch {
3142
+ // Compression failed — fall back silently (caller will truncate if needed)
3143
+ onEvent({ type: 'context_compression_end', originalTokens: 0, compressedTokens: 0 });
3144
+ return messages;
3145
+ }
3146
+ }
3147
+ // ── Token estimation ──────────────────────────────────────────────────────
3148
+ static estimateTextTokens(text) {
3149
+ // Conservative: uses length/3 (not length/4) because code, JSON, and
3150
+ // tool results tokenize at ~2.5-3 chars per token.
3151
+ return Math.ceil(text.length / 3);
3152
+ }
3153
+ estimateMessageTokens(msg) {
3154
+ let tokens = 4; // message overhead (role, separators)
3155
+ const { content } = msg;
3156
+ if (content === null) {
3157
+ // no content
3158
+ }
3159
+ else if (typeof content === 'string') {
3160
+ tokens += Agent.estimateTextTokens(content);
3161
+ }
3162
+ else {
3163
+ for (const part of content) {
3164
+ if (part.type === 'text')
3165
+ tokens += Agent.estimateTextTokens(part.text);
3166
+ else if (part.type === 'image_url')
3167
+ tokens += 85;
3168
+ }
3169
+ }
3170
+ // Count tool calls in assistant messages (function name + JSON arguments)
3171
+ const toolCalls = msg.tool_calls;
3172
+ if (toolCalls) {
3173
+ for (const tc of toolCalls) {
3174
+ tokens += Agent.estimateTextTokens(tc.function.name) + Agent.estimateTextTokens(tc.function.arguments) + 8;
3175
+ }
3176
+ }
3177
+ return tokens;
3178
+ }
3179
+ /** Estimate total token count across an array of messages. */
3180
+ estimateTokenCount(messages) {
3181
+ return messages.reduce((sum, m) => sum + this.estimateMessageTokens(m), 0);
3182
+ }
3183
+ // ── Tool result trimming ────────────────────────────────────────────────
3184
+ /**
3185
+ * Collapse old tool results to save tokens. Tool outputs older than
3186
+ * KEEP_RECENT messages get trimmed to 200 chars + a note.
3187
+ * This prevents token bleed from accumulated file reads, grep results, etc.
3188
+ */
3189
+ /**
3190
+ * Trim older messages for token-cost control, preserving everything that
3191
+ * matters for continuity:
3192
+ * - The system prompt is never touched.
3193
+ * - The pinned original user task is preserved verbatim (it's the root
3194
+ * intent of the whole session and must survive every trim pass).
3195
+ * - The last 8 messages are kept verbatim for recent context.
3196
+ * - `tool`-role messages older than the recent window are trimmed to
3197
+ * MAX_OLD_TOOL_CHARS (very aggressive — 200 chars — because tool
3198
+ * outputs rarely matter in full once the next turn has consumed them).
3199
+ * - `user` and `assistant` message bodies older than the recent window
3200
+ * get trimmed if they exceed OLD_MESSAGE_BODY_MAX_CHARS. The structural
3201
+ * "who said what" stays intact but verbose inlined content gets cut.
3202
+ * - `reasoning_content` on old plain-text assistant messages is stripped
3203
+ * entirely. Reasoning is working memory for the turn that produced it
3204
+ * and has zero value once the next turn has landed — but it can be
3205
+ * 10x larger than the actual response and was previously kept forever.
3206
+ * Tool-calling assistant turns are an exception: DeepSeek V4 thinking
3207
+ * mode requires reasoning_content to be re-sent on every subsequent
3208
+ * request that follows a tool call, so it stays put on those.
3209
+ *
3210
+ * This is the primary lever for keeping per-turn token cost in check on
3211
+ * long sessions. Combined with the earlier compression trigger (40%
3212
+ * instead of 70%), it dramatically reduces the cost of running an agent
3213
+ * for 60+ minutes on a single conversation.
3214
+ */
3215
+ trimOldToolResults(messages) {
3216
+ const KEEP_RECENT = 8; // Keep last 8 messages at full size
3217
+ const MAX_OLD_TOOL_CHARS = 200;
3218
+ if (messages.length <= KEEP_RECENT + 1)
3219
+ return messages; // +1 for system
3220
+ const cutoff = messages.length - KEEP_RECENT;
3221
+ const pinnedIdx = findOriginalUserTaskIndex(messages);
3222
+ return messages.map((m, i) => {
3223
+ // Never touch the system prompt or messages in the recent window
3224
+ if (i === 0 || i >= cutoff)
3225
+ return m;
3226
+ // Never touch the pinned original user task — it's the root of the
3227
+ // whole session and must survive every trim pass
3228
+ if (i === pinnedIdx)
3229
+ return m;
3230
+ // ── Tool-role trimming (most aggressive) ─────────────────────────
3231
+ if (m.role === 'tool' && typeof m.content === 'string') {
3232
+ if (m.content.length <= MAX_OLD_TOOL_CHARS)
3233
+ return m;
3234
+ return {
3235
+ ...m,
3236
+ content: m.content.slice(0, MAX_OLD_TOOL_CHARS) + `\n\n[Trimmed — original ${m.content.length} chars]`,
3237
+ };
3238
+ }
3239
+ // ── Assistant-role: strip reasoning_content + trim body ───────────
3240
+ if (m.role === 'assistant') {
3241
+ const assistantMsg = m;
3242
+ const hasReasoning = assistantMsg.reasoning_content !== undefined && assistantMsg.reasoning_content !== null;
3243
+ const textContent = typeof assistantMsg.content === 'string' ? assistantMsg.content : null;
3244
+ const needsBodyTrim = textContent !== null && textContent.length > OLD_MESSAGE_BODY_MAX_CHARS;
3245
+ // DeepSeek V4 thinking-mode rule: assistant turns that produced
3246
+ // tool_calls MUST keep their reasoning_content in every subsequent
3247
+ // request, or the API rejects with 400 "reasoning_content in the
3248
+ // thinking mode must be passed back". Plain-text assistant turns
3249
+ // can still drop it (the field is ignored on those by DeepSeek and
3250
+ // by every other provider).
3251
+ const hasToolCalls = Array.isArray(assistantMsg.tool_calls) && assistantMsg.tool_calls.length > 0;
3252
+ const stripReasoning = hasReasoning && !hasToolCalls;
3253
+ if (!stripReasoning && !needsBodyTrim)
3254
+ return m;
3255
+ const trimmed = {
3256
+ ...assistantMsg,
3257
+ // Reasoning is stripped from old plain-text turns (zero value once
3258
+ // the next turn is live, and often the biggest single allocation
3259
+ // in a long conversation's token budget) but PRESERVED on
3260
+ // tool-calling turns for DeepSeek V4 multi-turn correctness.
3261
+ ...(stripReasoning ? { reasoning_content: null } : {}),
3262
+ // Body is trimmed only if it's over threshold
3263
+ content: needsBodyTrim && textContent !== null
3264
+ ? trimMessageBody(textContent)
3265
+ : assistantMsg.content,
3266
+ };
3267
+ return trimmed;
3268
+ }
3269
+ // ── User-role: trim long bodies (skip meta-prefixed messages) ─────
3270
+ if (m.role === 'user' && typeof m.content === 'string') {
3271
+ // Don't trim meta-prefixed messages (compression summaries, memory
3272
+ // briefs, system notices, task blocks) — their headers matter and
3273
+ // they're usually already short enough anyway.
3274
+ if (isMetaPrefix(m.content))
3275
+ return m;
3276
+ if (m.content.length <= OLD_MESSAGE_BODY_MAX_CHARS)
3277
+ return m;
3278
+ return { ...m, content: trimMessageBody(m.content) };
3279
+ }
3280
+ return m;
3281
+ });
3282
+ }
3283
+ // ── Truncation ──────────────────────────────────────────────────────────
3284
+ truncateMessages(messages, maxTokens) {
3285
+ const total = messages.reduce((sum, m) => sum + this.estimateMessageTokens(m), 0);
3286
+ if (total <= maxTokens)
3287
+ return messages;
3288
+ // Keep system prompt (first message) and trim from the beginning of the rest
3289
+ const systemMsg = messages[0]?.role === 'system' ? messages[0] : null;
3290
+ const rest = systemMsg ? messages.slice(1) : [...messages];
3291
+ const systemTokens = systemMsg ? this.estimateMessageTokens(systemMsg) : 0;
3292
+ // ── Preserve the pinned original user task ──────────────────────
3293
+ // Same reasoning as compression paths: the root intent of the session
3294
+ // must survive even emergency truncation. We reserve tokens for it
3295
+ // upfront and then fill the rest of the budget from the most recent
3296
+ // messages backwards.
3297
+ const pinnedIdx = findOriginalUserTaskIndex(messages);
3298
+ const pinnedMsg = pinnedIdx !== -1 ? messages[pinnedIdx] : null;
3299
+ const pinnedTokens = pinnedMsg ? this.estimateMessageTokens(pinnedMsg) : 0;
3300
+ const budget = maxTokens - systemTokens - pinnedTokens;
3301
+ const kept = [];
3302
+ let used = 0;
3303
+ for (let i = rest.length - 1; i >= 0; i--) {
3304
+ // Skip the pinned message during the backward walk — it will be
3305
+ // re-inserted at the pinned slot at the end. Including it twice
3306
+ // would double-charge its tokens and confuse the final order.
3307
+ if (pinnedMsg && rest[i] === pinnedMsg)
3308
+ continue;
3309
+ const msgTokens = this.estimateMessageTokens(rest[i]);
3310
+ if (used + msgTokens > budget)
3311
+ break;
3312
+ kept.unshift(rest[i]);
3313
+ used += msgTokens;
3314
+ }
3315
+ // Fix orphaned tool messages — if truncation cut in the middle of a
3316
+ // tool call/result sequence, the kept list may start with `tool` messages
3317
+ // that reference a dropped assistant message. The API rejects these.
3318
+ // Also drop any assistant messages whose tool_calls lost their results.
3319
+ const fixed = this.fixToolPairing(kept);
3320
+ // Fold the original task into the system prompt as a reference note
3321
+ // instead of re-injecting it as a user message. Re-injection made the
3322
+ // model treat the replay as a fresh user turn ("acts on initial
3323
+ // message again"). A system-prompt note preserves the task context
3324
+ // without the new-input signal.
3325
+ let enrichedSystem = systemMsg;
3326
+ if (pinnedMsg && systemMsg) {
3327
+ const pinnedText = getTextContent(pinnedMsg.content);
3328
+ if (pinnedText) {
3329
+ const pinnedNote = `\n\n[Original request at session start] "${pinnedText.slice(0, 800)}" — context for what the user initially asked. You were already in the middle of working on this; continue from where you left off. Do NOT treat this as a new request.`;
3330
+ enrichedSystem = { ...systemMsg, content: (typeof systemMsg.content === 'string' ? systemMsg.content : '') + pinnedNote };
3331
+ }
3332
+ }
3333
+ return enrichedSystem ? [enrichedSystem, ...fixed] : fixed;
3334
+ }
3335
+ /**
3336
+ * Detect the "first turn after Stop" pattern and strip prior conversation.
3337
+ *
3338
+ * When cancelRun() fires in the extension, a marker is pushed into the
3339
+ * conversation: `[User pressed Stop — previous task terminated...]`.
3340
+ * After the user's next real message, the conversation looks like:
3341
+ *
3342
+ * [... prior task, maybe 150K tokens ...]
3343
+ * [User pressed Stop — ...] (meta user message)
3344
+ * Actual new user message
3345
+ *
3346
+ * The prior 150K tokens are dead weight. Sending them back means the
3347
+ * model re-draws on the task the user told us to abandon. The fix is
3348
+ * to detect this pattern and return a restricted message array:
3349
+ *
3350
+ * [system prompt + stop directive]
3351
+ * [new user message]
3352
+ *
3353
+ * Detection: find the most recent stop marker. If it exists AND there
3354
+ * is exactly one non-meta user message after it (and the assistant
3355
+ * hasn't yet responded to that message), we're in the first post-stop
3356
+ * turn — restrict.
3357
+ *
3358
+ * If more messages exist after that point (assistant replies, tool
3359
+ * results), we've already handled the first post-stop turn normally;
3360
+ * further turns see full context and operate as normal.
3361
+ */
3362
+ maybeRestrictPostStopContext(messages) {
3363
+ // Find the most recent stop marker (a user-role message starting with the marker prefix).
3364
+ let markerIdx = -1;
3365
+ for (let i = messages.length - 1; i >= 0; i--) {
3366
+ const m = messages[i];
3367
+ if (m.role !== 'user')
3368
+ continue;
3369
+ const text = getTextContent(m.content);
3370
+ if (text.trimStart().startsWith('[User pressed Stop')) {
3371
+ markerIdx = i;
3372
+ break;
3373
+ }
3374
+ }
3375
+ if (markerIdx < 0)
3376
+ return messages;
3377
+ // Everything after the marker must be: exactly one non-meta user
3378
+ // message, nothing else. If there's a REAL assistant reply after the
3379
+ // marker, we're past the first post-stop turn — don't restrict.
3380
+ //
3381
+ // Critical edge case: if the abort fired mid-stream, an empty or
3382
+ // partial assistant message can sit in the transcript (assistant
3383
+ // started speaking, stop fired, no content). That half-message must
3384
+ // NOT count as a "real response" or the restriction bails and all
3385
+ // pre-stop context leaks into the next turn. An empty string, an
3386
+ // empty content parts array, or pure whitespace all count as "no
3387
+ // real response" for this purpose.
3388
+ const afterMarker = messages.slice(markerIdx + 1);
3389
+ const nonMetaUsers = afterMarker.filter(m => m.role === 'user' && !isMetaPrefix(getTextContent(m.content)));
3390
+ const hasRealAssistantResponse = afterMarker.some(m => {
3391
+ if (m.role !== 'assistant')
3392
+ return false;
3393
+ const text = getTextContent(m.content);
3394
+ if (text.trim().length > 0)
3395
+ return true;
3396
+ const hasToolCalls = Array.isArray(m.tool_calls)
3397
+ && m.tool_calls.length > 0;
3398
+ return hasToolCalls;
3399
+ });
3400
+ if (nonMetaUsers.length !== 1 || hasRealAssistantResponse) {
3401
+ return messages;
3402
+ }
3403
+ const newUserMessage = nonMetaUsers[0];
3404
+ const systemMsg = messages.find(m => m.role === 'system');
3405
+ // Merge the stop directive into the system prompt so the model sees
3406
+ // it at the highest-authority layer, not as a floating user message
3407
+ // (which some providers reject when followed by another user msg).
3408
+ const stopDirective = `\n\n[Post-stop context] The previous task was terminated by the user. Do not resume it. Treat the user's message below as a fresh request on its own terms. If they reference prior work ambiguously (e.g. "fix that", "continue"), ask them to be specific — you do not have the prior context and should not assume.`;
3409
+ const restrictedSystem = systemMsg
3410
+ ? { ...systemMsg, content: (typeof systemMsg.content === 'string' ? systemMsg.content : '') + stopDirective }
3411
+ : { role: 'system', content: stopDirective.trimStart() };
3412
+ logger.debug(`[agent] Post-stop context restriction: dropped ${messages.length - 2} prior messages, keeping system + new user message`);
3413
+ return restrictedSystem ? [restrictedSystem, newUserMessage] : [newUserMessage];
3414
+ }
3415
+ /**
3416
+ * Age out old images and old tool results to cut per-turn token cost.
3417
+ *
3418
+ * Keeps the N most recent image-bearing user messages and the M most
3419
+ * recent tool results in full fidelity. Older ones become text-only
3420
+ * placeholders. The assistant's prior reasoning about the content
3421
+ * remains intact in the assistant messages — we're just dropping raw
3422
+ * payloads that the model no longer needs pixel-for-pixel.
3423
+ *
3424
+ * Why this exists: a single 25K-token screenshot re-sent across 10
3425
+ * turns burns 250K tokens for no informational gain after turn 2 or 3.
3426
+ * Same applies to verbose bash/file_read output: after the assistant
3427
+ * has reasoned about it, we don't need the full dump in context
3428
+ * anymore. This is the single biggest lever on token consumption in
3429
+ * the agent loop. Conservative keep counts (2 images, 5 tool results)
3430
+ * preserve enough active context for normal multi-step work while
3431
+ * eliminating the long tail of stale payloads.
3432
+ */
3433
+ ageHistoryContent(messages) {
3434
+ const KEEP_RECENT_IMAGES = 2;
3435
+ // DIAGNOSTIC: tool-result trimming disabled while we confirm it's
3436
+ // corrupting Qwen's function-calling expectations. Leaving trimmed
3437
+ // summaries in role:'tool' messages appears to make the model emit
3438
+ // malformed tool_calls (empty function.name) and fall back to
3439
+ // text-format tool calls. Image trimming stays on (biggest savings,
3440
+ // doesn't touch tool-pair structure). If disabling this resolves
3441
+ // the empty-name tool_call regression, the real fix is to collapse
3442
+ // old tool_call + tool_result pairs into a single assistant text
3443
+ // summary rather than leaving orphan-style summaries in tool slots.
3444
+ const KEEP_RECENT_TOOL_RESULTS = Number.POSITIVE_INFINITY;
3445
+ const TOOL_RESULT_TRIM_THRESHOLD = 300;
3446
+ // Walk backward to find indices of the N most recent image-bearing
3447
+ // user messages and tool results.
3448
+ const imageIndicesSeen = [];
3449
+ const toolIndicesSeen = [];
3450
+ for (let i = messages.length - 1; i >= 0; i--) {
3451
+ const m = messages[i];
3452
+ if (m.role === 'user' && Array.isArray(m.content)) {
3453
+ const hasImage = m.content.some(p => p.type === 'image_url');
3454
+ if (hasImage)
3455
+ imageIndicesSeen.push(i);
3456
+ }
3457
+ if (m.role === 'tool')
3458
+ toolIndicesSeen.push(i);
3459
+ }
3460
+ const imageIndicesToStrip = new Set(imageIndicesSeen.slice(KEEP_RECENT_IMAGES));
3461
+ const toolIndicesToTrim = new Set(toolIndicesSeen.slice(KEEP_RECENT_TOOL_RESULTS));
3462
+ if (imageIndicesToStrip.size === 0 && toolIndicesToTrim.size === 0) {
3463
+ return messages;
3464
+ }
3465
+ return messages.map((m, i) => {
3466
+ // Strip image payload from old image-bearing user messages.
3467
+ if (imageIndicesToStrip.has(i) && Array.isArray(m.content)) {
3468
+ const textParts = m.content
3469
+ .filter(p => p.type === 'text')
3470
+ .map(p => ('text' in p ? p.text : ''))
3471
+ .filter(Boolean)
3472
+ .join(' ')
3473
+ .trim();
3474
+ const placeholder = textParts
3475
+ ? `${textParts} — image discarded from history to save context. Re-capture if you need to see it again.`
3476
+ : '[Image previously captured — discarded from history to save context. Re-capture if needed.]';
3477
+ return { ...m, content: placeholder };
3478
+ }
3479
+ // Trim old tool results to a short summary.
3480
+ if (toolIndicesToTrim.has(i) && typeof m.content === 'string' && m.content.length > TOOL_RESULT_TRIM_THRESHOLD) {
3481
+ const preview = m.content.slice(0, 160).replace(/\s+/g, ' ').trim();
3482
+ const toolName = m.name || 'tool';
3483
+ return {
3484
+ ...m,
3485
+ content: `[${toolName} result from earlier turn — ${m.content.length} chars trimmed to save context. Preview: ${preview}...]`,
3486
+ };
3487
+ }
3488
+ return m;
3489
+ });
3490
+ }
3491
+ /**
3492
+ * Public wrapper around fixToolPairing for post-error recovery flows.
3493
+ * When a provider returns 400, the message history may have landed in
3494
+ * an invalid shape (orphan tool_calls, unmatched tool results). The
3495
+ * extension's Retry handler calls this on the live conversation before
3496
+ * issuing another request so the user isn't stuck in a loop of the
3497
+ * same broken payload bouncing off the provider.
3498
+ */
3499
+ repairMessages(messages) {
3500
+ return this.fixToolPairing(messages);
3501
+ }
3502
+ /**
3503
+ * Ensure every `tool` message has a preceding `assistant` with a matching
3504
+ * `tool_calls` entry, and every `assistant` with `tool_calls` has all its
3505
+ * `tool` results following it. Drops orphans from the front.
3506
+ */
3507
+ fixToolPairing(messages) {
3508
+ // 1. Drop leading orphaned tool messages (their assistant parent was truncated)
3509
+ let start = 0;
3510
+ while (start < messages.length && messages[start].role === 'tool') {
3511
+ start++;
3512
+ }
3513
+ if (start === messages.length)
3514
+ return [];
3515
+ const trimmed = start > 0 ? messages.slice(start) : messages;
3516
+ // 2. Scan ALL messages — remove any tool message whose parent assistant
3517
+ // (with matching tool_call_id) is not in the conversation
3518
+ const assistantToolCallIds = new Set();
3519
+ for (const m of trimmed) {
3520
+ if (m.role === 'assistant') {
3521
+ const toolCalls = m.tool_calls;
3522
+ if (toolCalls) {
3523
+ for (const tc of toolCalls) {
3524
+ assistantToolCallIds.add(tc.id);
3525
+ }
3526
+ }
3527
+ }
3528
+ }
3529
+ const fixed = trimmed.filter(m => {
3530
+ if (m.role === 'tool') {
3531
+ const toolMsg = m;
3532
+ return assistantToolCallIds.has(toolMsg.tool_call_id ?? '');
3533
+ }
3534
+ return true;
3535
+ });
3536
+ // 3. Check for assistant messages with tool_calls but missing ALL tool results
3537
+ // (incomplete pair) — remove them too
3538
+ const toolResultIds = new Set();
3539
+ for (const m of fixed) {
3540
+ if (m.role === 'tool') {
3541
+ const toolMsg = m;
3542
+ if (toolMsg.tool_call_id)
3543
+ toolResultIds.add(toolMsg.tool_call_id);
3544
+ }
3545
+ }
3546
+ return fixed.filter(m => {
3547
+ if (m.role === 'assistant') {
3548
+ const toolCalls = m.tool_calls;
3549
+ if (toolCalls && toolCalls.length > 0) {
3550
+ // Keep only if at least one tool result exists
3551
+ return toolCalls.some(tc => toolResultIds.has(tc.id));
3552
+ }
3553
+ }
3554
+ return true;
3555
+ });
3556
+ }
3557
+ }
3558
+ //# sourceMappingURL=agent.js.map