@caupulican/pi-adaptative 0.81.38 → 0.81.39

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 (433) hide show
  1. package/CHANGELOG.md +79 -0
  2. package/dist/bundled-resources/extensions/tmux-agent-manager/README.md +18 -1
  3. package/dist/bundled-resources/extensions/tmux-agent-manager/dispatch-grant.d.ts +131 -0
  4. package/dist/bundled-resources/extensions/tmux-agent-manager/dispatch-grant.d.ts.map +1 -0
  5. package/dist/bundled-resources/extensions/tmux-agent-manager/dispatch-grant.js +196 -0
  6. package/dist/bundled-resources/extensions/tmux-agent-manager/dispatch-grant.js.map +1 -0
  7. package/dist/bundled-resources/extensions/tmux-agent-manager/dispatch-grant.ts +308 -0
  8. package/dist/bundled-resources/extensions/tmux-agent-manager/index.d.ts +22 -2
  9. package/dist/bundled-resources/extensions/tmux-agent-manager/index.d.ts.map +1 -1
  10. package/dist/bundled-resources/extensions/tmux-agent-manager/index.js +585 -24
  11. package/dist/bundled-resources/extensions/tmux-agent-manager/index.js.map +1 -1
  12. package/dist/bundled-resources/extensions/tmux-agent-manager/index.ts +749 -27
  13. package/dist/bundled-resources/runtimes/pi-shell-engine/commands/__init__.py +43 -0
  14. package/dist/bundled-resources/runtimes/pi-shell-engine/commands/fs.py +270 -0
  15. package/dist/bundled-resources/runtimes/pi-shell-engine/commands/search.py +252 -0
  16. package/dist/bundled-resources/runtimes/pi-shell-engine/commands/strings.py +399 -0
  17. package/dist/bundled-resources/runtimes/pi-shell-engine/commands/text.py +575 -0
  18. package/dist/bundled-resources/runtimes/pi-shell-engine/context.py +52 -0
  19. package/dist/bundled-resources/runtimes/pi-shell-engine/errors.py +49 -0
  20. package/dist/bundled-resources/runtimes/pi-shell-engine/exec.py +734 -0
  21. package/dist/bundled-resources/runtimes/pi-shell-engine/expand.py +238 -0
  22. package/dist/bundled-resources/runtimes/pi-shell-engine/main.py +132 -0
  23. package/dist/bundled-resources/runtimes/pi-shell-engine/nodes.py +116 -0
  24. package/dist/bundled-resources/runtimes/pi-shell-engine/parser.py +287 -0
  25. package/dist/bundled-resources/runtimes/pi-shell-engine/proc.py +137 -0
  26. package/dist/bundled-resources/runtimes/pi-shell-engine/state.py +100 -0
  27. package/dist/bundled-resources/runtimes/pi-shell-engine/tokens.py +579 -0
  28. package/dist/bundled-resources/skills/tool-call-repair/SKILL.md +19 -9
  29. package/dist/bundled-resources/skills/tool-call-repair/references/failure-grammar.md +22 -5
  30. package/dist/bundled-resources/skills/tool-call-repair/references/repair-catalogue.md +5 -5
  31. package/dist/bundled-resources/skills/tool-call-repair/references/text-protocol-grammar.md +28 -7
  32. package/dist/cli/args.d.ts +3 -0
  33. package/dist/cli/args.d.ts.map +1 -1
  34. package/dist/cli/args.js +15 -0
  35. package/dist/cli/args.js.map +1 -1
  36. package/dist/core/agent-paths.d.ts +49 -0
  37. package/dist/core/agent-paths.d.ts.map +1 -0
  38. package/dist/core/agent-paths.js +107 -0
  39. package/dist/core/agent-paths.js.map +1 -0
  40. package/dist/core/agent-session-services.d.ts.map +1 -1
  41. package/dist/core/agent-session-services.js +3 -3
  42. package/dist/core/agent-session-services.js.map +1 -1
  43. package/dist/core/agent-session.d.ts +234 -17
  44. package/dist/core/agent-session.d.ts.map +1 -1
  45. package/dist/core/agent-session.js +484 -62
  46. package/dist/core/agent-session.js.map +1 -1
  47. package/dist/core/autonomy/contracts.d.ts +9 -0
  48. package/dist/core/autonomy/contracts.d.ts.map +1 -1
  49. package/dist/core/autonomy/contracts.js.map +1 -1
  50. package/dist/core/autonomy/lane-tracker.d.ts +10 -1
  51. package/dist/core/autonomy/lane-tracker.d.ts.map +1 -1
  52. package/dist/core/autonomy/lane-tracker.js +5 -1
  53. package/dist/core/autonomy/lane-tracker.js.map +1 -1
  54. package/dist/core/background-lane-controller.d.ts +122 -6
  55. package/dist/core/background-lane-controller.d.ts.map +1 -1
  56. package/dist/core/background-lane-controller.js +446 -89
  57. package/dist/core/background-lane-controller.js.map +1 -1
  58. package/dist/core/bash-execution-controller.d.ts +4 -0
  59. package/dist/core/bash-execution-controller.d.ts.map +1 -1
  60. package/dist/core/bash-execution-controller.js +7 -1
  61. package/dist/core/bash-execution-controller.js.map +1 -1
  62. package/dist/core/compaction-support.d.ts +13 -3
  63. package/dist/core/compaction-support.d.ts.map +1 -1
  64. package/dist/core/compaction-support.js +43 -7
  65. package/dist/core/compaction-support.js.map +1 -1
  66. package/dist/core/context/context-audit.d.ts +33 -1
  67. package/dist/core/context/context-audit.d.ts.map +1 -1
  68. package/dist/core/context/context-audit.js +31 -5
  69. package/dist/core/context/context-audit.js.map +1 -1
  70. package/dist/core/context-gc.d.ts.map +1 -1
  71. package/dist/core/context-gc.js +16 -1
  72. package/dist/core/context-gc.js.map +1 -1
  73. package/dist/core/context-pipeline.d.ts +26 -4
  74. package/dist/core/context-pipeline.d.ts.map +1 -1
  75. package/dist/core/context-pipeline.js +137 -14
  76. package/dist/core/context-pipeline.js.map +1 -1
  77. package/dist/core/cost-guard.d.ts +19 -3
  78. package/dist/core/cost-guard.d.ts.map +1 -1
  79. package/dist/core/cost-guard.js +18 -3
  80. package/dist/core/cost-guard.js.map +1 -1
  81. package/dist/core/delegation/session-worker-result.d.ts +34 -4
  82. package/dist/core/delegation/session-worker-result.d.ts.map +1 -1
  83. package/dist/core/delegation/session-worker-result.js +64 -4
  84. package/dist/core/delegation/session-worker-result.js.map +1 -1
  85. package/dist/core/delegation/worker-result.d.ts +42 -1
  86. package/dist/core/delegation/worker-result.d.ts.map +1 -1
  87. package/dist/core/delegation/worker-result.js +68 -0
  88. package/dist/core/delegation/worker-result.js.map +1 -1
  89. package/dist/core/extensions/loader.d.ts.map +1 -1
  90. package/dist/core/extensions/loader.js +20 -0
  91. package/dist/core/extensions/loader.js.map +1 -1
  92. package/dist/core/extensions/runner.d.ts.map +1 -1
  93. package/dist/core/extensions/runner.js +1 -0
  94. package/dist/core/extensions/runner.js.map +1 -1
  95. package/dist/core/extensions/types.d.ts +51 -0
  96. package/dist/core/extensions/types.d.ts.map +1 -1
  97. package/dist/core/extensions/types.js.map +1 -1
  98. package/dist/core/goal-loop-controller.d.ts +17 -1
  99. package/dist/core/goal-loop-controller.d.ts.map +1 -1
  100. package/dist/core/goal-loop-controller.js +79 -11
  101. package/dist/core/goal-loop-controller.js.map +1 -1
  102. package/dist/core/goals/goal-continuation-controller.d.ts +48 -2
  103. package/dist/core/goals/goal-continuation-controller.d.ts.map +1 -1
  104. package/dist/core/goals/goal-continuation-controller.js +77 -0
  105. package/dist/core/goals/goal-continuation-controller.js.map +1 -1
  106. package/dist/core/goals/goal-continuation-defaults.d.ts +39 -0
  107. package/dist/core/goals/goal-continuation-defaults.d.ts.map +1 -1
  108. package/dist/core/goals/goal-continuation-defaults.js +42 -0
  109. package/dist/core/goals/goal-continuation-defaults.js.map +1 -1
  110. package/dist/core/goals/goal-continuation-prompt.d.ts +4 -0
  111. package/dist/core/goals/goal-continuation-prompt.d.ts.map +1 -1
  112. package/dist/core/goals/goal-continuation-prompt.js +51 -10
  113. package/dist/core/goals/goal-continuation-prompt.js.map +1 -1
  114. package/dist/core/goals/goal-runtime-snapshot.d.ts +101 -2
  115. package/dist/core/goals/goal-runtime-snapshot.d.ts.map +1 -1
  116. package/dist/core/goals/goal-runtime-snapshot.js +87 -4
  117. package/dist/core/goals/goal-runtime-snapshot.js.map +1 -1
  118. package/dist/core/goals/goal-state.d.ts +89 -1
  119. package/dist/core/goals/goal-state.d.ts.map +1 -1
  120. package/dist/core/goals/goal-state.js +71 -5
  121. package/dist/core/goals/goal-state.js.map +1 -1
  122. package/dist/core/goals/goal-tool-core.d.ts +56 -2
  123. package/dist/core/goals/goal-tool-core.d.ts.map +1 -1
  124. package/dist/core/goals/goal-tool-core.js +111 -5
  125. package/dist/core/goals/goal-tool-core.js.map +1 -1
  126. package/dist/core/goals/session-goal-state.d.ts +11 -2
  127. package/dist/core/goals/session-goal-state.d.ts.map +1 -1
  128. package/dist/core/goals/session-goal-state.js +32 -17
  129. package/dist/core/goals/session-goal-state.js.map +1 -1
  130. package/dist/core/keybindings.d.ts +10 -0
  131. package/dist/core/keybindings.d.ts.map +1 -1
  132. package/dist/core/keybindings.js +10 -2
  133. package/dist/core/keybindings.js.map +1 -1
  134. package/dist/core/learning/observation-store.d.ts +12 -3
  135. package/dist/core/learning/observation-store.d.ts.map +1 -1
  136. package/dist/core/learning/observation-store.js +30 -15
  137. package/dist/core/learning/observation-store.js.map +1 -1
  138. package/dist/core/learning/skill-curator.d.ts +5 -1
  139. package/dist/core/learning/skill-curator.d.ts.map +1 -1
  140. package/dist/core/learning/skill-curator.js +21 -19
  141. package/dist/core/learning/skill-curator.js.map +1 -1
  142. package/dist/core/local-runtime-controller.d.ts +65 -3
  143. package/dist/core/local-runtime-controller.d.ts.map +1 -1
  144. package/dist/core/local-runtime-controller.js +186 -26
  145. package/dist/core/local-runtime-controller.js.map +1 -1
  146. package/dist/core/memory/providers/file-store.d.ts +1 -1
  147. package/dist/core/memory/providers/file-store.d.ts.map +1 -1
  148. package/dist/core/memory/providers/file-store.js +6 -6
  149. package/dist/core/memory/providers/file-store.js.map +1 -1
  150. package/dist/core/model-capability.d.ts +34 -0
  151. package/dist/core/model-capability.d.ts.map +1 -1
  152. package/dist/core/model-capability.js +42 -1
  153. package/dist/core/model-capability.js.map +1 -1
  154. package/dist/core/model-router/tool-escalation.d.ts +15 -0
  155. package/dist/core/model-router/tool-escalation.d.ts.map +1 -1
  156. package/dist/core/model-router/tool-escalation.js +23 -1
  157. package/dist/core/model-router/tool-escalation.js.map +1 -1
  158. package/dist/core/model-router-controller.d.ts +34 -7
  159. package/dist/core/model-router-controller.d.ts.map +1 -1
  160. package/dist/core/model-router-controller.js +95 -16
  161. package/dist/core/model-router-controller.js.map +1 -1
  162. package/dist/core/models/adaptation-store.d.ts +7 -0
  163. package/dist/core/models/adaptation-store.d.ts.map +1 -1
  164. package/dist/core/models/adaptation-store.js +47 -11
  165. package/dist/core/models/adaptation-store.js.map +1 -1
  166. package/dist/core/models/default-model-suggestions.d.ts.map +1 -1
  167. package/dist/core/models/default-model-suggestions.js +17 -0
  168. package/dist/core/models/default-model-suggestions.js.map +1 -1
  169. package/dist/core/models/fitness-store.d.ts +3 -0
  170. package/dist/core/models/fitness-store.d.ts.map +1 -1
  171. package/dist/core/models/fitness-store.js +11 -2
  172. package/dist/core/models/fitness-store.js.map +1 -1
  173. package/dist/core/models/llamacpp-runtime.d.ts +180 -0
  174. package/dist/core/models/llamacpp-runtime.d.ts.map +1 -0
  175. package/dist/core/models/llamacpp-runtime.js +475 -0
  176. package/dist/core/models/llamacpp-runtime.js.map +1 -0
  177. package/dist/core/models/local-registration.d.ts +40 -0
  178. package/dist/core/models/local-registration.d.ts.map +1 -1
  179. package/dist/core/models/local-registration.js +94 -5
  180. package/dist/core/models/local-registration.js.map +1 -1
  181. package/dist/core/models/local-runtime.d.ts.map +1 -1
  182. package/dist/core/models/local-runtime.js +6 -5
  183. package/dist/core/models/local-runtime.js.map +1 -1
  184. package/dist/core/models/model-ref.d.ts +7 -0
  185. package/dist/core/models/model-ref.d.ts.map +1 -1
  186. package/dist/core/models/model-ref.js +26 -0
  187. package/dist/core/models/model-ref.js.map +1 -1
  188. package/dist/core/models/needle-runtime.d.ts +257 -0
  189. package/dist/core/models/needle-runtime.d.ts.map +1 -0
  190. package/dist/core/models/needle-runtime.js +519 -0
  191. package/dist/core/models/needle-runtime.js.map +1 -0
  192. package/dist/core/models/prism-llamacpp-lifecycle.d.ts +89 -0
  193. package/dist/core/models/prism-llamacpp-lifecycle.d.ts.map +1 -0
  194. package/dist/core/models/prism-llamacpp-lifecycle.js +121 -0
  195. package/dist/core/models/prism-llamacpp-lifecycle.js.map +1 -0
  196. package/dist/core/package-manager.d.ts.map +1 -1
  197. package/dist/core/package-manager.js +11 -10
  198. package/dist/core/package-manager.js.map +1 -1
  199. package/dist/core/process-matrix/codes.d.ts +72 -0
  200. package/dist/core/process-matrix/codes.d.ts.map +1 -0
  201. package/dist/core/process-matrix/codes.js +15 -0
  202. package/dist/core/process-matrix/codes.js.map +1 -0
  203. package/dist/core/process-matrix/runtime.d.ts +63 -0
  204. package/dist/core/process-matrix/runtime.d.ts.map +1 -0
  205. package/dist/core/process-matrix/runtime.js +310 -0
  206. package/dist/core/process-matrix/runtime.js.map +1 -0
  207. package/dist/core/process-matrix/store.d.ts +22 -0
  208. package/dist/core/process-matrix/store.d.ts.map +1 -0
  209. package/dist/core/process-matrix/store.js +80 -0
  210. package/dist/core/process-matrix/store.js.map +1 -0
  211. package/dist/core/process-matrix/supervisor.d.ts +72 -0
  212. package/dist/core/process-matrix/supervisor.d.ts.map +1 -0
  213. package/dist/core/process-matrix/supervisor.js +130 -0
  214. package/dist/core/process-matrix/supervisor.js.map +1 -0
  215. package/dist/core/profile-registry.d.ts +7 -0
  216. package/dist/core/profile-registry.d.ts.map +1 -1
  217. package/dist/core/profile-registry.js +20 -0
  218. package/dist/core/profile-registry.js.map +1 -1
  219. package/dist/core/python-runtime.d.ts.map +1 -1
  220. package/dist/core/python-runtime.js +3 -3
  221. package/dist/core/python-runtime.js.map +1 -1
  222. package/dist/core/reflection-controller.d.ts +29 -5
  223. package/dist/core/reflection-controller.d.ts.map +1 -1
  224. package/dist/core/reflection-controller.js +215 -126
  225. package/dist/core/reflection-controller.js.map +1 -1
  226. package/dist/core/reload-blockers.d.ts +36 -0
  227. package/dist/core/reload-blockers.d.ts.map +1 -1
  228. package/dist/core/reload-blockers.js +44 -0
  229. package/dist/core/reload-blockers.js.map +1 -1
  230. package/dist/core/resource-loader.d.ts.map +1 -1
  231. package/dist/core/resource-loader.js +8 -7
  232. package/dist/core/resource-loader.js.map +1 -1
  233. package/dist/core/runtime-builder.d.ts +58 -5
  234. package/dist/core/runtime-builder.d.ts.map +1 -1
  235. package/dist/core/runtime-builder.js +209 -25
  236. package/dist/core/runtime-builder.js.map +1 -1
  237. package/dist/core/scout-controller.d.ts +6 -0
  238. package/dist/core/scout-controller.d.ts.map +1 -1
  239. package/dist/core/scout-controller.js +66 -52
  240. package/dist/core/scout-controller.js.map +1 -1
  241. package/dist/core/sdk.d.ts.map +1 -1
  242. package/dist/core/sdk.js +3 -3
  243. package/dist/core/sdk.js.map +1 -1
  244. package/dist/core/session-role.d.ts +31 -0
  245. package/dist/core/session-role.d.ts.map +1 -0
  246. package/dist/core/session-role.js +52 -0
  247. package/dist/core/session-role.js.map +1 -0
  248. package/dist/core/settings-manager.d.ts +42 -3
  249. package/dist/core/settings-manager.d.ts.map +1 -1
  250. package/dist/core/settings-manager.js +94 -60
  251. package/dist/core/settings-manager.js.map +1 -1
  252. package/dist/core/system-prompt-builder.d.ts +12 -0
  253. package/dist/core/system-prompt-builder.d.ts.map +1 -1
  254. package/dist/core/system-prompt-builder.js +6 -0
  255. package/dist/core/system-prompt-builder.js.map +1 -1
  256. package/dist/core/system-prompt.d.ts +14 -1
  257. package/dist/core/system-prompt.d.ts.map +1 -1
  258. package/dist/core/system-prompt.js +20 -3
  259. package/dist/core/system-prompt.js.map +1 -1
  260. package/dist/core/tasks/session-task-state.d.ts +11 -2
  261. package/dist/core/tasks/session-task-state.d.ts.map +1 -1
  262. package/dist/core/tasks/session-task-state.js +27 -11
  263. package/dist/core/tasks/session-task-state.js.map +1 -1
  264. package/dist/core/tasks/task-contract-monitor.d.ts +45 -0
  265. package/dist/core/tasks/task-contract-monitor.d.ts.map +1 -0
  266. package/dist/core/tasks/task-contract-monitor.js +56 -0
  267. package/dist/core/tasks/task-contract-monitor.js.map +1 -0
  268. package/dist/core/tasks/task-state.d.ts +8 -0
  269. package/dist/core/tasks/task-state.d.ts.map +1 -1
  270. package/dist/core/tasks/task-state.js +28 -1
  271. package/dist/core/tasks/task-state.js.map +1 -1
  272. package/dist/core/tool-gate-controller.d.ts.map +1 -1
  273. package/dist/core/tool-gate-controller.js +5 -0
  274. package/dist/core/tool-gate-controller.js.map +1 -1
  275. package/dist/core/tool-recovery-log-records.d.ts +7 -0
  276. package/dist/core/tool-recovery-log-records.d.ts.map +1 -1
  277. package/dist/core/tool-recovery-log-records.js +14 -6
  278. package/dist/core/tool-recovery-log-records.js.map +1 -1
  279. package/dist/core/tool-selection/promotion.d.ts +54 -0
  280. package/dist/core/tool-selection/promotion.d.ts.map +1 -0
  281. package/dist/core/tool-selection/promotion.js +81 -0
  282. package/dist/core/tool-selection/promotion.js.map +1 -0
  283. package/dist/core/tool-selection/tool-performance-store.d.ts +37 -0
  284. package/dist/core/tool-selection/tool-performance-store.d.ts.map +1 -1
  285. package/dist/core/tool-selection/tool-performance-store.js +87 -3
  286. package/dist/core/tool-selection/tool-performance-store.js.map +1 -1
  287. package/dist/core/tool-selection/tool-selection-controller.d.ts +45 -0
  288. package/dist/core/tool-selection/tool-selection-controller.d.ts.map +1 -1
  289. package/dist/core/tool-selection/tool-selection-controller.js +96 -0
  290. package/dist/core/tool-selection/tool-selection-controller.js.map +1 -1
  291. package/dist/core/tools/bash.d.ts +18 -0
  292. package/dist/core/tools/bash.d.ts.map +1 -1
  293. package/dist/core/tools/bash.js +95 -16
  294. package/dist/core/tools/bash.js.map +1 -1
  295. package/dist/core/tools/delegate-status.d.ts +14 -0
  296. package/dist/core/tools/delegate-status.d.ts.map +1 -1
  297. package/dist/core/tools/delegate-status.js +82 -7
  298. package/dist/core/tools/delegate-status.js.map +1 -1
  299. package/dist/core/tools/delegate.d.ts.map +1 -1
  300. package/dist/core/tools/delegate.js +25 -8
  301. package/dist/core/tools/delegate.js.map +1 -1
  302. package/dist/core/tools/goal.d.ts +94 -3
  303. package/dist/core/tools/goal.d.ts.map +1 -1
  304. package/dist/core/tools/goal.js +165 -15
  305. package/dist/core/tools/goal.js.map +1 -1
  306. package/dist/core/tools/model-fitness.d.ts +7 -0
  307. package/dist/core/tools/model-fitness.d.ts.map +1 -1
  308. package/dist/core/tools/model-fitness.js +2 -2
  309. package/dist/core/tools/model-fitness.js.map +1 -1
  310. package/dist/core/tools/shell-contract-router.d.ts +6 -1
  311. package/dist/core/tools/shell-contract-router.d.ts.map +1 -1
  312. package/dist/core/tools/shell-contract-router.js +23 -2
  313. package/dist/core/tools/shell-contract-router.js.map +1 -1
  314. package/dist/core/tools/shell-session.d.ts +89 -0
  315. package/dist/core/tools/shell-session.d.ts.map +1 -0
  316. package/dist/core/tools/shell-session.js +432 -0
  317. package/dist/core/tools/shell-session.js.map +1 -0
  318. package/dist/core/tools/task-steps.d.ts +4 -0
  319. package/dist/core/tools/task-steps.d.ts.map +1 -1
  320. package/dist/core/tools/task-steps.js +63 -8
  321. package/dist/core/tools/task-steps.js.map +1 -1
  322. package/dist/core/tools/tmux-dispatch.d.ts +86 -0
  323. package/dist/core/tools/tmux-dispatch.d.ts.map +1 -0
  324. package/dist/core/tools/tmux-dispatch.js +91 -0
  325. package/dist/core/tools/tmux-dispatch.js.map +1 -0
  326. package/dist/core/tools/windows-shell-engine.d.ts +42 -0
  327. package/dist/core/tools/windows-shell-engine.d.ts.map +1 -0
  328. package/dist/core/tools/windows-shell-engine.js +153 -0
  329. package/dist/core/tools/windows-shell-engine.js.map +1 -0
  330. package/dist/core/tools/windows-shell-state.d.ts +40 -0
  331. package/dist/core/tools/windows-shell-state.d.ts.map +1 -0
  332. package/dist/core/tools/windows-shell-state.js +59 -0
  333. package/dist/core/tools/windows-shell-state.js.map +1 -0
  334. package/dist/core/tools/worktree-sync.d.ts +24 -0
  335. package/dist/core/tools/worktree-sync.d.ts.map +1 -0
  336. package/dist/core/tools/worktree-sync.js +338 -0
  337. package/dist/core/tools/worktree-sync.js.map +1 -0
  338. package/dist/core/trust-manager.d.ts +4 -1
  339. package/dist/core/trust-manager.d.ts.map +1 -1
  340. package/dist/core/trust-manager.js +20 -2
  341. package/dist/core/trust-manager.js.map +1 -1
  342. package/dist/core/util/atomic-file.d.ts +55 -0
  343. package/dist/core/util/atomic-file.d.ts.map +1 -0
  344. package/dist/core/util/atomic-file.js +255 -0
  345. package/dist/core/util/atomic-file.js.map +1 -0
  346. package/dist/core/util/minimatch-cache.d.ts +33 -0
  347. package/dist/core/util/minimatch-cache.d.ts.map +1 -0
  348. package/dist/core/util/minimatch-cache.js +0 -0
  349. package/dist/core/util/minimatch-cache.js.map +1 -0
  350. package/dist/core/worktree-sync/codes.d.ts +227 -0
  351. package/dist/core/worktree-sync/codes.d.ts.map +1 -0
  352. package/dist/core/worktree-sync/codes.js +14 -0
  353. package/dist/core/worktree-sync/codes.js.map +1 -0
  354. package/dist/core/worktree-sync/git-engine.d.ts +156 -0
  355. package/dist/core/worktree-sync/git-engine.d.ts.map +1 -0
  356. package/dist/core/worktree-sync/git-engine.js +1191 -0
  357. package/dist/core/worktree-sync/git-engine.js.map +1 -0
  358. package/dist/core/worktree-sync/lane-gate.d.ts +75 -0
  359. package/dist/core/worktree-sync/lane-gate.d.ts.map +1 -0
  360. package/dist/core/worktree-sync/lane-gate.js +360 -0
  361. package/dist/core/worktree-sync/lane-gate.js.map +1 -0
  362. package/dist/core/worktree-sync/runtime.d.ts +47 -0
  363. package/dist/core/worktree-sync/runtime.d.ts.map +1 -0
  364. package/dist/core/worktree-sync/runtime.js +96 -0
  365. package/dist/core/worktree-sync/runtime.js.map +1 -0
  366. package/dist/core/worktree-sync/store.d.ts +69 -0
  367. package/dist/core/worktree-sync/store.d.ts.map +1 -0
  368. package/dist/core/worktree-sync/store.js +247 -0
  369. package/dist/core/worktree-sync/store.js.map +1 -0
  370. package/dist/core/worktree-sync/watcher.d.ts +29 -0
  371. package/dist/core/worktree-sync/watcher.d.ts.map +1 -0
  372. package/dist/core/worktree-sync/watcher.js +93 -0
  373. package/dist/core/worktree-sync/watcher.js.map +1 -0
  374. package/dist/main.d.ts.map +1 -1
  375. package/dist/main.js +94 -0
  376. package/dist/main.js.map +1 -1
  377. package/dist/migrations.d.ts +9 -0
  378. package/dist/migrations.d.ts.map +1 -1
  379. package/dist/migrations.js +38 -0
  380. package/dist/migrations.js.map +1 -1
  381. package/dist/modes/interactive/auto-learn-controller.d.ts +16 -1
  382. package/dist/modes/interactive/auto-learn-controller.d.ts.map +1 -1
  383. package/dist/modes/interactive/auto-learn-controller.js +50 -8
  384. package/dist/modes/interactive/auto-learn-controller.js.map +1 -1
  385. package/dist/modes/interactive/components/profile-resource-editor.d.ts.map +1 -1
  386. package/dist/modes/interactive/components/profile-resource-editor.js +23 -1
  387. package/dist/modes/interactive/components/profile-resource-editor.js.map +1 -1
  388. package/dist/modes/interactive/interactive-mode.d.ts +1 -0
  389. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  390. package/dist/modes/interactive/interactive-mode.js +4 -0
  391. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  392. package/dist/modes/interactive/local-model-commands.d.ts +43 -0
  393. package/dist/modes/interactive/local-model-commands.d.ts.map +1 -1
  394. package/dist/modes/interactive/local-model-commands.js +290 -3
  395. package/dist/modes/interactive/local-model-commands.js.map +1 -1
  396. package/dist/modes/interactive/session-flow-commands.d.ts.map +1 -1
  397. package/dist/modes/interactive/session-flow-commands.js +24 -4
  398. package/dist/modes/interactive/session-flow-commands.js.map +1 -1
  399. package/dist/utils/fs-watch.d.ts +11 -0
  400. package/dist/utils/fs-watch.d.ts.map +1 -1
  401. package/dist/utils/fs-watch.js +20 -2
  402. package/dist/utils/fs-watch.js.map +1 -1
  403. package/dist/utils/highlight-js-languages.d.ts +4 -0
  404. package/dist/utils/highlight-js-languages.d.ts.map +1 -0
  405. package/dist/utils/highlight-js-languages.js +573 -0
  406. package/dist/utils/highlight-js-languages.js.map +1 -0
  407. package/dist/utils/shell.d.ts +7 -1
  408. package/dist/utils/shell.d.ts.map +1 -1
  409. package/dist/utils/shell.js +21 -6
  410. package/dist/utils/shell.js.map +1 -1
  411. package/dist/utils/syntax-highlight.d.ts.map +1 -1
  412. package/dist/utils/syntax-highlight.js +53 -5
  413. package/dist/utils/syntax-highlight.js.map +1 -1
  414. package/dist/utils/tools-manager.d.ts.map +1 -1
  415. package/dist/utils/tools-manager.js +112 -1
  416. package/dist/utils/tools-manager.js.map +1 -1
  417. package/docs/process-matrix.md +120 -0
  418. package/docs/settings.md +3 -0
  419. package/docs/tmux-agent-manager.md +85 -2
  420. package/docs/windows.md +52 -3
  421. package/docs/work-directory.md +29 -0
  422. package/docs/worktree-sync.md +250 -0
  423. package/examples/extensions/custom-provider-anthropic/package-lock.json +2 -2
  424. package/examples/extensions/custom-provider-anthropic/package.json +1 -1
  425. package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
  426. package/examples/extensions/sandbox/package-lock.json +2 -2
  427. package/examples/extensions/sandbox/package.json +1 -1
  428. package/examples/extensions/with-deps/package-lock.json +2 -2
  429. package/examples/extensions/with-deps/package.json +1 -1
  430. package/npm-shrinkwrap.json +12 -12
  431. package/package.json +10 -4
  432. package/docs/integration-sweep-builder-blueprint-2026-07-09.md +0 -365
  433. package/docs/integration-sweep-resume-2026-07-09.md +0 -407
@@ -1 +1 @@
1
- {"version":3,"file":"tool-recovery-log-records.d.ts","sourceRoot":"","sources":["../../src/core/tool-recovery-log-records.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,oCAAoC,EAAE,MAAM,mBAAmB,CAAC;AAQ9E,eAAO,MAAM,4BAA4B,+BAA+B,CAAC;AACzE,eAAO,MAAM,iCAAiC,6BAA6B,CAAC;AAQ5E,MAAM,WAAW,+BAAgC,SAAQ,oCAAoC;IAC5F,IAAI,EAAE,OAAO,iCAAiC,CAAC;IAC/C,OAAO,EAAE,CAAC,CAAC;IACX,QAAQ,EAAE,MAAM,CAAC;IACjB,EAAE,EAAE,MAAM,CAAC;IACX,SAAS,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,2BAA2B;IAC3C,YAAY,EAAE,MAAM,CAAC;IACrB,iBAAiB,EAAE,MAAM,CAAC;IAC1B,MAAM,EAAE,+BAA+B,CAAC;CACxC;AAyBD,wBAAgB,qCAAqC,CAAC,IAAI,EAAE;IAC3D,KAAK,EAAE,oCAAoC,CAAC;IAC5C,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,EAAE,EAAE,MAAM,CAAC;CACX,GAAG,+BAA+B,CAmBlC;AAED,wBAAgB,iCAAiC,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,+BAA+B,CAc1G;AAsBD,wBAAgB,0BAA0B,CAAC,KAAK,EAAE,2BAA2B,GAAG,IAAI,CAsBnF","sourcesContent":["import { appendFileSync, existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from \"node:fs\";\nimport { dirname } from \"node:path\";\nimport type { ToolArgumentValidationTelemetryEvent } from \"@caupulican/pi-ai\";\nimport {\n\tcreateToolValidationFailureCorpusRecord,\n\ttype ToolValidationFailureShapeEntry,\n\twriteFailureCorpusRecord,\n} from \"./failure-corpus.ts\";\nimport { updatePersistedToolRecoveryStats } from \"./tool-recovery-stats.ts\";\n\nexport const TOOL_RECOVERY_EVENT_LOG_FILE = \"tool-recovery-events.jsonl\";\nexport const TOOL_ARGUMENT_VALIDATION_LOG_KIND = \"tool_argument_validation\";\n\nconst MAX_EVENT_LOG_BYTES = 4 * 1024 * 1024;\nconst ROTATED_EVENT_LOG_TARGET_BYTES = Math.floor(MAX_EVENT_LOG_BYTES * 0.75);\nconst MAX_ROTATED_EVENT_LOG_RECORDS = 5_000;\nconst MAX_TELEMETRY_LIST_ITEMS = 50;\nconst MAX_TELEMETRY_TEXT_CHARS = 256;\n\nexport interface ToolArgumentValidationLogRecord extends ToolArgumentValidationTelemetryEvent {\n\tkind: typeof TOOL_ARGUMENT_VALIDATION_LOG_KIND;\n\tversion: 1;\n\trecordId: string;\n\tts: string;\n\tsessionId: string;\n}\n\nexport interface ToolRecoveryLogWorkerRecord {\n\teventLogPath: string;\n\tfailureCorpusPath: string;\n\trecord: ToolArgumentValidationLogRecord;\n}\n\nfunction boundedText(value: string): string {\n\treturn value.slice(0, MAX_TELEMETRY_TEXT_CHARS);\n}\n\nfunction boundedList<T extends string>(values: readonly T[] | undefined): T[] | undefined {\n\treturn values?.slice(0, MAX_TELEMETRY_LIST_ITEMS);\n}\n\nfunction boundedTextList(values: readonly string[] | undefined): string[] | undefined {\n\treturn values?.slice(0, MAX_TELEMETRY_LIST_ITEMS).map(boundedText);\n}\n\nfunction copyFailureShape(\n\tshape: readonly ToolValidationFailureShapeEntry[] | undefined,\n): ToolValidationFailureShapeEntry[] {\n\treturn (shape ?? []).slice(0, MAX_TELEMETRY_LIST_ITEMS).map((entry) => ({\n\t\tpath: boundedText(entry.path),\n\t\texpectedType: boundedText(entry.expectedType),\n\t\treceivedType: boundedText(entry.receivedType),\n\t\t...(entry.keyword ? { keyword: boundedText(entry.keyword) } : {}),\n\t}));\n}\n\nexport function createToolArgumentValidationLogRecord(args: {\n\tevent: ToolArgumentValidationTelemetryEvent;\n\trecordId: string;\n\tsessionId: string;\n\tts: string;\n}): ToolArgumentValidationLogRecord {\n\treturn {\n\t\tkind: TOOL_ARGUMENT_VALIDATION_LOG_KIND,\n\t\tversion: 1,\n\t\trecordId: args.recordId,\n\t\tts: args.ts,\n\t\tsessionId: args.sessionId,\n\t\toutcome: args.event.outcome,\n\t\tprovider: args.event.provider,\n\t\tmodel: args.event.model,\n\t\ttool: args.event.tool,\n\t\tsource: args.event.source,\n\t\tfailureModes: boundedList(args.event.failureModes) ?? [],\n\t\trepairsApplied: boundedList(args.event.repairsApplied) ?? [],\n\t\tfailureShape: copyFailureShape(args.event.failureShape),\n\t\terrorKeywords: boundedTextList(args.event.errorKeywords),\n\t\ttaught: args.event.taught,\n\t\texecutionOutcome: args.event.executionOutcome,\n\t};\n}\n\nexport function isToolArgumentValidationLogRecord(value: unknown): value is ToolArgumentValidationLogRecord {\n\tif (!value || typeof value !== \"object\") return false;\n\tconst record = value as Partial<ToolArgumentValidationLogRecord>;\n\treturn (\n\t\trecord.kind === TOOL_ARGUMENT_VALIDATION_LOG_KIND &&\n\t\trecord.version === 1 &&\n\t\ttypeof record.recordId === \"string\" &&\n\t\ttypeof record.ts === \"string\" &&\n\t\ttypeof record.sessionId === \"string\" &&\n\t\t(record.outcome === \"repaired\" || record.outcome === \"bounced\") &&\n\t\ttypeof record.tool === \"string\" &&\n\t\tArray.isArray(record.failureModes) &&\n\t\tArray.isArray(record.repairsApplied)\n\t);\n}\n\nfunction rotateToolRecoveryEventLogIfNeeded(filePath: string): void {\n\tif (!existsSync(filePath) || statSync(filePath).size <= MAX_EVENT_LOG_BYTES) return;\n\tconst lines = readFileSync(filePath, \"utf-8\")\n\t\t.split(\"\\n\")\n\t\t.filter((line) => line.trim().length > 0);\n\tconst retained: string[] = [];\n\tlet retainedBytes = 0;\n\tfor (let index = lines.length - 1; index >= 0 && retained.length < MAX_ROTATED_EVENT_LOG_RECORDS; index--) {\n\t\tconst line = lines[index];\n\t\tconst lineBytes = Buffer.byteLength(line, \"utf-8\") + 1;\n\t\tif (lineBytes > MAX_EVENT_LOG_BYTES) continue;\n\t\tif (retained.length > 0 && retainedBytes + lineBytes > ROTATED_EVENT_LOG_TARGET_BYTES) break;\n\t\tretained.push(line);\n\t\tretainedBytes += lineBytes;\n\t\tif (retainedBytes >= ROTATED_EVENT_LOG_TARGET_BYTES) break;\n\t}\n\tretained.reverse();\n\twriteFileSync(filePath, retained.length > 0 ? `${retained.join(\"\\n\")}\\n` : \"\", \"utf-8\");\n}\n\nexport function writeToolRecoveryLogRecord(entry: ToolRecoveryLogWorkerRecord): void {\n\tmkdirSync(dirname(entry.eventLogPath), { recursive: true });\n\tappendFileSync(entry.eventLogPath, `${JSON.stringify(entry.record)}\\n`, \"utf-8\");\n\trotateToolRecoveryEventLogIfNeeded(entry.eventLogPath);\n\ttry {\n\t\tupdatePersistedToolRecoveryStats(entry.eventLogPath, entry.record);\n\t} catch {\n\t\t// The bounded event log remains the recovery source if the cumulative summary cannot be updated.\n\t}\n\tif (entry.record.outcome !== \"bounced\") return;\n\twriteFailureCorpusRecord(\n\t\tentry.failureCorpusPath,\n\t\tcreateToolValidationFailureCorpusRecord({\n\t\t\tts: entry.record.ts,\n\t\t\tprovider: entry.record.provider,\n\t\t\tmodelId: entry.record.model,\n\t\t\ttool: entry.record.tool,\n\t\t\tfailureModes: entry.record.failureModes,\n\t\t\tshape: copyFailureShape(entry.record.failureShape),\n\t\t\terrorKeywords: entry.record.errorKeywords,\n\t\t}),\n\t);\n}\n"]}
1
+ {"version":3,"file":"tool-recovery-log-records.d.ts","sourceRoot":"","sources":["../../src/core/tool-recovery-log-records.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,oCAAoC,EAAE,MAAM,mBAAmB,CAAC;AAS9E,eAAO,MAAM,4BAA4B,+BAA+B,CAAC;AACzE,eAAO,MAAM,iCAAiC,6BAA6B,CAAC;AAQ5E,MAAM,WAAW,+BAAgC,SAAQ,oCAAoC;IAC5F,IAAI,EAAE,OAAO,iCAAiC,CAAC;IAC/C,OAAO,EAAE,CAAC,CAAC;IACX,QAAQ,EAAE,MAAM,CAAC;IACjB,EAAE,EAAE,MAAM,CAAC;IACX,SAAS,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,2BAA2B;IAC3C,YAAY,EAAE,MAAM,CAAC;IACrB,iBAAiB,EAAE,MAAM,CAAC;IAC1B,MAAM,EAAE,+BAA+B,CAAC;CACxC;AAyBD,wBAAgB,qCAAqC,CAAC,IAAI,EAAE;IAC3D,KAAK,EAAE,oCAAoC,CAAC;IAC5C,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,EAAE,EAAE,MAAM,CAAC;CACX,GAAG,+BAA+B,CAmBlC;AAED,wBAAgB,iCAAiC,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,+BAA+B,CAc1G;AAsBD;;;;;;GAMG;AACH,wBAAgB,0BAA0B,CAAC,KAAK,EAAE,2BAA2B,GAAG,IAAI,CAuBnF","sourcesContent":["import { appendFileSync, existsSync, readFileSync, statSync } from \"node:fs\";\nimport type { ToolArgumentValidationTelemetryEvent } from \"@caupulican/pi-ai\";\nimport {\n\tcreateToolValidationFailureCorpusRecord,\n\ttype ToolValidationFailureShapeEntry,\n\twriteFailureCorpusRecord,\n} from \"./failure-corpus.ts\";\nimport { updatePersistedToolRecoveryStats } from \"./tool-recovery-stats.ts\";\nimport { withFileLockSync, writeFileAtomicSync } from \"./util/atomic-file.ts\";\n\nexport const TOOL_RECOVERY_EVENT_LOG_FILE = \"tool-recovery-events.jsonl\";\nexport const TOOL_ARGUMENT_VALIDATION_LOG_KIND = \"tool_argument_validation\";\n\nconst MAX_EVENT_LOG_BYTES = 4 * 1024 * 1024;\nconst ROTATED_EVENT_LOG_TARGET_BYTES = Math.floor(MAX_EVENT_LOG_BYTES * 0.75);\nconst MAX_ROTATED_EVENT_LOG_RECORDS = 5_000;\nconst MAX_TELEMETRY_LIST_ITEMS = 50;\nconst MAX_TELEMETRY_TEXT_CHARS = 256;\n\nexport interface ToolArgumentValidationLogRecord extends ToolArgumentValidationTelemetryEvent {\n\tkind: typeof TOOL_ARGUMENT_VALIDATION_LOG_KIND;\n\tversion: 1;\n\trecordId: string;\n\tts: string;\n\tsessionId: string;\n}\n\nexport interface ToolRecoveryLogWorkerRecord {\n\teventLogPath: string;\n\tfailureCorpusPath: string;\n\trecord: ToolArgumentValidationLogRecord;\n}\n\nfunction boundedText(value: string): string {\n\treturn value.slice(0, MAX_TELEMETRY_TEXT_CHARS);\n}\n\nfunction boundedList<T extends string>(values: readonly T[] | undefined): T[] | undefined {\n\treturn values?.slice(0, MAX_TELEMETRY_LIST_ITEMS);\n}\n\nfunction boundedTextList(values: readonly string[] | undefined): string[] | undefined {\n\treturn values?.slice(0, MAX_TELEMETRY_LIST_ITEMS).map(boundedText);\n}\n\nfunction copyFailureShape(\n\tshape: readonly ToolValidationFailureShapeEntry[] | undefined,\n): ToolValidationFailureShapeEntry[] {\n\treturn (shape ?? []).slice(0, MAX_TELEMETRY_LIST_ITEMS).map((entry) => ({\n\t\tpath: boundedText(entry.path),\n\t\texpectedType: boundedText(entry.expectedType),\n\t\treceivedType: boundedText(entry.receivedType),\n\t\t...(entry.keyword ? { keyword: boundedText(entry.keyword) } : {}),\n\t}));\n}\n\nexport function createToolArgumentValidationLogRecord(args: {\n\tevent: ToolArgumentValidationTelemetryEvent;\n\trecordId: string;\n\tsessionId: string;\n\tts: string;\n}): ToolArgumentValidationLogRecord {\n\treturn {\n\t\tkind: TOOL_ARGUMENT_VALIDATION_LOG_KIND,\n\t\tversion: 1,\n\t\trecordId: args.recordId,\n\t\tts: args.ts,\n\t\tsessionId: args.sessionId,\n\t\toutcome: args.event.outcome,\n\t\tprovider: args.event.provider,\n\t\tmodel: args.event.model,\n\t\ttool: args.event.tool,\n\t\tsource: args.event.source,\n\t\tfailureModes: boundedList(args.event.failureModes) ?? [],\n\t\trepairsApplied: boundedList(args.event.repairsApplied) ?? [],\n\t\tfailureShape: copyFailureShape(args.event.failureShape),\n\t\terrorKeywords: boundedTextList(args.event.errorKeywords),\n\t\ttaught: args.event.taught,\n\t\texecutionOutcome: args.event.executionOutcome,\n\t};\n}\n\nexport function isToolArgumentValidationLogRecord(value: unknown): value is ToolArgumentValidationLogRecord {\n\tif (!value || typeof value !== \"object\") return false;\n\tconst record = value as Partial<ToolArgumentValidationLogRecord>;\n\treturn (\n\t\trecord.kind === TOOL_ARGUMENT_VALIDATION_LOG_KIND &&\n\t\trecord.version === 1 &&\n\t\ttypeof record.recordId === \"string\" &&\n\t\ttypeof record.ts === \"string\" &&\n\t\ttypeof record.sessionId === \"string\" &&\n\t\t(record.outcome === \"repaired\" || record.outcome === \"bounced\") &&\n\t\ttypeof record.tool === \"string\" &&\n\t\tArray.isArray(record.failureModes) &&\n\t\tArray.isArray(record.repairsApplied)\n\t);\n}\n\nfunction rotateToolRecoveryEventLogIfNeeded(filePath: string): void {\n\tif (!existsSync(filePath) || statSync(filePath).size <= MAX_EVENT_LOG_BYTES) return;\n\tconst lines = readFileSync(filePath, \"utf-8\")\n\t\t.split(\"\\n\")\n\t\t.filter((line) => line.trim().length > 0);\n\tconst retained: string[] = [];\n\tlet retainedBytes = 0;\n\tfor (let index = lines.length - 1; index >= 0 && retained.length < MAX_ROTATED_EVENT_LOG_RECORDS; index--) {\n\t\tconst line = lines[index];\n\t\tconst lineBytes = Buffer.byteLength(line, \"utf-8\") + 1;\n\t\tif (lineBytes > MAX_EVENT_LOG_BYTES) continue;\n\t\tif (retained.length > 0 && retainedBytes + lineBytes > ROTATED_EVENT_LOG_TARGET_BYTES) break;\n\t\tretained.push(line);\n\t\tretainedBytes += lineBytes;\n\t\tif (retainedBytes >= ROTATED_EVENT_LOG_TARGET_BYTES) break;\n\t}\n\tretained.reverse();\n\twriteFileAtomicSync(filePath, retained.length > 0 ? `${retained.join(\"\\n\")}\\n` : \"\");\n}\n\n/**\n * Append + rotate under a single exclusive lock on `eventLogPath`. Without the lock, a concurrent\n * writer (e.g. another session's tool-recovery worker) could append between this call's read and its\n * rotated rewrite, and that append would be silently discarded when the rewrite lands; the atomic\n * tmp+rename in {@link rotateToolRecoveryEventLogIfNeeded} additionally ensures a reader never observes\n * a partially-rewritten (torn) log file.\n */\nexport function writeToolRecoveryLogRecord(entry: ToolRecoveryLogWorkerRecord): void {\n\twithFileLockSync(entry.eventLogPath, () => {\n\t\tappendFileSync(entry.eventLogPath, `${JSON.stringify(entry.record)}\\n`, \"utf-8\");\n\t\trotateToolRecoveryEventLogIfNeeded(entry.eventLogPath);\n\t});\n\ttry {\n\t\tupdatePersistedToolRecoveryStats(entry.eventLogPath, entry.record);\n\t} catch {\n\t\t// The bounded event log remains the recovery source if the cumulative summary cannot be updated.\n\t}\n\tif (entry.record.outcome !== \"bounced\") return;\n\twriteFailureCorpusRecord(\n\t\tentry.failureCorpusPath,\n\t\tcreateToolValidationFailureCorpusRecord({\n\t\t\tts: entry.record.ts,\n\t\t\tprovider: entry.record.provider,\n\t\t\tmodelId: entry.record.model,\n\t\t\ttool: entry.record.tool,\n\t\t\tfailureModes: entry.record.failureModes,\n\t\t\tshape: copyFailureShape(entry.record.failureShape),\n\t\t\terrorKeywords: entry.record.errorKeywords,\n\t\t}),\n\t);\n}\n"]}
@@ -1,7 +1,7 @@
1
- import { appendFileSync, existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
2
- import { dirname } from "node:path";
1
+ import { appendFileSync, existsSync, readFileSync, statSync } from "node:fs";
3
2
  import { createToolValidationFailureCorpusRecord, writeFailureCorpusRecord, } from "./failure-corpus.js";
4
3
  import { updatePersistedToolRecoveryStats } from "./tool-recovery-stats.js";
4
+ import { withFileLockSync, writeFileAtomicSync } from "./util/atomic-file.js";
5
5
  export const TOOL_RECOVERY_EVENT_LOG_FILE = "tool-recovery-events.jsonl";
6
6
  export const TOOL_ARGUMENT_VALIDATION_LOG_KIND = "tool_argument_validation";
7
7
  const MAX_EVENT_LOG_BYTES = 4 * 1024 * 1024;
@@ -81,12 +81,20 @@ function rotateToolRecoveryEventLogIfNeeded(filePath) {
81
81
  break;
82
82
  }
83
83
  retained.reverse();
84
- writeFileSync(filePath, retained.length > 0 ? `${retained.join("\n")}\n` : "", "utf-8");
84
+ writeFileAtomicSync(filePath, retained.length > 0 ? `${retained.join("\n")}\n` : "");
85
85
  }
86
+ /**
87
+ * Append + rotate under a single exclusive lock on `eventLogPath`. Without the lock, a concurrent
88
+ * writer (e.g. another session's tool-recovery worker) could append between this call's read and its
89
+ * rotated rewrite, and that append would be silently discarded when the rewrite lands; the atomic
90
+ * tmp+rename in {@link rotateToolRecoveryEventLogIfNeeded} additionally ensures a reader never observes
91
+ * a partially-rewritten (torn) log file.
92
+ */
86
93
  export function writeToolRecoveryLogRecord(entry) {
87
- mkdirSync(dirname(entry.eventLogPath), { recursive: true });
88
- appendFileSync(entry.eventLogPath, `${JSON.stringify(entry.record)}\n`, "utf-8");
89
- rotateToolRecoveryEventLogIfNeeded(entry.eventLogPath);
94
+ withFileLockSync(entry.eventLogPath, () => {
95
+ appendFileSync(entry.eventLogPath, `${JSON.stringify(entry.record)}\n`, "utf-8");
96
+ rotateToolRecoveryEventLogIfNeeded(entry.eventLogPath);
97
+ });
90
98
  try {
91
99
  updatePersistedToolRecoveryStats(entry.eventLogPath, entry.record);
92
100
  }
@@ -1 +1 @@
1
- {"version":3,"file":"tool-recovery-log-records.js","sourceRoot":"","sources":["../../src/core/tool-recovery-log-records.ts"],"names":[],"mappings":"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","sourcesContent":["import { appendFileSync, existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from \"node:fs\";\nimport { dirname } from \"node:path\";\nimport type { ToolArgumentValidationTelemetryEvent } from \"@caupulican/pi-ai\";\nimport {\n\tcreateToolValidationFailureCorpusRecord,\n\ttype ToolValidationFailureShapeEntry,\n\twriteFailureCorpusRecord,\n} from \"./failure-corpus.ts\";\nimport { updatePersistedToolRecoveryStats } from \"./tool-recovery-stats.ts\";\n\nexport const TOOL_RECOVERY_EVENT_LOG_FILE = \"tool-recovery-events.jsonl\";\nexport const TOOL_ARGUMENT_VALIDATION_LOG_KIND = \"tool_argument_validation\";\n\nconst MAX_EVENT_LOG_BYTES = 4 * 1024 * 1024;\nconst ROTATED_EVENT_LOG_TARGET_BYTES = Math.floor(MAX_EVENT_LOG_BYTES * 0.75);\nconst MAX_ROTATED_EVENT_LOG_RECORDS = 5_000;\nconst MAX_TELEMETRY_LIST_ITEMS = 50;\nconst MAX_TELEMETRY_TEXT_CHARS = 256;\n\nexport interface ToolArgumentValidationLogRecord extends ToolArgumentValidationTelemetryEvent {\n\tkind: typeof TOOL_ARGUMENT_VALIDATION_LOG_KIND;\n\tversion: 1;\n\trecordId: string;\n\tts: string;\n\tsessionId: string;\n}\n\nexport interface ToolRecoveryLogWorkerRecord {\n\teventLogPath: string;\n\tfailureCorpusPath: string;\n\trecord: ToolArgumentValidationLogRecord;\n}\n\nfunction boundedText(value: string): string {\n\treturn value.slice(0, MAX_TELEMETRY_TEXT_CHARS);\n}\n\nfunction boundedList<T extends string>(values: readonly T[] | undefined): T[] | undefined {\n\treturn values?.slice(0, MAX_TELEMETRY_LIST_ITEMS);\n}\n\nfunction boundedTextList(values: readonly string[] | undefined): string[] | undefined {\n\treturn values?.slice(0, MAX_TELEMETRY_LIST_ITEMS).map(boundedText);\n}\n\nfunction copyFailureShape(\n\tshape: readonly ToolValidationFailureShapeEntry[] | undefined,\n): ToolValidationFailureShapeEntry[] {\n\treturn (shape ?? []).slice(0, MAX_TELEMETRY_LIST_ITEMS).map((entry) => ({\n\t\tpath: boundedText(entry.path),\n\t\texpectedType: boundedText(entry.expectedType),\n\t\treceivedType: boundedText(entry.receivedType),\n\t\t...(entry.keyword ? { keyword: boundedText(entry.keyword) } : {}),\n\t}));\n}\n\nexport function createToolArgumentValidationLogRecord(args: {\n\tevent: ToolArgumentValidationTelemetryEvent;\n\trecordId: string;\n\tsessionId: string;\n\tts: string;\n}): ToolArgumentValidationLogRecord {\n\treturn {\n\t\tkind: TOOL_ARGUMENT_VALIDATION_LOG_KIND,\n\t\tversion: 1,\n\t\trecordId: args.recordId,\n\t\tts: args.ts,\n\t\tsessionId: args.sessionId,\n\t\toutcome: args.event.outcome,\n\t\tprovider: args.event.provider,\n\t\tmodel: args.event.model,\n\t\ttool: args.event.tool,\n\t\tsource: args.event.source,\n\t\tfailureModes: boundedList(args.event.failureModes) ?? [],\n\t\trepairsApplied: boundedList(args.event.repairsApplied) ?? [],\n\t\tfailureShape: copyFailureShape(args.event.failureShape),\n\t\terrorKeywords: boundedTextList(args.event.errorKeywords),\n\t\ttaught: args.event.taught,\n\t\texecutionOutcome: args.event.executionOutcome,\n\t};\n}\n\nexport function isToolArgumentValidationLogRecord(value: unknown): value is ToolArgumentValidationLogRecord {\n\tif (!value || typeof value !== \"object\") return false;\n\tconst record = value as Partial<ToolArgumentValidationLogRecord>;\n\treturn (\n\t\trecord.kind === TOOL_ARGUMENT_VALIDATION_LOG_KIND &&\n\t\trecord.version === 1 &&\n\t\ttypeof record.recordId === \"string\" &&\n\t\ttypeof record.ts === \"string\" &&\n\t\ttypeof record.sessionId === \"string\" &&\n\t\t(record.outcome === \"repaired\" || record.outcome === \"bounced\") &&\n\t\ttypeof record.tool === \"string\" &&\n\t\tArray.isArray(record.failureModes) &&\n\t\tArray.isArray(record.repairsApplied)\n\t);\n}\n\nfunction rotateToolRecoveryEventLogIfNeeded(filePath: string): void {\n\tif (!existsSync(filePath) || statSync(filePath).size <= MAX_EVENT_LOG_BYTES) return;\n\tconst lines = readFileSync(filePath, \"utf-8\")\n\t\t.split(\"\\n\")\n\t\t.filter((line) => line.trim().length > 0);\n\tconst retained: string[] = [];\n\tlet retainedBytes = 0;\n\tfor (let index = lines.length - 1; index >= 0 && retained.length < MAX_ROTATED_EVENT_LOG_RECORDS; index--) {\n\t\tconst line = lines[index];\n\t\tconst lineBytes = Buffer.byteLength(line, \"utf-8\") + 1;\n\t\tif (lineBytes > MAX_EVENT_LOG_BYTES) continue;\n\t\tif (retained.length > 0 && retainedBytes + lineBytes > ROTATED_EVENT_LOG_TARGET_BYTES) break;\n\t\tretained.push(line);\n\t\tretainedBytes += lineBytes;\n\t\tif (retainedBytes >= ROTATED_EVENT_LOG_TARGET_BYTES) break;\n\t}\n\tretained.reverse();\n\twriteFileSync(filePath, retained.length > 0 ? `${retained.join(\"\\n\")}\\n` : \"\", \"utf-8\");\n}\n\nexport function writeToolRecoveryLogRecord(entry: ToolRecoveryLogWorkerRecord): void {\n\tmkdirSync(dirname(entry.eventLogPath), { recursive: true });\n\tappendFileSync(entry.eventLogPath, `${JSON.stringify(entry.record)}\\n`, \"utf-8\");\n\trotateToolRecoveryEventLogIfNeeded(entry.eventLogPath);\n\ttry {\n\t\tupdatePersistedToolRecoveryStats(entry.eventLogPath, entry.record);\n\t} catch {\n\t\t// The bounded event log remains the recovery source if the cumulative summary cannot be updated.\n\t}\n\tif (entry.record.outcome !== \"bounced\") return;\n\twriteFailureCorpusRecord(\n\t\tentry.failureCorpusPath,\n\t\tcreateToolValidationFailureCorpusRecord({\n\t\t\tts: entry.record.ts,\n\t\t\tprovider: entry.record.provider,\n\t\t\tmodelId: entry.record.model,\n\t\t\ttool: entry.record.tool,\n\t\t\tfailureModes: entry.record.failureModes,\n\t\t\tshape: copyFailureShape(entry.record.failureShape),\n\t\t\terrorKeywords: entry.record.errorKeywords,\n\t\t}),\n\t);\n}\n"]}
1
+ {"version":3,"file":"tool-recovery-log-records.js","sourceRoot":"","sources":["../../src/core/tool-recovery-log-records.ts"],"names":[],"mappings":"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","sourcesContent":["import { appendFileSync, existsSync, readFileSync, statSync } from \"node:fs\";\nimport type { ToolArgumentValidationTelemetryEvent } from \"@caupulican/pi-ai\";\nimport {\n\tcreateToolValidationFailureCorpusRecord,\n\ttype ToolValidationFailureShapeEntry,\n\twriteFailureCorpusRecord,\n} from \"./failure-corpus.ts\";\nimport { updatePersistedToolRecoveryStats } from \"./tool-recovery-stats.ts\";\nimport { withFileLockSync, writeFileAtomicSync } from \"./util/atomic-file.ts\";\n\nexport const TOOL_RECOVERY_EVENT_LOG_FILE = \"tool-recovery-events.jsonl\";\nexport const TOOL_ARGUMENT_VALIDATION_LOG_KIND = \"tool_argument_validation\";\n\nconst MAX_EVENT_LOG_BYTES = 4 * 1024 * 1024;\nconst ROTATED_EVENT_LOG_TARGET_BYTES = Math.floor(MAX_EVENT_LOG_BYTES * 0.75);\nconst MAX_ROTATED_EVENT_LOG_RECORDS = 5_000;\nconst MAX_TELEMETRY_LIST_ITEMS = 50;\nconst MAX_TELEMETRY_TEXT_CHARS = 256;\n\nexport interface ToolArgumentValidationLogRecord extends ToolArgumentValidationTelemetryEvent {\n\tkind: typeof TOOL_ARGUMENT_VALIDATION_LOG_KIND;\n\tversion: 1;\n\trecordId: string;\n\tts: string;\n\tsessionId: string;\n}\n\nexport interface ToolRecoveryLogWorkerRecord {\n\teventLogPath: string;\n\tfailureCorpusPath: string;\n\trecord: ToolArgumentValidationLogRecord;\n}\n\nfunction boundedText(value: string): string {\n\treturn value.slice(0, MAX_TELEMETRY_TEXT_CHARS);\n}\n\nfunction boundedList<T extends string>(values: readonly T[] | undefined): T[] | undefined {\n\treturn values?.slice(0, MAX_TELEMETRY_LIST_ITEMS);\n}\n\nfunction boundedTextList(values: readonly string[] | undefined): string[] | undefined {\n\treturn values?.slice(0, MAX_TELEMETRY_LIST_ITEMS).map(boundedText);\n}\n\nfunction copyFailureShape(\n\tshape: readonly ToolValidationFailureShapeEntry[] | undefined,\n): ToolValidationFailureShapeEntry[] {\n\treturn (shape ?? []).slice(0, MAX_TELEMETRY_LIST_ITEMS).map((entry) => ({\n\t\tpath: boundedText(entry.path),\n\t\texpectedType: boundedText(entry.expectedType),\n\t\treceivedType: boundedText(entry.receivedType),\n\t\t...(entry.keyword ? { keyword: boundedText(entry.keyword) } : {}),\n\t}));\n}\n\nexport function createToolArgumentValidationLogRecord(args: {\n\tevent: ToolArgumentValidationTelemetryEvent;\n\trecordId: string;\n\tsessionId: string;\n\tts: string;\n}): ToolArgumentValidationLogRecord {\n\treturn {\n\t\tkind: TOOL_ARGUMENT_VALIDATION_LOG_KIND,\n\t\tversion: 1,\n\t\trecordId: args.recordId,\n\t\tts: args.ts,\n\t\tsessionId: args.sessionId,\n\t\toutcome: args.event.outcome,\n\t\tprovider: args.event.provider,\n\t\tmodel: args.event.model,\n\t\ttool: args.event.tool,\n\t\tsource: args.event.source,\n\t\tfailureModes: boundedList(args.event.failureModes) ?? [],\n\t\trepairsApplied: boundedList(args.event.repairsApplied) ?? [],\n\t\tfailureShape: copyFailureShape(args.event.failureShape),\n\t\terrorKeywords: boundedTextList(args.event.errorKeywords),\n\t\ttaught: args.event.taught,\n\t\texecutionOutcome: args.event.executionOutcome,\n\t};\n}\n\nexport function isToolArgumentValidationLogRecord(value: unknown): value is ToolArgumentValidationLogRecord {\n\tif (!value || typeof value !== \"object\") return false;\n\tconst record = value as Partial<ToolArgumentValidationLogRecord>;\n\treturn (\n\t\trecord.kind === TOOL_ARGUMENT_VALIDATION_LOG_KIND &&\n\t\trecord.version === 1 &&\n\t\ttypeof record.recordId === \"string\" &&\n\t\ttypeof record.ts === \"string\" &&\n\t\ttypeof record.sessionId === \"string\" &&\n\t\t(record.outcome === \"repaired\" || record.outcome === \"bounced\") &&\n\t\ttypeof record.tool === \"string\" &&\n\t\tArray.isArray(record.failureModes) &&\n\t\tArray.isArray(record.repairsApplied)\n\t);\n}\n\nfunction rotateToolRecoveryEventLogIfNeeded(filePath: string): void {\n\tif (!existsSync(filePath) || statSync(filePath).size <= MAX_EVENT_LOG_BYTES) return;\n\tconst lines = readFileSync(filePath, \"utf-8\")\n\t\t.split(\"\\n\")\n\t\t.filter((line) => line.trim().length > 0);\n\tconst retained: string[] = [];\n\tlet retainedBytes = 0;\n\tfor (let index = lines.length - 1; index >= 0 && retained.length < MAX_ROTATED_EVENT_LOG_RECORDS; index--) {\n\t\tconst line = lines[index];\n\t\tconst lineBytes = Buffer.byteLength(line, \"utf-8\") + 1;\n\t\tif (lineBytes > MAX_EVENT_LOG_BYTES) continue;\n\t\tif (retained.length > 0 && retainedBytes + lineBytes > ROTATED_EVENT_LOG_TARGET_BYTES) break;\n\t\tretained.push(line);\n\t\tretainedBytes += lineBytes;\n\t\tif (retainedBytes >= ROTATED_EVENT_LOG_TARGET_BYTES) break;\n\t}\n\tretained.reverse();\n\twriteFileAtomicSync(filePath, retained.length > 0 ? `${retained.join(\"\\n\")}\\n` : \"\");\n}\n\n/**\n * Append + rotate under a single exclusive lock on `eventLogPath`. Without the lock, a concurrent\n * writer (e.g. another session's tool-recovery worker) could append between this call's read and its\n * rotated rewrite, and that append would be silently discarded when the rewrite lands; the atomic\n * tmp+rename in {@link rotateToolRecoveryEventLogIfNeeded} additionally ensures a reader never observes\n * a partially-rewritten (torn) log file.\n */\nexport function writeToolRecoveryLogRecord(entry: ToolRecoveryLogWorkerRecord): void {\n\twithFileLockSync(entry.eventLogPath, () => {\n\t\tappendFileSync(entry.eventLogPath, `${JSON.stringify(entry.record)}\\n`, \"utf-8\");\n\t\trotateToolRecoveryEventLogIfNeeded(entry.eventLogPath);\n\t});\n\ttry {\n\t\tupdatePersistedToolRecoveryStats(entry.eventLogPath, entry.record);\n\t} catch {\n\t\t// The bounded event log remains the recovery source if the cumulative summary cannot be updated.\n\t}\n\tif (entry.record.outcome !== \"bounced\") return;\n\twriteFailureCorpusRecord(\n\t\tentry.failureCorpusPath,\n\t\tcreateToolValidationFailureCorpusRecord({\n\t\t\tts: entry.record.ts,\n\t\t\tprovider: entry.record.provider,\n\t\t\tmodelId: entry.record.model,\n\t\t\ttool: entry.record.tool,\n\t\t\tfailureModes: entry.record.failureModes,\n\t\t\tshape: copyFailureShape(entry.record.failureShape),\n\t\t\terrorKeywords: entry.record.errorKeywords,\n\t\t}),\n\t);\n}\n"]}
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Evidence-gated tool-selection promotion (closing the observe/promote loop).
3
+ *
4
+ * `tool-selection-controller.ts` records, per tool call, a full expected-utility decision
5
+ * (`ToolSelectionController.begin()`), but that per-call decision is scoped to whichever
6
+ * tool the model already chose — only the chosen tool is ever eligible to be "recommended"
7
+ * (see `candidateFor` in tool-selection-controller.ts), so it can validate a choice but never
8
+ * suggest a different one. This module is the SEPARATE, aggregate read-side: given the
9
+ * per-(model,intent,tool) track record accumulated in `ToolPerformanceStore` over many calls,
10
+ * decide whether one tool has become the clear, evidence-backed default for a (model, intent)
11
+ * pair — and if so, render a compact, cache-stable prompt hint.
12
+ *
13
+ * Reuses the exact same expected-utility gate (`decideExpectedUtility`) as call-time decisions,
14
+ * so promotion and the call-time recommend/shortlist/abstain gate share one threshold
15
+ * vocabulary: positive best-utility, a minimum margin over the runner-up, bounded entropy, and a
16
+ * minimum sample count. A tool is promoted only when the gate returns "recommend"; ambiguous
17
+ * (high-entropy/low-margin) or thin-evidence buckets never surface a hint — they stay silent,
18
+ * which is the "shortlist on high entropy, never a false-confident pick" behavior.
19
+ *
20
+ * Purely functional: no store/env access, so it is trivial to unit test and never mutates state.
21
+ * The stateful pieces (kill switches, reading the store, formatting into the live system prompt)
22
+ * live in `tool-selection-controller.ts` and `system-prompt-builder.ts`.
23
+ */
24
+ import { type ExpectedUtilityWeights } from "./expected-utility.ts";
25
+ import type { ToolPerformanceStats, ToolSelectionIntentClass } from "./tool-performance-store.ts";
26
+ export interface ToolSelectionHint {
27
+ modelRef: string;
28
+ intentClass: ToolSelectionIntentClass;
29
+ tool: string;
30
+ sampleCount: number;
31
+ margin: number;
32
+ entropy: number;
33
+ }
34
+ export interface ToolPromotionDecision {
35
+ /** The promoted tool, present only when the evidence gate returned "recommend". */
36
+ tool?: string;
37
+ sampleCount: number;
38
+ margin: number;
39
+ entropy: number;
40
+ }
41
+ /**
42
+ * Evidence-gated promotion for one (model, intentClass) bucket: which tool, if any, has become
43
+ * the clear default given its accumulated track record. `statsForIntent` should already be
44
+ * scoped to a single (modelRef, intentClass) — see `ToolPerformanceStore.getStatsForIntent`.
45
+ */
46
+ export declare function evaluateToolPromotion(statsForIntent: readonly ToolPerformanceStats[], weights?: ExpectedUtilityWeights): ToolPromotionDecision;
47
+ /**
48
+ * Renders the active hints into a small, stable prompt block. Deliberately omits live numbers
49
+ * (sample counts, rates) that would change every time evidence accumulates — only the SET of
50
+ * (intent -> tool) pairs is included, so the block's text changes only when the promoted tool for
51
+ * an intent actually flips, not on every tool call (system-prompt cache stability).
52
+ */
53
+ export declare function formatToolSelectionHints(hints: readonly ToolSelectionHint[]): string | undefined;
54
+ //# sourceMappingURL=promotion.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"promotion.d.ts","sourceRoot":"","sources":["../../../src/core/tool-selection/promotion.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,OAAO,EAIN,KAAK,sBAAsB,EAC3B,MAAM,uBAAuB,CAAC;AAC/B,OAAO,KAAK,EAAE,oBAAoB,EAAE,wBAAwB,EAAE,MAAM,6BAA6B,CAAC;AAElG,MAAM,WAAW,iBAAiB;IACjC,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,EAAE,wBAAwB,CAAC;IACtC,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,qBAAqB;IACrC,mFAAmF;IACnF,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;IACpB,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;CAChB;AAqBD;;;;GAIG;AACH,wBAAgB,qBAAqB,CACpC,cAAc,EAAE,SAAS,oBAAoB,EAAE,EAC/C,OAAO,GAAE,sBAAyD,GAChE,qBAAqB,CAevB;AAED;;;;;GAKG;AACH,wBAAgB,wBAAwB,CAAC,KAAK,EAAE,SAAS,iBAAiB,EAAE,GAAG,MAAM,GAAG,SAAS,CAUhG","sourcesContent":["/**\n * Evidence-gated tool-selection promotion (closing the observe/promote loop).\n *\n * `tool-selection-controller.ts` records, per tool call, a full expected-utility decision\n * (`ToolSelectionController.begin()`), but that per-call decision is scoped to whichever\n * tool the model already chose — only the chosen tool is ever eligible to be \"recommended\"\n * (see `candidateFor` in tool-selection-controller.ts), so it can validate a choice but never\n * suggest a different one. This module is the SEPARATE, aggregate read-side: given the\n * per-(model,intent,tool) track record accumulated in `ToolPerformanceStore` over many calls,\n * decide whether one tool has become the clear, evidence-backed default for a (model, intent)\n * pair — and if so, render a compact, cache-stable prompt hint.\n *\n * Reuses the exact same expected-utility gate (`decideExpectedUtility`) as call-time decisions,\n * so promotion and the call-time recommend/shortlist/abstain gate share one threshold\n * vocabulary: positive best-utility, a minimum margin over the runner-up, bounded entropy, and a\n * minimum sample count. A tool is promoted only when the gate returns \"recommend\"; ambiguous\n * (high-entropy/low-margin) or thin-evidence buckets never surface a hint — they stay silent,\n * which is the \"shortlist on high entropy, never a false-confident pick\" behavior.\n *\n * Purely functional: no store/env access, so it is trivial to unit test and never mutates state.\n * The stateful pieces (kill switches, reading the store, formatting into the live system prompt)\n * live in `tool-selection-controller.ts` and `system-prompt-builder.ts`.\n */\n\nimport {\n\tDEFAULT_EXPECTED_UTILITY_WEIGHTS,\n\tdecideExpectedUtility,\n\ttype ExpectedUtilityCandidate,\n\ttype ExpectedUtilityWeights,\n} from \"./expected-utility.ts\";\nimport type { ToolPerformanceStats, ToolSelectionIntentClass } from \"./tool-performance-store.ts\";\n\nexport interface ToolSelectionHint {\n\tmodelRef: string;\n\tintentClass: ToolSelectionIntentClass;\n\ttool: string;\n\tsampleCount: number;\n\tmargin: number;\n\tentropy: number;\n}\n\nexport interface ToolPromotionDecision {\n\t/** The promoted tool, present only when the evidence gate returned \"recommend\". */\n\ttool?: string;\n\tsampleCount: number;\n\tmargin: number;\n\tentropy: number;\n}\n\nfunction candidateFromStats(stats: ToolPerformanceStats): ExpectedUtilityCandidate {\n\treturn {\n\t\ttool: stats.tool,\n\t\t// All candidates here were recorded under the SAME (modelRef, intentClass) bucket — the\n\t\t// write path only ever stores a tool's stats under its own classified intent (see\n\t\t// classifyToolIntent + candidateFor in tool-selection-controller.ts) — so there is no\n\t\t// per-candidate affinity prior left to apply; every candidate is equally \"in scope\".\n\t\tvalue: 1,\n\t\talpha: stats.alpha,\n\t\tbeta: stats.beta,\n\t\tsampleCount: stats.sampleCount,\n\t\tlatencyMs: stats.latencyEwmaMs,\n\t\ttokenEstimate:\n\t\t\tstats.inputTokenEstimateEwma === undefined && stats.outputTokenEstimateEwma === undefined\n\t\t\t\t? undefined\n\t\t\t\t: (stats.inputTokenEstimateEwma ?? 0) + (stats.outputTokenEstimateEwma ?? 0),\n\t};\n}\n\n/**\n * Evidence-gated promotion for one (model, intentClass) bucket: which tool, if any, has become\n * the clear default given its accumulated track record. `statsForIntent` should already be\n * scoped to a single (modelRef, intentClass) — see `ToolPerformanceStore.getStatsForIntent`.\n */\nexport function evaluateToolPromotion(\n\tstatsForIntent: readonly ToolPerformanceStats[],\n\tweights: ExpectedUtilityWeights = DEFAULT_EXPECTED_UTILITY_WEIGHTS,\n): ToolPromotionDecision {\n\tif (statsForIntent.length === 0) {\n\t\treturn { sampleCount: 0, margin: 0, entropy: 0 };\n\t}\n\tconst decision = decideExpectedUtility(statsForIntent.map(candidateFromStats), weights);\n\tif (decision.disposition !== \"recommend\" || !decision.recommendation) {\n\t\treturn { sampleCount: 0, margin: decision.margin, entropy: decision.entropy };\n\t}\n\tconst winner = decision.ranked.find((candidate) => candidate.tool === decision.recommendation);\n\treturn {\n\t\ttool: decision.recommendation,\n\t\tsampleCount: winner?.sampleCount ?? 0,\n\t\tmargin: decision.margin,\n\t\tentropy: decision.entropy,\n\t};\n}\n\n/**\n * Renders the active hints into a small, stable prompt block. Deliberately omits live numbers\n * (sample counts, rates) that would change every time evidence accumulates — only the SET of\n * (intent -> tool) pairs is included, so the block's text changes only when the promoted tool for\n * an intent actually flips, not on every tool call (system-prompt cache stability).\n */\nexport function formatToolSelectionHints(hints: readonly ToolSelectionHint[]): string | undefined {\n\tif (hints.length === 0) return undefined;\n\tconst lines = [...hints]\n\t\t.sort((left, right) => left.intentClass.localeCompare(right.intentClass))\n\t\t.map((hint) => `- ${hint.intentClass}: \\`${hint.tool}\\` has the established track record for this model.`);\n\treturn [\n\t\t\"Learned tool preferences (evidence-gated observation, not a directive):\",\n\t\t...lines,\n\t\t\"Treat as a shortlist signal only — use judgment for the actual task at hand.\",\n\t].join(\"\\n\");\n}\n"]}
@@ -0,0 +1,81 @@
1
+ /**
2
+ * Evidence-gated tool-selection promotion (closing the observe/promote loop).
3
+ *
4
+ * `tool-selection-controller.ts` records, per tool call, a full expected-utility decision
5
+ * (`ToolSelectionController.begin()`), but that per-call decision is scoped to whichever
6
+ * tool the model already chose — only the chosen tool is ever eligible to be "recommended"
7
+ * (see `candidateFor` in tool-selection-controller.ts), so it can validate a choice but never
8
+ * suggest a different one. This module is the SEPARATE, aggregate read-side: given the
9
+ * per-(model,intent,tool) track record accumulated in `ToolPerformanceStore` over many calls,
10
+ * decide whether one tool has become the clear, evidence-backed default for a (model, intent)
11
+ * pair — and if so, render a compact, cache-stable prompt hint.
12
+ *
13
+ * Reuses the exact same expected-utility gate (`decideExpectedUtility`) as call-time decisions,
14
+ * so promotion and the call-time recommend/shortlist/abstain gate share one threshold
15
+ * vocabulary: positive best-utility, a minimum margin over the runner-up, bounded entropy, and a
16
+ * minimum sample count. A tool is promoted only when the gate returns "recommend"; ambiguous
17
+ * (high-entropy/low-margin) or thin-evidence buckets never surface a hint — they stay silent,
18
+ * which is the "shortlist on high entropy, never a false-confident pick" behavior.
19
+ *
20
+ * Purely functional: no store/env access, so it is trivial to unit test and never mutates state.
21
+ * The stateful pieces (kill switches, reading the store, formatting into the live system prompt)
22
+ * live in `tool-selection-controller.ts` and `system-prompt-builder.ts`.
23
+ */
24
+ import { DEFAULT_EXPECTED_UTILITY_WEIGHTS, decideExpectedUtility, } from "./expected-utility.js";
25
+ function candidateFromStats(stats) {
26
+ return {
27
+ tool: stats.tool,
28
+ // All candidates here were recorded under the SAME (modelRef, intentClass) bucket — the
29
+ // write path only ever stores a tool's stats under its own classified intent (see
30
+ // classifyToolIntent + candidateFor in tool-selection-controller.ts) — so there is no
31
+ // per-candidate affinity prior left to apply; every candidate is equally "in scope".
32
+ value: 1,
33
+ alpha: stats.alpha,
34
+ beta: stats.beta,
35
+ sampleCount: stats.sampleCount,
36
+ latencyMs: stats.latencyEwmaMs,
37
+ tokenEstimate: stats.inputTokenEstimateEwma === undefined && stats.outputTokenEstimateEwma === undefined
38
+ ? undefined
39
+ : (stats.inputTokenEstimateEwma ?? 0) + (stats.outputTokenEstimateEwma ?? 0),
40
+ };
41
+ }
42
+ /**
43
+ * Evidence-gated promotion for one (model, intentClass) bucket: which tool, if any, has become
44
+ * the clear default given its accumulated track record. `statsForIntent` should already be
45
+ * scoped to a single (modelRef, intentClass) — see `ToolPerformanceStore.getStatsForIntent`.
46
+ */
47
+ export function evaluateToolPromotion(statsForIntent, weights = DEFAULT_EXPECTED_UTILITY_WEIGHTS) {
48
+ if (statsForIntent.length === 0) {
49
+ return { sampleCount: 0, margin: 0, entropy: 0 };
50
+ }
51
+ const decision = decideExpectedUtility(statsForIntent.map(candidateFromStats), weights);
52
+ if (decision.disposition !== "recommend" || !decision.recommendation) {
53
+ return { sampleCount: 0, margin: decision.margin, entropy: decision.entropy };
54
+ }
55
+ const winner = decision.ranked.find((candidate) => candidate.tool === decision.recommendation);
56
+ return {
57
+ tool: decision.recommendation,
58
+ sampleCount: winner?.sampleCount ?? 0,
59
+ margin: decision.margin,
60
+ entropy: decision.entropy,
61
+ };
62
+ }
63
+ /**
64
+ * Renders the active hints into a small, stable prompt block. Deliberately omits live numbers
65
+ * (sample counts, rates) that would change every time evidence accumulates — only the SET of
66
+ * (intent -> tool) pairs is included, so the block's text changes only when the promoted tool for
67
+ * an intent actually flips, not on every tool call (system-prompt cache stability).
68
+ */
69
+ export function formatToolSelectionHints(hints) {
70
+ if (hints.length === 0)
71
+ return undefined;
72
+ const lines = [...hints]
73
+ .sort((left, right) => left.intentClass.localeCompare(right.intentClass))
74
+ .map((hint) => `- ${hint.intentClass}: \`${hint.tool}\` has the established track record for this model.`);
75
+ return [
76
+ "Learned tool preferences (evidence-gated observation, not a directive):",
77
+ ...lines,
78
+ "Treat as a shortlist signal only — use judgment for the actual task at hand.",
79
+ ].join("\n");
80
+ }
81
+ //# sourceMappingURL=promotion.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"promotion.js","sourceRoot":"","sources":["../../../src/core/tool-selection/promotion.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,OAAO,EACN,gCAAgC,EAChC,qBAAqB,GAGrB,MAAM,uBAAuB,CAAC;AAoB/B,SAAS,kBAAkB,CAAC,KAA2B,EAA4B;IAClF,OAAO;QACN,IAAI,EAAE,KAAK,CAAC,IAAI;QAChB,0FAAwF;QACxF,kFAAkF;QAClF,wFAAsF;QACtF,qFAAqF;QACrF,KAAK,EAAE,CAAC;QACR,KAAK,EAAE,KAAK,CAAC,KAAK;QAClB,IAAI,EAAE,KAAK,CAAC,IAAI;QAChB,WAAW,EAAE,KAAK,CAAC,WAAW;QAC9B,SAAS,EAAE,KAAK,CAAC,aAAa;QAC9B,aAAa,EACZ,KAAK,CAAC,sBAAsB,KAAK,SAAS,IAAI,KAAK,CAAC,uBAAuB,KAAK,SAAS;YACxF,CAAC,CAAC,SAAS;YACX,CAAC,CAAC,CAAC,KAAK,CAAC,sBAAsB,IAAI,CAAC,CAAC,GAAG,CAAC,KAAK,CAAC,uBAAuB,IAAI,CAAC,CAAC;KAC9E,CAAC;AAAA,CACF;AAED;;;;GAIG;AACH,MAAM,UAAU,qBAAqB,CACpC,cAA+C,EAC/C,OAAO,GAA2B,gCAAgC,EAC1C;IACxB,IAAI,cAAc,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACjC,OAAO,EAAE,WAAW,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,OAAO,EAAE,CAAC,EAAE,CAAC;IAClD,CAAC;IACD,MAAM,QAAQ,GAAG,qBAAqB,CAAC,cAAc,CAAC,GAAG,CAAC,kBAAkB,CAAC,EAAE,OAAO,CAAC,CAAC;IACxF,IAAI,QAAQ,CAAC,WAAW,KAAK,WAAW,IAAI,CAAC,QAAQ,CAAC,cAAc,EAAE,CAAC;QACtE,OAAO,EAAE,WAAW,EAAE,CAAC,EAAE,MAAM,EAAE,QAAQ,CAAC,MAAM,EAAE,OAAO,EAAE,QAAQ,CAAC,OAAO,EAAE,CAAC;IAC/E,CAAC;IACD,MAAM,MAAM,GAAG,QAAQ,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,SAAS,EAAE,EAAE,CAAC,SAAS,CAAC,IAAI,KAAK,QAAQ,CAAC,cAAc,CAAC,CAAC;IAC/F,OAAO;QACN,IAAI,EAAE,QAAQ,CAAC,cAAc;QAC7B,WAAW,EAAE,MAAM,EAAE,WAAW,IAAI,CAAC;QACrC,MAAM,EAAE,QAAQ,CAAC,MAAM;QACvB,OAAO,EAAE,QAAQ,CAAC,OAAO;KACzB,CAAC;AAAA,CACF;AAED;;;;;GAKG;AACH,MAAM,UAAU,wBAAwB,CAAC,KAAmC,EAAsB;IACjG,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,SAAS,CAAC;IACzC,MAAM,KAAK,GAAG,CAAC,GAAG,KAAK,CAAC;SACtB,IAAI,CAAC,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE,CAAC,IAAI,CAAC,WAAW,CAAC,aAAa,CAAC,KAAK,CAAC,WAAW,CAAC,CAAC;SACxE,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,KAAK,IAAI,CAAC,WAAW,OAAO,IAAI,CAAC,IAAI,qDAAqD,CAAC,CAAC;IAC5G,OAAO;QACN,yEAAyE;QACzE,GAAG,KAAK;QACR,gFAA8E;KAC9E,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AAAA,CACb","sourcesContent":["/**\n * Evidence-gated tool-selection promotion (closing the observe/promote loop).\n *\n * `tool-selection-controller.ts` records, per tool call, a full expected-utility decision\n * (`ToolSelectionController.begin()`), but that per-call decision is scoped to whichever\n * tool the model already chose — only the chosen tool is ever eligible to be \"recommended\"\n * (see `candidateFor` in tool-selection-controller.ts), so it can validate a choice but never\n * suggest a different one. This module is the SEPARATE, aggregate read-side: given the\n * per-(model,intent,tool) track record accumulated in `ToolPerformanceStore` over many calls,\n * decide whether one tool has become the clear, evidence-backed default for a (model, intent)\n * pair — and if so, render a compact, cache-stable prompt hint.\n *\n * Reuses the exact same expected-utility gate (`decideExpectedUtility`) as call-time decisions,\n * so promotion and the call-time recommend/shortlist/abstain gate share one threshold\n * vocabulary: positive best-utility, a minimum margin over the runner-up, bounded entropy, and a\n * minimum sample count. A tool is promoted only when the gate returns \"recommend\"; ambiguous\n * (high-entropy/low-margin) or thin-evidence buckets never surface a hint — they stay silent,\n * which is the \"shortlist on high entropy, never a false-confident pick\" behavior.\n *\n * Purely functional: no store/env access, so it is trivial to unit test and never mutates state.\n * The stateful pieces (kill switches, reading the store, formatting into the live system prompt)\n * live in `tool-selection-controller.ts` and `system-prompt-builder.ts`.\n */\n\nimport {\n\tDEFAULT_EXPECTED_UTILITY_WEIGHTS,\n\tdecideExpectedUtility,\n\ttype ExpectedUtilityCandidate,\n\ttype ExpectedUtilityWeights,\n} from \"./expected-utility.ts\";\nimport type { ToolPerformanceStats, ToolSelectionIntentClass } from \"./tool-performance-store.ts\";\n\nexport interface ToolSelectionHint {\n\tmodelRef: string;\n\tintentClass: ToolSelectionIntentClass;\n\ttool: string;\n\tsampleCount: number;\n\tmargin: number;\n\tentropy: number;\n}\n\nexport interface ToolPromotionDecision {\n\t/** The promoted tool, present only when the evidence gate returned \"recommend\". */\n\ttool?: string;\n\tsampleCount: number;\n\tmargin: number;\n\tentropy: number;\n}\n\nfunction candidateFromStats(stats: ToolPerformanceStats): ExpectedUtilityCandidate {\n\treturn {\n\t\ttool: stats.tool,\n\t\t// All candidates here were recorded under the SAME (modelRef, intentClass) bucket — the\n\t\t// write path only ever stores a tool's stats under its own classified intent (see\n\t\t// classifyToolIntent + candidateFor in tool-selection-controller.ts) — so there is no\n\t\t// per-candidate affinity prior left to apply; every candidate is equally \"in scope\".\n\t\tvalue: 1,\n\t\talpha: stats.alpha,\n\t\tbeta: stats.beta,\n\t\tsampleCount: stats.sampleCount,\n\t\tlatencyMs: stats.latencyEwmaMs,\n\t\ttokenEstimate:\n\t\t\tstats.inputTokenEstimateEwma === undefined && stats.outputTokenEstimateEwma === undefined\n\t\t\t\t? undefined\n\t\t\t\t: (stats.inputTokenEstimateEwma ?? 0) + (stats.outputTokenEstimateEwma ?? 0),\n\t};\n}\n\n/**\n * Evidence-gated promotion for one (model, intentClass) bucket: which tool, if any, has become\n * the clear default given its accumulated track record. `statsForIntent` should already be\n * scoped to a single (modelRef, intentClass) — see `ToolPerformanceStore.getStatsForIntent`.\n */\nexport function evaluateToolPromotion(\n\tstatsForIntent: readonly ToolPerformanceStats[],\n\tweights: ExpectedUtilityWeights = DEFAULT_EXPECTED_UTILITY_WEIGHTS,\n): ToolPromotionDecision {\n\tif (statsForIntent.length === 0) {\n\t\treturn { sampleCount: 0, margin: 0, entropy: 0 };\n\t}\n\tconst decision = decideExpectedUtility(statsForIntent.map(candidateFromStats), weights);\n\tif (decision.disposition !== \"recommend\" || !decision.recommendation) {\n\t\treturn { sampleCount: 0, margin: decision.margin, entropy: decision.entropy };\n\t}\n\tconst winner = decision.ranked.find((candidate) => candidate.tool === decision.recommendation);\n\treturn {\n\t\ttool: decision.recommendation,\n\t\tsampleCount: winner?.sampleCount ?? 0,\n\t\tmargin: decision.margin,\n\t\tentropy: decision.entropy,\n\t};\n}\n\n/**\n * Renders the active hints into a small, stable prompt block. Deliberately omits live numbers\n * (sample counts, rates) that would change every time evidence accumulates — only the SET of\n * (intent -> tool) pairs is included, so the block's text changes only when the promoted tool for\n * an intent actually flips, not on every tool call (system-prompt cache stability).\n */\nexport function formatToolSelectionHints(hints: readonly ToolSelectionHint[]): string | undefined {\n\tif (hints.length === 0) return undefined;\n\tconst lines = [...hints]\n\t\t.sort((left, right) => left.intentClass.localeCompare(right.intentClass))\n\t\t.map((hint) => `- ${hint.intentClass}: \\`${hint.tool}\\` has the established track record for this model.`);\n\treturn [\n\t\t\"Learned tool preferences (evidence-gated observation, not a directive):\",\n\t\t...lines,\n\t\t\"Treat as a shortlist signal only — use judgment for the actual task at hand.\",\n\t].join(\"\\n\");\n}\n"]}
@@ -52,6 +52,26 @@ export interface ToolSelectionMetrics {
52
52
  averageInputTokenEstimate?: number;
53
53
  averageOutputTokenEstimate?: number;
54
54
  }
55
+ /**
56
+ * Durable, per-(model,intent) aggregate of the observe-mode loop: does the raw expected-utility
57
+ * ranking's top pick (`ToolSelectionObservation.ranked[0]`) match what the model actually called
58
+ * (`ToolSelectionObservation.actualTool`)? Tracked separately from the capped, rolling
59
+ * `observations` log so evidence for a given (model,intent) pair survives that log's trimming.
60
+ * `hintActive*` fields are the SAME agreement measure, but restricted to calls made while an
61
+ * evidence-gated promotion hint (see promotion.ts) was active for this bucket — the hint's own
62
+ * efficacy trace: it never gates activation directly (that is always
63
+ * recomputed live from `ToolPerformanceStats`), but it is the durable evidence a report can show
64
+ * for "is the hint still earning its keep".
65
+ */
66
+ export interface ToolSelectionIntentAgreement {
67
+ modelRef: string;
68
+ intentClass: ToolSelectionIntentClass;
69
+ sampleCount: number;
70
+ agreementCount: number;
71
+ hintActiveSampleCount: number;
72
+ hintActiveAgreementCount: number;
73
+ lastUpdatedAt: string;
74
+ }
55
75
  export interface ToolExecutionObservation {
56
76
  key: ToolPerformanceKey;
57
77
  success: boolean;
@@ -60,20 +80,37 @@ export interface ToolExecutionObservation {
60
80
  outputTokenEstimate?: number;
61
81
  selection: Omit<ToolSelectionObservation, "at" | "modelRef" | "intentClass" | "actualTool" | "succeeded">;
62
82
  at?: string;
83
+ /**
84
+ * Whether an evidence-gated promotion hint (promotion.ts) was already active for this
85
+ * (model,intent) bucket BEFORE this call was recorded — captured by the controller at
86
+ * `begin()` time, so it reflects evidence up to but not including this observation. Used only
87
+ * to bucket the durable agreement stats (`ToolSelectionIntentAgreement.hintActive*`); never
88
+ * changes what gets recorded, only how it is split for the efficacy report.
89
+ */
90
+ hintActiveAtCallTime?: boolean;
63
91
  }
64
92
  export declare class ToolPerformanceStore {
65
93
  private readonly filePath;
66
94
  private readonly fingerprint;
95
+ private readonly readOnly;
67
96
  constructor(filePath: string, options?: {
68
97
  fingerprint?: () => HostFingerprint;
98
+ readOnly?: boolean;
69
99
  });
70
100
  static forAgentDir(agentDir: string, options?: {
71
101
  fingerprint?: () => HostFingerprint;
102
+ readOnly?: boolean;
72
103
  }): ToolPerformanceStore;
73
104
  private load;
74
105
  private save;
75
106
  private hostData;
76
107
  get(key: ToolPerformanceKey): ToolPerformanceStats;
108
+ /** Every per-tool track record recorded for a (model,intent) bucket — the promotion.ts input. */
109
+ getStatsForIntent(modelRef: string, intentClass: ToolSelectionIntentClass): ToolPerformanceStats[];
110
+ /** Durable observe-mode agreement for one (model,intent) bucket (see {@link ToolSelectionIntentAgreement}). */
111
+ getIntentAgreement(modelRef: string, intentClass: ToolSelectionIntentClass): ToolSelectionIntentAgreement;
112
+ /** All recorded (model,intent) agreement buckets, optionally scoped to one model — report input. */
113
+ getAllIntentAgreements(modelRef?: string): ToolSelectionIntentAgreement[];
77
114
  recordValidation(key: ToolPerformanceKey, outcome: "repaired" | "bounced", at?: string): ToolPerformanceStats;
78
115
  recordExecution(observation: ToolExecutionObservation): ToolPerformanceStats;
79
116
  getMetrics(modelRef?: string): ToolSelectionMetrics;
@@ -1 +1 @@
1
- {"version":3,"file":"tool-performance-store.d.ts","sourceRoot":"","sources":["../../../src/core/tool-selection/tool-performance-store.ts"],"names":[],"mappings":"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","sourcesContent":["import { existsSync, mkdirSync, readFileSync, writeFileSync } from \"node:fs\";\nimport { dirname, join } from \"node:path\";\nimport { currentHostFingerprint, type HostFingerprint } from \"../models/fitness-store.ts\";\n\nconst STORE_VERSION = 1;\nconst MAX_STATS_PER_HOST = 500;\nconst MAX_OBSERVATIONS_PER_HOST = 1_000;\nconst EWMA_ALPHA = 0.25;\n\nexport type ToolSelectionIntentClass = \"read\" | \"search\" | \"execute\" | \"write\" | \"retrieve\" | \"explain\" | \"other\";\n\nexport interface ToolPerformanceKey {\n\tmodelRef: string;\n\tintentClass: ToolSelectionIntentClass;\n\ttool: string;\n}\n\nexport interface ToolPerformanceStats extends ToolPerformanceKey {\n\talpha: number;\n\tbeta: number;\n\tsampleCount: number;\n\tlatencyEwmaMs?: number;\n\tlatencyDeviationEwmaMs?: number;\n\tinputTokenEstimateEwma?: number;\n\toutputTokenEstimateEwma?: number;\n\trepairCount: number;\n\tbounceCount: number;\n\tfailureCount: number;\n\tlastUsedAt: string;\n}\n\nexport interface ToolSelectionObservation {\n\tat: string;\n\tmodelRef: string;\n\tintentClass: ToolSelectionIntentClass;\n\tactualTool: string;\n\tfirstTool: boolean;\n\tsucceeded: boolean;\n\tdisposition: \"recommend\" | \"shortlist\" | \"abstain\";\n\trecommendation?: string;\n\tshortlist: string[];\n\tentropy: number;\n\tmargin: number;\n\t/** Redacted ranking: names and numeric scores only, never prompts, arguments, paths, or output. */\n\tranked: Array<{ tool: string; utility: number; probability: number }>;\n\tlatencyMs?: number;\n\tinputTokenEstimate?: number;\n\toutputTokenEstimate?: number;\n}\n\nexport interface ToolSelectionMetrics {\n\tfirstToolAttempts: number;\n\tfirstToolSuccesses: number;\n\twrongToolOrFailureCount: number;\n\trecommendationCount: number;\n\trecommendationMatchedCount: number;\n\tshortlistCount: number;\n\tabstentionCount: number;\n\taverageLatencyMs?: number;\n\taverageInputTokenEstimate?: number;\n\taverageOutputTokenEstimate?: number;\n}\n\ninterface HostToolPerformanceData {\n\thost: HostFingerprint;\n\tstats: Record<string, ToolPerformanceStats>;\n\tobservations: ToolSelectionObservation[];\n}\n\ninterface ToolPerformanceStoreFile {\n\tversion: 1;\n\thosts: Record<string, HostToolPerformanceData>;\n}\n\nexport interface ToolExecutionObservation {\n\tkey: ToolPerformanceKey;\n\tsuccess: boolean;\n\tlatencyMs: number;\n\tinputTokenEstimate?: number;\n\toutputTokenEstimate?: number;\n\tselection: Omit<ToolSelectionObservation, \"at\" | \"modelRef\" | \"intentClass\" | \"actualTool\" | \"succeeded\">;\n\tat?: string;\n}\n\nfunction statKey(key: ToolPerformanceKey): string {\n\treturn `${key.modelRef}\\0${key.intentClass}\\0${key.tool}`;\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n\treturn typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\nfunction isIntentClass(value: unknown): value is ToolSelectionIntentClass {\n\treturn (\n\t\tvalue === \"read\" ||\n\t\tvalue === \"search\" ||\n\t\tvalue === \"execute\" ||\n\t\tvalue === \"write\" ||\n\t\tvalue === \"retrieve\" ||\n\t\tvalue === \"explain\" ||\n\t\tvalue === \"other\"\n\t);\n}\n\nfunction isStats(value: unknown): value is ToolPerformanceStats {\n\treturn (\n\t\tisRecord(value) &&\n\t\ttypeof value.modelRef === \"string\" &&\n\t\tisIntentClass(value.intentClass) &&\n\t\ttypeof value.tool === \"string\" &&\n\t\ttypeof value.alpha === \"number\" &&\n\t\ttypeof value.beta === \"number\" &&\n\t\ttypeof value.sampleCount === \"number\" &&\n\t\ttypeof value.repairCount === \"number\" &&\n\t\ttypeof value.bounceCount === \"number\" &&\n\t\ttypeof value.failureCount === \"number\" &&\n\t\ttypeof value.lastUsedAt === \"string\"\n\t);\n}\n\nfunction isObservation(value: unknown): value is ToolSelectionObservation {\n\treturn (\n\t\tisRecord(value) &&\n\t\ttypeof value.at === \"string\" &&\n\t\ttypeof value.modelRef === \"string\" &&\n\t\tisIntentClass(value.intentClass) &&\n\t\ttypeof value.actualTool === \"string\" &&\n\t\ttypeof value.firstTool === \"boolean\" &&\n\t\ttypeof value.succeeded === \"boolean\" &&\n\t\t(value.disposition === \"recommend\" || value.disposition === \"shortlist\" || value.disposition === \"abstain\") &&\n\t\tArray.isArray(value.shortlist) &&\n\t\tvalue.shortlist.every((tool) => typeof tool === \"string\") &&\n\t\ttypeof value.entropy === \"number\" &&\n\t\ttypeof value.margin === \"number\" &&\n\t\tArray.isArray(value.ranked) &&\n\t\tvalue.ranked.every(\n\t\t\t(entry) =>\n\t\t\t\tisRecord(entry) &&\n\t\t\t\ttypeof entry.tool === \"string\" &&\n\t\t\t\ttypeof entry.utility === \"number\" &&\n\t\t\t\ttypeof entry.probability === \"number\",\n\t\t) &&\n\t\t(value.latencyMs === undefined || typeof value.latencyMs === \"number\") &&\n\t\t(value.inputTokenEstimate === undefined || typeof value.inputTokenEstimate === \"number\") &&\n\t\t(value.outputTokenEstimate === undefined || typeof value.outputTokenEstimate === \"number\")\n\t);\n}\n\nfunction emptyStats(key: ToolPerformanceKey, at: string): ToolPerformanceStats {\n\treturn {\n\t\t...key,\n\t\talpha: 1,\n\t\tbeta: 1,\n\t\tsampleCount: 0,\n\t\trepairCount: 0,\n\t\tbounceCount: 0,\n\t\tfailureCount: 0,\n\t\tlastUsedAt: at,\n\t};\n}\n\nfunction finiteNonNegative(value: number | undefined): number | undefined {\n\treturn typeof value === \"number\" && Number.isFinite(value) && value >= 0 ? value : undefined;\n}\n\nfunction updateEwma(previous: number | undefined, next: number | undefined): number | undefined {\n\tif (next === undefined) return previous;\n\treturn previous === undefined ? next : previous + EWMA_ALPHA * (next - previous);\n}\n\nfunction updateDeviation(\n\tpreviousValue: number | undefined,\n\tpreviousDeviation: number | undefined,\n\tnext: number | undefined,\n): number | undefined {\n\tif (next === undefined) return previousDeviation;\n\tconst deviation = previousValue === undefined ? 0 : Math.abs(next - previousValue);\n\treturn updateEwma(previousDeviation, deviation);\n}\n\nfunction parseHost(value: unknown): HostToolPerformanceData | undefined {\n\tif (!isRecord(value) || !isRecord(value.host) || !isRecord(value.stats) || !Array.isArray(value.observations))\n\t\treturn undefined;\n\tconst host = value.host;\n\tif (\n\t\ttypeof host.id !== \"string\" ||\n\t\ttypeof host.cpu !== \"string\" ||\n\t\ttypeof host.cores !== \"number\" ||\n\t\ttypeof host.totalMemGb !== \"number\"\n\t)\n\t\treturn undefined;\n\treturn {\n\t\thost: { id: host.id, cpu: host.cpu, cores: host.cores, totalMemGb: host.totalMemGb },\n\t\tstats: Object.fromEntries(\n\t\t\tObject.entries(value.stats).filter((entry): entry is [string, ToolPerformanceStats] => isStats(entry[1])),\n\t\t),\n\t\tobservations: value.observations.filter(isObservation),\n\t};\n}\n\nfunction parseFile(value: unknown): ToolPerformanceStoreFile {\n\tif (!isRecord(value) || value.version !== STORE_VERSION || !isRecord(value.hosts)) {\n\t\treturn { version: STORE_VERSION, hosts: {} };\n\t}\n\treturn {\n\t\tversion: STORE_VERSION,\n\t\thosts: Object.fromEntries(\n\t\t\tObject.entries(value.hosts)\n\t\t\t\t.map(([hostId, host]) => [hostId, parseHost(host)] as const)\n\t\t\t\t.filter((entry): entry is readonly [string, HostToolPerformanceData] => entry[1] !== undefined),\n\t\t),\n\t};\n}\n\nfunction trimStats(stats: Record<string, ToolPerformanceStats>): Record<string, ToolPerformanceStats> {\n\tconst entries = Object.entries(stats);\n\tif (entries.length <= MAX_STATS_PER_HOST) return stats;\n\treturn Object.fromEntries(\n\t\tentries\n\t\t\t.sort(([, left], [, right]) => Date.parse(right.lastUsedAt) - Date.parse(left.lastUsedAt))\n\t\t\t.slice(0, MAX_STATS_PER_HOST),\n\t);\n}\n\nexport class ToolPerformanceStore {\n\tprivate readonly filePath: string;\n\tprivate readonly fingerprint: () => HostFingerprint;\n\n\tconstructor(filePath: string, options: { fingerprint?: () => HostFingerprint } = {}) {\n\t\tthis.filePath = filePath;\n\t\tthis.fingerprint = options.fingerprint ?? currentHostFingerprint;\n\t}\n\n\tstatic forAgentDir(agentDir: string, options: { fingerprint?: () => HostFingerprint } = {}): ToolPerformanceStore {\n\t\treturn new ToolPerformanceStore(join(agentDir, \"state\", \"tool-performance.json\"), options);\n\t}\n\n\tprivate load(): ToolPerformanceStoreFile {\n\t\ttry {\n\t\t\tif (!existsSync(this.filePath)) return { version: STORE_VERSION, hosts: {} };\n\t\t\treturn parseFile(JSON.parse(readFileSync(this.filePath, \"utf8\")));\n\t\t} catch {\n\t\t\treturn { version: STORE_VERSION, hosts: {} };\n\t\t}\n\t}\n\n\tprivate save(file: ToolPerformanceStoreFile): void {\n\t\tmkdirSync(dirname(this.filePath), { recursive: true });\n\t\twriteFileSync(this.filePath, `${JSON.stringify(file, null, \"\\t\")}\\n`, \"utf8\");\n\t}\n\n\tprivate hostData(file: ToolPerformanceStoreFile): HostToolPerformanceData {\n\t\tconst fingerprint = this.fingerprint();\n\t\tconst existing = file.hosts[fingerprint.id];\n\t\tif (existing) return existing;\n\t\tconst created: HostToolPerformanceData = { host: fingerprint, stats: {}, observations: [] };\n\t\tfile.hosts[fingerprint.id] = created;\n\t\treturn created;\n\t}\n\n\tget(key: ToolPerformanceKey): ToolPerformanceStats {\n\t\tconst file = this.load();\n\t\tconst host = file.hosts[this.fingerprint().id];\n\t\tconst stats = host?.stats[statKey(key)];\n\t\treturn stats ? { ...stats } : emptyStats(key, new Date(0).toISOString());\n\t}\n\n\trecordValidation(\n\t\tkey: ToolPerformanceKey,\n\t\toutcome: \"repaired\" | \"bounced\",\n\t\tat = new Date().toISOString(),\n\t): ToolPerformanceStats {\n\t\tconst file = this.load();\n\t\tconst host = this.hostData(file);\n\t\tconst storageKey = statKey(key);\n\t\tconst current = host.stats[storageKey] ?? emptyStats(key, at);\n\t\tconst next: ToolPerformanceStats = {\n\t\t\t...current,\n\t\t\trepairCount: current.repairCount + (outcome === \"repaired\" ? 1 : 0),\n\t\t\tbounceCount: current.bounceCount + (outcome === \"bounced\" ? 1 : 0),\n\t\t\tlastUsedAt: at,\n\t\t};\n\t\thost.stats[storageKey] = next;\n\t\thost.stats = trimStats(host.stats);\n\t\tthis.save(file);\n\t\treturn { ...next };\n\t}\n\n\trecordExecution(observation: ToolExecutionObservation): ToolPerformanceStats {\n\t\tconst at = observation.at ?? new Date().toISOString();\n\t\tconst file = this.load();\n\t\tconst host = this.hostData(file);\n\t\tconst storageKey = statKey(observation.key);\n\t\tconst current = host.stats[storageKey] ?? emptyStats(observation.key, at);\n\t\tconst latencyMs = finiteNonNegative(observation.latencyMs);\n\t\tconst inputTokenEstimate = finiteNonNegative(observation.inputTokenEstimate);\n\t\tconst outputTokenEstimate = finiteNonNegative(observation.outputTokenEstimate);\n\t\tconst next: ToolPerformanceStats = {\n\t\t\t...current,\n\t\t\talpha: current.alpha + (observation.success ? 1 : 0),\n\t\t\tbeta: current.beta + (observation.success ? 0 : 1),\n\t\t\tsampleCount: current.sampleCount + 1,\n\t\t\tlatencyEwmaMs: updateEwma(current.latencyEwmaMs, latencyMs),\n\t\t\tlatencyDeviationEwmaMs: updateDeviation(current.latencyEwmaMs, current.latencyDeviationEwmaMs, latencyMs),\n\t\t\tinputTokenEstimateEwma: updateEwma(current.inputTokenEstimateEwma, inputTokenEstimate),\n\t\t\toutputTokenEstimateEwma: updateEwma(current.outputTokenEstimateEwma, outputTokenEstimate),\n\t\t\tfailureCount: current.failureCount + (observation.success ? 0 : 1),\n\t\t\tlastUsedAt: at,\n\t\t};\n\t\thost.stats[storageKey] = next;\n\t\thost.stats = trimStats(host.stats);\n\t\thost.observations.push({\n\t\t\t...observation.selection,\n\t\t\tat,\n\t\t\tmodelRef: observation.key.modelRef,\n\t\t\tintentClass: observation.key.intentClass,\n\t\t\tactualTool: observation.key.tool,\n\t\t\tsucceeded: observation.success,\n\t\t\tranked: observation.selection.ranked.slice(0, 6),\n\t\t\tshortlist: observation.selection.shortlist.slice(0, 3),\n\t\t\tlatencyMs,\n\t\t\tinputTokenEstimate,\n\t\t\toutputTokenEstimate,\n\t\t});\n\t\tif (host.observations.length > MAX_OBSERVATIONS_PER_HOST) {\n\t\t\thost.observations = host.observations.slice(-MAX_OBSERVATIONS_PER_HOST);\n\t\t}\n\t\tthis.save(file);\n\t\treturn { ...next };\n\t}\n\n\tgetMetrics(modelRef?: string): ToolSelectionMetrics {\n\t\tconst host = this.load().hosts[this.fingerprint().id];\n\t\tconst observations = (host?.observations ?? []).filter(\n\t\t\t(observation) => modelRef === undefined || observation.modelRef === modelRef,\n\t\t);\n\t\tconst firstTools = observations.filter((observation) => observation.firstTool);\n\t\tconst recommended = observations.filter((observation) => observation.disposition === \"recommend\");\n\t\tconst average = (values: Array<number | undefined>): number | undefined => {\n\t\t\tconst present = values.filter((value): value is number => value !== undefined);\n\t\t\treturn present.length > 0 ? present.reduce((sum, value) => sum + value, 0) / present.length : undefined;\n\t\t};\n\t\treturn {\n\t\t\tfirstToolAttempts: firstTools.length,\n\t\t\tfirstToolSuccesses: firstTools.filter((observation) => observation.succeeded).length,\n\t\t\twrongToolOrFailureCount: observations.filter(\n\t\t\t\t(observation) =>\n\t\t\t\t\t!observation.succeeded ||\n\t\t\t\t\t(observation.disposition === \"recommend\" && observation.recommendation !== observation.actualTool),\n\t\t\t).length,\n\t\t\trecommendationCount: recommended.length,\n\t\t\trecommendationMatchedCount: recommended.filter(\n\t\t\t\t(observation) => observation.recommendation === observation.actualTool,\n\t\t\t).length,\n\t\t\tshortlistCount: observations.filter((observation) => observation.disposition === \"shortlist\").length,\n\t\t\tabstentionCount: observations.filter((observation) => observation.disposition === \"abstain\").length,\n\t\t\taverageLatencyMs: average(observations.map((observation) => observation.latencyMs)),\n\t\t\taverageInputTokenEstimate: average(observations.map((observation) => observation.inputTokenEstimate)),\n\t\t\taverageOutputTokenEstimate: average(observations.map((observation) => observation.outputTokenEstimate)),\n\t\t};\n\t}\n\n\tgetObservations(modelRef?: string): ToolSelectionObservation[] {\n\t\tconst host = this.load().hosts[this.fingerprint().id];\n\t\treturn (host?.observations ?? [])\n\t\t\t.filter((observation) => modelRef === undefined || observation.modelRef === modelRef)\n\t\t\t.map((observation) => ({\n\t\t\t\t...observation,\n\t\t\t\tshortlist: [...observation.shortlist],\n\t\t\t\tranked: observation.ranked.map((candidate) => ({ ...candidate })),\n\t\t\t}));\n\t}\n}\n"]}
1
+ {"version":3,"file":"tool-performance-store.d.ts","sourceRoot":"","sources":["../../../src/core/tool-selection/tool-performance-store.ts"],"names":[],"mappings":"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","sourcesContent":["import { existsSync, mkdirSync, readFileSync, writeFileSync } from \"node:fs\";\nimport { dirname } from \"node:path\";\nimport { stateFile } from \"../agent-paths.ts\";\nimport { currentHostFingerprint, type HostFingerprint } from \"../models/fitness-store.ts\";\nimport { isWorkerSession } from \"../session-role.ts\";\n\nconst STORE_VERSION = 1;\nconst MAX_STATS_PER_HOST = 500;\nconst MAX_OBSERVATIONS_PER_HOST = 1_000;\nconst MAX_INTENT_AGREEMENT_PER_HOST = 500;\nconst EWMA_ALPHA = 0.25;\n\nexport type ToolSelectionIntentClass = \"read\" | \"search\" | \"execute\" | \"write\" | \"retrieve\" | \"explain\" | \"other\";\n\nexport interface ToolPerformanceKey {\n\tmodelRef: string;\n\tintentClass: ToolSelectionIntentClass;\n\ttool: string;\n}\n\nexport interface ToolPerformanceStats extends ToolPerformanceKey {\n\talpha: number;\n\tbeta: number;\n\tsampleCount: number;\n\tlatencyEwmaMs?: number;\n\tlatencyDeviationEwmaMs?: number;\n\tinputTokenEstimateEwma?: number;\n\toutputTokenEstimateEwma?: number;\n\trepairCount: number;\n\tbounceCount: number;\n\tfailureCount: number;\n\tlastUsedAt: string;\n}\n\nexport interface ToolSelectionObservation {\n\tat: string;\n\tmodelRef: string;\n\tintentClass: ToolSelectionIntentClass;\n\tactualTool: string;\n\tfirstTool: boolean;\n\tsucceeded: boolean;\n\tdisposition: \"recommend\" | \"shortlist\" | \"abstain\";\n\trecommendation?: string;\n\tshortlist: string[];\n\tentropy: number;\n\tmargin: number;\n\t/** Redacted ranking: names and numeric scores only, never prompts, arguments, paths, or output. */\n\tranked: Array<{ tool: string; utility: number; probability: number }>;\n\tlatencyMs?: number;\n\tinputTokenEstimate?: number;\n\toutputTokenEstimate?: number;\n}\n\nexport interface ToolSelectionMetrics {\n\tfirstToolAttempts: number;\n\tfirstToolSuccesses: number;\n\twrongToolOrFailureCount: number;\n\trecommendationCount: number;\n\trecommendationMatchedCount: number;\n\tshortlistCount: number;\n\tabstentionCount: number;\n\taverageLatencyMs?: number;\n\taverageInputTokenEstimate?: number;\n\taverageOutputTokenEstimate?: number;\n}\n\n/**\n * Durable, per-(model,intent) aggregate of the observe-mode loop: does the raw expected-utility\n * ranking's top pick (`ToolSelectionObservation.ranked[0]`) match what the model actually called\n * (`ToolSelectionObservation.actualTool`)? Tracked separately from the capped, rolling\n * `observations` log so evidence for a given (model,intent) pair survives that log's trimming.\n * `hintActive*` fields are the SAME agreement measure, but restricted to calls made while an\n * evidence-gated promotion hint (see promotion.ts) was active for this bucket — the hint's own\n * efficacy trace: it never gates activation directly (that is always\n * recomputed live from `ToolPerformanceStats`), but it is the durable evidence a report can show\n * for \"is the hint still earning its keep\".\n */\nexport interface ToolSelectionIntentAgreement {\n\tmodelRef: string;\n\tintentClass: ToolSelectionIntentClass;\n\tsampleCount: number;\n\tagreementCount: number;\n\thintActiveSampleCount: number;\n\thintActiveAgreementCount: number;\n\tlastUpdatedAt: string;\n}\n\ninterface HostToolPerformanceData {\n\thost: HostFingerprint;\n\tstats: Record<string, ToolPerformanceStats>;\n\tobservations: ToolSelectionObservation[];\n\tintentAgreement: Record<string, ToolSelectionIntentAgreement>;\n}\n\ninterface ToolPerformanceStoreFile {\n\tversion: 1;\n\thosts: Record<string, HostToolPerformanceData>;\n}\n\nexport interface ToolExecutionObservation {\n\tkey: ToolPerformanceKey;\n\tsuccess: boolean;\n\tlatencyMs: number;\n\tinputTokenEstimate?: number;\n\toutputTokenEstimate?: number;\n\tselection: Omit<ToolSelectionObservation, \"at\" | \"modelRef\" | \"intentClass\" | \"actualTool\" | \"succeeded\">;\n\tat?: string;\n\t/**\n\t * Whether an evidence-gated promotion hint (promotion.ts) was already active for this\n\t * (model,intent) bucket BEFORE this call was recorded — captured by the controller at\n\t * `begin()` time, so it reflects evidence up to but not including this observation. Used only\n\t * to bucket the durable agreement stats (`ToolSelectionIntentAgreement.hintActive*`); never\n\t * changes what gets recorded, only how it is split for the efficacy report.\n\t */\n\thintActiveAtCallTime?: boolean;\n}\n\nfunction statKey(key: ToolPerformanceKey): string {\n\treturn `${key.modelRef}\\0${key.intentClass}\\0${key.tool}`;\n}\n\nfunction intentAgreementKey(modelRef: string, intentClass: ToolSelectionIntentClass): string {\n\treturn `${modelRef}\\0${intentClass}`;\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n\treturn typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\nfunction isIntentClass(value: unknown): value is ToolSelectionIntentClass {\n\treturn (\n\t\tvalue === \"read\" ||\n\t\tvalue === \"search\" ||\n\t\tvalue === \"execute\" ||\n\t\tvalue === \"write\" ||\n\t\tvalue === \"retrieve\" ||\n\t\tvalue === \"explain\" ||\n\t\tvalue === \"other\"\n\t);\n}\n\nfunction isStats(value: unknown): value is ToolPerformanceStats {\n\treturn (\n\t\tisRecord(value) &&\n\t\ttypeof value.modelRef === \"string\" &&\n\t\tisIntentClass(value.intentClass) &&\n\t\ttypeof value.tool === \"string\" &&\n\t\ttypeof value.alpha === \"number\" &&\n\t\ttypeof value.beta === \"number\" &&\n\t\ttypeof value.sampleCount === \"number\" &&\n\t\ttypeof value.repairCount === \"number\" &&\n\t\ttypeof value.bounceCount === \"number\" &&\n\t\ttypeof value.failureCount === \"number\" &&\n\t\ttypeof value.lastUsedAt === \"string\"\n\t);\n}\n\nfunction isObservation(value: unknown): value is ToolSelectionObservation {\n\treturn (\n\t\tisRecord(value) &&\n\t\ttypeof value.at === \"string\" &&\n\t\ttypeof value.modelRef === \"string\" &&\n\t\tisIntentClass(value.intentClass) &&\n\t\ttypeof value.actualTool === \"string\" &&\n\t\ttypeof value.firstTool === \"boolean\" &&\n\t\ttypeof value.succeeded === \"boolean\" &&\n\t\t(value.disposition === \"recommend\" || value.disposition === \"shortlist\" || value.disposition === \"abstain\") &&\n\t\tArray.isArray(value.shortlist) &&\n\t\tvalue.shortlist.every((tool) => typeof tool === \"string\") &&\n\t\ttypeof value.entropy === \"number\" &&\n\t\ttypeof value.margin === \"number\" &&\n\t\tArray.isArray(value.ranked) &&\n\t\tvalue.ranked.every(\n\t\t\t(entry) =>\n\t\t\t\tisRecord(entry) &&\n\t\t\t\ttypeof entry.tool === \"string\" &&\n\t\t\t\ttypeof entry.utility === \"number\" &&\n\t\t\t\ttypeof entry.probability === \"number\",\n\t\t) &&\n\t\t(value.latencyMs === undefined || typeof value.latencyMs === \"number\") &&\n\t\t(value.inputTokenEstimate === undefined || typeof value.inputTokenEstimate === \"number\") &&\n\t\t(value.outputTokenEstimate === undefined || typeof value.outputTokenEstimate === \"number\")\n\t);\n}\n\nfunction isIntentAgreement(value: unknown): value is ToolSelectionIntentAgreement {\n\treturn (\n\t\tisRecord(value) &&\n\t\ttypeof value.modelRef === \"string\" &&\n\t\tisIntentClass(value.intentClass) &&\n\t\ttypeof value.sampleCount === \"number\" &&\n\t\ttypeof value.agreementCount === \"number\" &&\n\t\ttypeof value.hintActiveSampleCount === \"number\" &&\n\t\ttypeof value.hintActiveAgreementCount === \"number\" &&\n\t\ttypeof value.lastUpdatedAt === \"string\"\n\t);\n}\n\nfunction emptyStats(key: ToolPerformanceKey, at: string): ToolPerformanceStats {\n\treturn {\n\t\t...key,\n\t\talpha: 1,\n\t\tbeta: 1,\n\t\tsampleCount: 0,\n\t\trepairCount: 0,\n\t\tbounceCount: 0,\n\t\tfailureCount: 0,\n\t\tlastUsedAt: at,\n\t};\n}\n\nfunction emptyIntentAgreement(\n\tmodelRef: string,\n\tintentClass: ToolSelectionIntentClass,\n\tat: string,\n): ToolSelectionIntentAgreement {\n\treturn {\n\t\tmodelRef,\n\t\tintentClass,\n\t\tsampleCount: 0,\n\t\tagreementCount: 0,\n\t\thintActiveSampleCount: 0,\n\t\thintActiveAgreementCount: 0,\n\t\tlastUpdatedAt: at,\n\t};\n}\n\nfunction finiteNonNegative(value: number | undefined): number | undefined {\n\treturn typeof value === \"number\" && Number.isFinite(value) && value >= 0 ? value : undefined;\n}\n\nfunction updateEwma(previous: number | undefined, next: number | undefined): number | undefined {\n\tif (next === undefined) return previous;\n\treturn previous === undefined ? next : previous + EWMA_ALPHA * (next - previous);\n}\n\nfunction updateDeviation(\n\tpreviousValue: number | undefined,\n\tpreviousDeviation: number | undefined,\n\tnext: number | undefined,\n): number | undefined {\n\tif (next === undefined) return previousDeviation;\n\tconst deviation = previousValue === undefined ? 0 : Math.abs(next - previousValue);\n\treturn updateEwma(previousDeviation, deviation);\n}\n\nfunction parseHost(value: unknown): HostToolPerformanceData | undefined {\n\tif (!isRecord(value) || !isRecord(value.host) || !isRecord(value.stats) || !Array.isArray(value.observations))\n\t\treturn undefined;\n\tconst host = value.host;\n\tif (\n\t\ttypeof host.id !== \"string\" ||\n\t\ttypeof host.cpu !== \"string\" ||\n\t\ttypeof host.cores !== \"number\" ||\n\t\ttypeof host.totalMemGb !== \"number\"\n\t)\n\t\treturn undefined;\n\t// intentAgreement is a purely additive field (older store files predate it) — tolerate absence\n\t// rather than bumping STORE_VERSION, same as any other backward-compatible default-empty field.\n\tconst intentAgreementRaw = isRecord(value.intentAgreement) ? value.intentAgreement : {};\n\treturn {\n\t\thost: { id: host.id, cpu: host.cpu, cores: host.cores, totalMemGb: host.totalMemGb },\n\t\tstats: Object.fromEntries(\n\t\t\tObject.entries(value.stats).filter((entry): entry is [string, ToolPerformanceStats] => isStats(entry[1])),\n\t\t),\n\t\tobservations: value.observations.filter(isObservation),\n\t\tintentAgreement: Object.fromEntries(\n\t\t\tObject.entries(intentAgreementRaw).filter((entry): entry is [string, ToolSelectionIntentAgreement] =>\n\t\t\t\tisIntentAgreement(entry[1]),\n\t\t\t),\n\t\t),\n\t};\n}\n\nfunction parseFile(value: unknown): ToolPerformanceStoreFile {\n\tif (!isRecord(value) || value.version !== STORE_VERSION || !isRecord(value.hosts)) {\n\t\treturn { version: STORE_VERSION, hosts: {} };\n\t}\n\treturn {\n\t\tversion: STORE_VERSION,\n\t\thosts: Object.fromEntries(\n\t\t\tObject.entries(value.hosts)\n\t\t\t\t.map(([hostId, host]) => [hostId, parseHost(host)] as const)\n\t\t\t\t.filter((entry): entry is readonly [string, HostToolPerformanceData] => entry[1] !== undefined),\n\t\t),\n\t};\n}\n\nfunction trimStats(stats: Record<string, ToolPerformanceStats>): Record<string, ToolPerformanceStats> {\n\tconst entries = Object.entries(stats);\n\tif (entries.length <= MAX_STATS_PER_HOST) return stats;\n\treturn Object.fromEntries(\n\t\tentries\n\t\t\t.sort(([, left], [, right]) => Date.parse(right.lastUsedAt) - Date.parse(left.lastUsedAt))\n\t\t\t.slice(0, MAX_STATS_PER_HOST),\n\t);\n}\n\nfunction trimIntentAgreement(\n\trecords: Record<string, ToolSelectionIntentAgreement>,\n): Record<string, ToolSelectionIntentAgreement> {\n\tconst entries = Object.entries(records);\n\tif (entries.length <= MAX_INTENT_AGREEMENT_PER_HOST) return records;\n\treturn Object.fromEntries(\n\t\tentries\n\t\t\t.sort(([, left], [, right]) => Date.parse(right.lastUpdatedAt) - Date.parse(left.lastUpdatedAt))\n\t\t\t.slice(0, MAX_INTENT_AGREEMENT_PER_HOST),\n\t);\n}\n\nexport class ToolPerformanceStore {\n\tprivate readonly filePath: string;\n\tprivate readonly fingerprint: () => HostFingerprint;\n\tprivate readonly readOnly: boolean;\n\n\tconstructor(filePath: string, options: { fingerprint?: () => HostFingerprint; readOnly?: boolean } = {}) {\n\t\tthis.filePath = filePath;\n\t\tthis.fingerprint = options.fingerprint ?? currentHostFingerprint;\n\t\tthis.readOnly = options.readOnly ?? isWorkerSession();\n\t}\n\n\tstatic forAgentDir(\n\t\tagentDir: string,\n\t\toptions: { fingerprint?: () => HostFingerprint; readOnly?: boolean } = {},\n\t): ToolPerformanceStore {\n\t\treturn new ToolPerformanceStore(stateFile(agentDir, \"tool-performance.json\"), options);\n\t}\n\n\tprivate load(): ToolPerformanceStoreFile {\n\t\ttry {\n\t\t\tif (!existsSync(this.filePath)) return { version: STORE_VERSION, hosts: {} };\n\t\t\treturn parseFile(JSON.parse(readFileSync(this.filePath, \"utf8\")));\n\t\t} catch {\n\t\t\treturn { version: STORE_VERSION, hosts: {} };\n\t\t}\n\t}\n\n\tprivate save(file: ToolPerformanceStoreFile): void {\n\t\t// Zero-footprint (worker session): never create the state dir, lock, or file -- the caller\n\t\t// still gets its normally-computed return value from the in-memory `file`.\n\t\tif (this.readOnly) return;\n\t\tmkdirSync(dirname(this.filePath), { recursive: true });\n\t\twriteFileSync(this.filePath, `${JSON.stringify(file, null, \"\\t\")}\\n`, \"utf8\");\n\t}\n\n\tprivate hostData(file: ToolPerformanceStoreFile): HostToolPerformanceData {\n\t\tconst fingerprint = this.fingerprint();\n\t\tconst existing = file.hosts[fingerprint.id];\n\t\tif (existing) return existing;\n\t\tconst created: HostToolPerformanceData = { host: fingerprint, stats: {}, observations: [], intentAgreement: {} };\n\t\tfile.hosts[fingerprint.id] = created;\n\t\treturn created;\n\t}\n\n\tget(key: ToolPerformanceKey): ToolPerformanceStats {\n\t\tconst file = this.load();\n\t\tconst host = file.hosts[this.fingerprint().id];\n\t\tconst stats = host?.stats[statKey(key)];\n\t\treturn stats ? { ...stats } : emptyStats(key, new Date(0).toISOString());\n\t}\n\n\t/** Every per-tool track record recorded for a (model,intent) bucket — the promotion.ts input. */\n\tgetStatsForIntent(modelRef: string, intentClass: ToolSelectionIntentClass): ToolPerformanceStats[] {\n\t\tconst host = this.load().hosts[this.fingerprint().id];\n\t\tif (!host) return [];\n\t\treturn Object.values(host.stats)\n\t\t\t.filter((stats) => stats.modelRef === modelRef && stats.intentClass === intentClass)\n\t\t\t.map((stats) => ({ ...stats }));\n\t}\n\n\t/** Durable observe-mode agreement for one (model,intent) bucket (see {@link ToolSelectionIntentAgreement}). */\n\tgetIntentAgreement(modelRef: string, intentClass: ToolSelectionIntentClass): ToolSelectionIntentAgreement {\n\t\tconst host = this.load().hosts[this.fingerprint().id];\n\t\tconst record = host?.intentAgreement[intentAgreementKey(modelRef, intentClass)];\n\t\treturn record ? { ...record } : emptyIntentAgreement(modelRef, intentClass, new Date(0).toISOString());\n\t}\n\n\t/** All recorded (model,intent) agreement buckets, optionally scoped to one model — report input. */\n\tgetAllIntentAgreements(modelRef?: string): ToolSelectionIntentAgreement[] {\n\t\tconst host = this.load().hosts[this.fingerprint().id];\n\t\treturn Object.values(host?.intentAgreement ?? {})\n\t\t\t.filter((record) => modelRef === undefined || record.modelRef === modelRef)\n\t\t\t.map((record) => ({ ...record }));\n\t}\n\n\trecordValidation(\n\t\tkey: ToolPerformanceKey,\n\t\toutcome: \"repaired\" | \"bounced\",\n\t\tat = new Date().toISOString(),\n\t): ToolPerformanceStats {\n\t\tconst file = this.load();\n\t\tconst host = this.hostData(file);\n\t\tconst storageKey = statKey(key);\n\t\tconst current = host.stats[storageKey] ?? emptyStats(key, at);\n\t\tconst next: ToolPerformanceStats = {\n\t\t\t...current,\n\t\t\trepairCount: current.repairCount + (outcome === \"repaired\" ? 1 : 0),\n\t\t\tbounceCount: current.bounceCount + (outcome === \"bounced\" ? 1 : 0),\n\t\t\tlastUsedAt: at,\n\t\t};\n\t\thost.stats[storageKey] = next;\n\t\thost.stats = trimStats(host.stats);\n\t\tthis.save(file);\n\t\treturn { ...next };\n\t}\n\n\trecordExecution(observation: ToolExecutionObservation): ToolPerformanceStats {\n\t\tconst at = observation.at ?? new Date().toISOString();\n\t\tconst file = this.load();\n\t\tconst host = this.hostData(file);\n\t\tconst storageKey = statKey(observation.key);\n\t\tconst current = host.stats[storageKey] ?? emptyStats(observation.key, at);\n\t\tconst latencyMs = finiteNonNegative(observation.latencyMs);\n\t\tconst inputTokenEstimate = finiteNonNegative(observation.inputTokenEstimate);\n\t\tconst outputTokenEstimate = finiteNonNegative(observation.outputTokenEstimate);\n\t\tconst next: ToolPerformanceStats = {\n\t\t\t...current,\n\t\t\talpha: current.alpha + (observation.success ? 1 : 0),\n\t\t\tbeta: current.beta + (observation.success ? 0 : 1),\n\t\t\tsampleCount: current.sampleCount + 1,\n\t\t\tlatencyEwmaMs: updateEwma(current.latencyEwmaMs, latencyMs),\n\t\t\tlatencyDeviationEwmaMs: updateDeviation(current.latencyEwmaMs, current.latencyDeviationEwmaMs, latencyMs),\n\t\t\tinputTokenEstimateEwma: updateEwma(current.inputTokenEstimateEwma, inputTokenEstimate),\n\t\t\toutputTokenEstimateEwma: updateEwma(current.outputTokenEstimateEwma, outputTokenEstimate),\n\t\t\tfailureCount: current.failureCount + (observation.success ? 0 : 1),\n\t\t\tlastUsedAt: at,\n\t\t};\n\t\thost.stats[storageKey] = next;\n\t\thost.stats = trimStats(host.stats);\n\t\thost.observations.push({\n\t\t\t...observation.selection,\n\t\t\tat,\n\t\t\tmodelRef: observation.key.modelRef,\n\t\t\tintentClass: observation.key.intentClass,\n\t\t\tactualTool: observation.key.tool,\n\t\t\tsucceeded: observation.success,\n\t\t\tranked: observation.selection.ranked.slice(0, 6),\n\t\t\tshortlist: observation.selection.shortlist.slice(0, 3),\n\t\t\tlatencyMs,\n\t\t\tinputTokenEstimate,\n\t\t\toutputTokenEstimate,\n\t\t});\n\t\tif (host.observations.length > MAX_OBSERVATIONS_PER_HOST) {\n\t\t\thost.observations = host.observations.slice(-MAX_OBSERVATIONS_PER_HOST);\n\t\t}\n\n\t\t// Observe-mode agreement: did the RAW ranking's top pick (before any actual-tool-only\n\t\t// eligibility gating) match what the model actually called? Recorded durably per\n\t\t// (model,intent), separate from the capped `observations` log above, so it survives trimming.\n\t\tconst predictedBest = observation.selection.ranked[0]?.tool;\n\t\tconst agreed = predictedBest !== undefined && predictedBest === observation.key.tool;\n\t\tconst agreementKey = intentAgreementKey(observation.key.modelRef, observation.key.intentClass);\n\t\tconst currentAgreement =\n\t\t\thost.intentAgreement[agreementKey] ??\n\t\t\temptyIntentAgreement(observation.key.modelRef, observation.key.intentClass, at);\n\t\thost.intentAgreement[agreementKey] = {\n\t\t\t...currentAgreement,\n\t\t\tsampleCount: currentAgreement.sampleCount + 1,\n\t\t\tagreementCount: currentAgreement.agreementCount + (agreed ? 1 : 0),\n\t\t\thintActiveSampleCount: currentAgreement.hintActiveSampleCount + (observation.hintActiveAtCallTime ? 1 : 0),\n\t\t\thintActiveAgreementCount:\n\t\t\t\tcurrentAgreement.hintActiveAgreementCount + (observation.hintActiveAtCallTime && agreed ? 1 : 0),\n\t\t\tlastUpdatedAt: at,\n\t\t};\n\t\thost.intentAgreement = trimIntentAgreement(host.intentAgreement);\n\n\t\tthis.save(file);\n\t\treturn { ...next };\n\t}\n\n\tgetMetrics(modelRef?: string): ToolSelectionMetrics {\n\t\tconst host = this.load().hosts[this.fingerprint().id];\n\t\tconst observations = (host?.observations ?? []).filter(\n\t\t\t(observation) => modelRef === undefined || observation.modelRef === modelRef,\n\t\t);\n\t\tconst firstTools = observations.filter((observation) => observation.firstTool);\n\t\tconst recommended = observations.filter((observation) => observation.disposition === \"recommend\");\n\t\tconst average = (values: Array<number | undefined>): number | undefined => {\n\t\t\tconst present = values.filter((value): value is number => value !== undefined);\n\t\t\treturn present.length > 0 ? present.reduce((sum, value) => sum + value, 0) / present.length : undefined;\n\t\t};\n\t\treturn {\n\t\t\tfirstToolAttempts: firstTools.length,\n\t\t\tfirstToolSuccesses: firstTools.filter((observation) => observation.succeeded).length,\n\t\t\twrongToolOrFailureCount: observations.filter(\n\t\t\t\t(observation) =>\n\t\t\t\t\t!observation.succeeded ||\n\t\t\t\t\t(observation.disposition === \"recommend\" && observation.recommendation !== observation.actualTool),\n\t\t\t).length,\n\t\t\trecommendationCount: recommended.length,\n\t\t\trecommendationMatchedCount: recommended.filter(\n\t\t\t\t(observation) => observation.recommendation === observation.actualTool,\n\t\t\t).length,\n\t\t\tshortlistCount: observations.filter((observation) => observation.disposition === \"shortlist\").length,\n\t\t\tabstentionCount: observations.filter((observation) => observation.disposition === \"abstain\").length,\n\t\t\taverageLatencyMs: average(observations.map((observation) => observation.latencyMs)),\n\t\t\taverageInputTokenEstimate: average(observations.map((observation) => observation.inputTokenEstimate)),\n\t\t\taverageOutputTokenEstimate: average(observations.map((observation) => observation.outputTokenEstimate)),\n\t\t};\n\t}\n\n\tgetObservations(modelRef?: string): ToolSelectionObservation[] {\n\t\tconst host = this.load().hosts[this.fingerprint().id];\n\t\treturn (host?.observations ?? [])\n\t\t\t.filter((observation) => modelRef === undefined || observation.modelRef === modelRef)\n\t\t\t.map((observation) => ({\n\t\t\t\t...observation,\n\t\t\t\tshortlist: [...observation.shortlist],\n\t\t\t\tranked: observation.ranked.map((candidate) => ({ ...candidate })),\n\t\t\t}));\n\t}\n}\n"]}