@phuetz/code-buddy 2.0.0 → 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (421) hide show
  1. package/README.fr.md +56 -0
  2. package/README.md +74 -16
  3. package/codebuddy-runtime.json +5 -5
  4. package/dist/agent/codebuddy-agent.d.ts +18 -0
  5. package/dist/agent/codebuddy-agent.js +45 -8
  6. package/dist/agent/execution/agent-executor.d.ts +0 -9
  7. package/dist/agent/execution/agent-executor.js +176 -12
  8. package/dist/agent/execution/context-pipeline.d.ts +9 -0
  9. package/dist/agent/execution/context-pipeline.js +64 -2
  10. package/dist/agent/execution/tool-loop-guard.d.ts +76 -0
  11. package/dist/agent/execution/tool-loop-guard.js +165 -0
  12. package/dist/agent/execution/tool-selection-strategy.d.ts +7 -0
  13. package/dist/agent/execution/tool-selection-strategy.js +32 -2
  14. package/dist/agent/facades/session-facade.d.ts +0 -6
  15. package/dist/agent/facades/session-facade.js +0 -45
  16. package/dist/agent/hermes-claw-migrate.d.ts +0 -21
  17. package/dist/agent/hermes-claw-migrate.js +9 -4
  18. package/dist/agent/hermes-protocol-gateways.js +8 -1
  19. package/dist/agent/repo-profiler.js +17 -4
  20. package/dist/agent/self-improvement/authored-tool-runtime.js +10 -2
  21. package/dist/agent/self-improvement/evolution/ast-novelty.d.ts +5 -0
  22. package/dist/agent/self-improvement/evolution/ast-novelty.js +58 -23
  23. package/dist/agent/self-improvement/evolution/code-variant-store.d.ts +3 -0
  24. package/dist/agent/self-improvement/evolution/evolution-engine.js +2 -0
  25. package/dist/agent/self-improvement/evolution/protected-paths.js +6 -0
  26. package/dist/agent/self-improvement/evolution/variant-fitness.d.ts +13 -2
  27. package/dist/agent/self-improvement/evolution/variant-fitness.js +184 -58
  28. package/dist/agent/self-improvement/evolution/worktree-scorer.d.ts +2 -0
  29. package/dist/agent/self-improvement/evolution/worktree-scorer.js +15 -0
  30. package/dist/agent/self-improvement/evolutionary-archive.d.ts +2 -1
  31. package/dist/agent/self-improvement/evolutionary-archive.js +34 -5
  32. package/dist/agent/self-improvement/proposal-store.d.ts +1 -0
  33. package/dist/agent/self-improvement/proposal-store.js +79 -17
  34. package/dist/agent/self-improvement/skill-apply-journal.d.ts +44 -0
  35. package/dist/agent/self-improvement/skill-apply-journal.js +178 -0
  36. package/dist/agent/self-improvement/skill-behavior-benchmark.d.ts +3 -0
  37. package/dist/agent/self-improvement/skill-behavior-benchmark.js +19 -0
  38. package/dist/agent/self-improvement/skill-behavior-gate.d.ts +34 -0
  39. package/dist/agent/self-improvement/skill-behavior-gate.js +96 -0
  40. package/dist/agent/self-improvement/skill-engine.d.ts +22 -0
  41. package/dist/agent/self-improvement/skill-engine.js +385 -13
  42. package/dist/agent/self-improvement/skill-gate.d.ts +2 -0
  43. package/dist/agent/self-improvement/skill-gate.js +17 -2
  44. package/dist/agent/self-improvement/skill-mutator.d.ts +6 -1
  45. package/dist/agent/self-improvement/skill-mutator.js +51 -15
  46. package/dist/agent/self-improvement/skill-types.d.ts +2 -1
  47. package/dist/agent/self-improvement/types.d.ts +11 -0
  48. package/dist/agent/tool-handler.d.ts +1 -1
  49. package/dist/agent/tool-handler.js +28 -9
  50. package/dist/auth/profile-manager.js +1 -1
  51. package/dist/channels/companion-channel-turn.d.ts +53 -4
  52. package/dist/channels/companion-channel-turn.js +218 -16
  53. package/dist/channels/slash-parity.d.ts +4 -6
  54. package/dist/channels/slash-parity.js +9 -44
  55. package/dist/cli/session-commands.d.ts +9 -0
  56. package/dist/cli/session-commands.js +45 -5
  57. package/dist/cli/session-picker.d.ts +43 -0
  58. package/dist/cli/session-picker.js +128 -0
  59. package/dist/codebuddy/a2a-call-tool-defs.d.ts +26 -0
  60. package/dist/codebuddy/a2a-call-tool-defs.js +12 -0
  61. package/dist/codebuddy/fleet-tool-defs.d.ts +2 -0
  62. package/dist/codebuddy/fleet-tool-defs.js +55 -0
  63. package/dist/codebuddy/providers/chatgpt-headers.d.ts +18 -0
  64. package/dist/codebuddy/providers/chatgpt-headers.js +37 -0
  65. package/dist/codebuddy/providers/provider-chatgpt-responses.d.ts +2 -1
  66. package/dist/codebuddy/providers/provider-chatgpt-responses.js +10 -23
  67. package/dist/codebuddy/providers/provider-openai-compat.d.ts +7 -0
  68. package/dist/codebuddy/providers/provider-openai-compat.js +29 -1
  69. package/dist/codebuddy/ragchat-tool-defs.d.ts +31 -0
  70. package/dist/codebuddy/ragchat-tool-defs.js +19 -0
  71. package/dist/codebuddy/resource-catalog-tool-defs.d.ts +28 -0
  72. package/dist/codebuddy/resource-catalog-tool-defs.js +18 -0
  73. package/dist/codebuddy/tool-definitions/agent-tools.js +2 -2
  74. package/dist/codebuddy/tool-definitions/code-exec-tools.js +5 -1
  75. package/dist/codebuddy/tool-definitions/cron-tools.js +6 -0
  76. package/dist/codebuddy/tool-definitions/self-describe-tools.js +6 -1
  77. package/dist/codebuddy/tools.d.ts +0 -6
  78. package/dist/codebuddy/tools.js +25 -3
  79. package/dist/commands/cli/daemon-commands.js +3 -1
  80. package/dist/commands/cli/evolve-command.js +21 -5
  81. package/dist/commands/cli/fleet-collaboration-commands.d.ts +2 -0
  82. package/dist/commands/cli/fleet-collaboration-commands.js +57 -0
  83. package/dist/commands/cli/fleet-commands.js +29 -0
  84. package/dist/commands/cli/fleet-mission-commands.d.ts +3 -0
  85. package/dist/commands/cli/fleet-mission-commands.js +72 -0
  86. package/dist/commands/cli/fleet-rooms-commands.d.ts +10 -0
  87. package/dist/commands/cli/fleet-rooms-commands.js +289 -0
  88. package/dist/commands/cli/tools-commands.js +2 -1
  89. package/dist/commands/cli/triage-command.d.ts +8 -0
  90. package/dist/commands/cli/triage-command.js +72 -0
  91. package/dist/commands/cli/utility-commands.js +25 -1
  92. package/dist/commands/client-dispatcher.d.ts +7 -0
  93. package/dist/commands/client-dispatcher.js +73 -21
  94. package/dist/commands/cron-cli/index.d.ts +3 -1
  95. package/dist/commands/cron-cli/index.js +25 -0
  96. package/dist/commands/device-auth.d.ts +10 -0
  97. package/dist/commands/device-auth.js +91 -0
  98. package/dist/commands/enhanced-command-handler.d.ts +9 -0
  99. package/dist/commands/enhanced-command-handler.js +28 -6
  100. package/dist/commands/handlers/agent-handlers.d.ts +1 -1
  101. package/dist/commands/handlers/agent-handlers.js +4 -4
  102. package/dist/commands/handlers/channel-handlers.js +53 -2
  103. package/dist/commands/handlers/export-handlers.d.ts +3 -2
  104. package/dist/commands/handlers/export-handlers.js +55 -28
  105. package/dist/commands/handlers/extra-handlers.js +5 -1
  106. package/dist/commands/handlers/grill-me-handler.js +26 -18
  107. package/dist/commands/handlers/memory-handlers.d.ts +6 -2
  108. package/dist/commands/handlers/memory-handlers.js +35 -15
  109. package/dist/commands/handlers/missing-handlers.d.ts +2 -2
  110. package/dist/commands/handlers/missing-handlers.js +12 -4
  111. package/dist/commands/handlers/resources-handler.d.ts +12 -0
  112. package/dist/commands/handlers/resources-handler.js +52 -0
  113. package/dist/commands/handlers/starter-handlers.d.ts +1 -1
  114. package/dist/commands/handlers/starter-handlers.js +32 -2
  115. package/dist/commands/handlers/switch-handler.d.ts +1 -1
  116. package/dist/commands/handlers/switch-handler.js +15 -0
  117. package/dist/commands/handlers/test-handlers.js +4 -86
  118. package/dist/commands/handlers/ui-handlers.js +11 -5
  119. package/dist/commands/login-chatgpt.d.ts +8 -0
  120. package/dist/commands/login-chatgpt.js +17 -0
  121. package/dist/commands/mcp-import.d.ts +11 -0
  122. package/dist/commands/mcp-import.js +67 -0
  123. package/dist/commands/mcp-invoke.d.ts +2 -0
  124. package/dist/commands/mcp-invoke.js +68 -0
  125. package/dist/commands/mcp.d.ts +13 -0
  126. package/dist/commands/mcp.js +66 -21
  127. package/dist/commands/pipeline.d.ts +7 -1
  128. package/dist/commands/pipeline.js +104 -7
  129. package/dist/commands/resources.d.ts +2 -0
  130. package/dist/commands/resources.js +64 -0
  131. package/dist/commands/skills-cli/index.js +35 -12
  132. package/dist/commands/slash/builtin-commands.js +16 -3
  133. package/dist/commands/slash/index.d.ts +3 -1
  134. package/dist/commands/slash/index.js +2 -0
  135. package/dist/commands/slash/surfaces.d.ts +53 -0
  136. package/dist/commands/slash/surfaces.js +177 -0
  137. package/dist/commands/slash/types.d.ts +17 -0
  138. package/dist/commands/try.d.ts +1 -1
  139. package/dist/commands/try.js +1 -1
  140. package/dist/companion/companion-identity.d.ts +43 -0
  141. package/dist/companion/companion-identity.js +136 -0
  142. package/dist/companion/companion-toolset.d.ts +66 -0
  143. package/dist/companion/companion-toolset.js +368 -0
  144. package/dist/companion/companion-turn.d.ts +10 -2
  145. package/dist/companion/companion-turn.js +38 -1
  146. package/dist/config/code-exec-policy.d.ts +25 -0
  147. package/dist/config/code-exec-policy.js +39 -0
  148. package/dist/config/model-tools.d.ts +9 -0
  149. package/dist/config/model-tools.js +103 -0
  150. package/dist/context/context-manager-v2.js +2 -2
  151. package/dist/context/lm-resizer-compressor.js +3 -2
  152. package/dist/context/lm-resizer-diagnostics.d.ts +9 -0
  153. package/dist/context/lm-resizer-diagnostics.js +30 -0
  154. package/dist/context/tool-output-masking.js +3 -4
  155. package/dist/curator/curator.d.ts +6 -0
  156. package/dist/curator/curator.js +53 -3
  157. package/dist/daemon/cron-agent-bridge.d.ts +6 -0
  158. package/dist/daemon/cron-agent-bridge.js +31 -4
  159. package/dist/doctor/index.d.ts +11 -2
  160. package/dist/doctor/index.js +84 -21
  161. package/dist/doctor/integrations.d.ts +197 -0
  162. package/dist/doctor/integrations.js +241 -0
  163. package/dist/doctor/triage.d.ts +108 -0
  164. package/dist/doctor/triage.js +304 -0
  165. package/dist/errors/crash-handler.js +2 -0
  166. package/dist/fleet/collaboration.d.ts +44 -0
  167. package/dist/fleet/collaboration.js +170 -0
  168. package/dist/fleet/fleet-listener.d.ts +1 -0
  169. package/dist/fleet/fleet-listener.js +87 -46
  170. package/dist/fleet/fleet-registry.d.ts +2 -0
  171. package/dist/fleet/resource-catalog.d.ts +245 -0
  172. package/dist/fleet/resource-catalog.js +211 -0
  173. package/dist/fleet/rooms/room-access.d.ts +138 -0
  174. package/dist/fleet/rooms/room-access.js +196 -0
  175. package/dist/fleet/rooms/room-client.d.ts +158 -0
  176. package/dist/fleet/rooms/room-client.js +685 -0
  177. package/dist/fleet/rooms/room-event.d.ts +139 -0
  178. package/dist/fleet/rooms/room-event.js +330 -0
  179. package/dist/fleet/rooms/room-filter.d.ts +48 -0
  180. package/dist/fleet/rooms/room-filter.js +152 -0
  181. package/dist/fleet/rooms/room-hub.d.ts +154 -0
  182. package/dist/fleet/rooms/room-hub.js +363 -0
  183. package/dist/fleet/rooms/room-identity.d.ts +26 -0
  184. package/dist/fleet/rooms/room-identity.js +82 -0
  185. package/dist/fleet/rooms/room-lock.d.ts +34 -0
  186. package/dist/fleet/rooms/room-lock.js +180 -0
  187. package/dist/fleet/rooms/room-observations.d.ts +7 -0
  188. package/dist/fleet/rooms/room-observations.js +61 -0
  189. package/dist/fleet/rooms/room-server.d.ts +28 -0
  190. package/dist/fleet/rooms/room-server.js +75 -0
  191. package/dist/fleet/rooms/room-store.d.ts +119 -0
  192. package/dist/fleet/rooms/room-store.js +448 -0
  193. package/dist/fleet/rooms/room-ws-bridge.d.ts +27 -0
  194. package/dist/fleet/rooms/room-ws-bridge.js +137 -0
  195. package/dist/fleet/sensory-mission-bridge.d.ts +42 -0
  196. package/dist/fleet/sensory-mission-bridge.js +136 -0
  197. package/dist/git/worktree-sessions.js +1 -0
  198. package/dist/harness/contract.d.ts +4 -4
  199. package/dist/harness/fleet-supervisor.d.ts +16 -0
  200. package/dist/harness/fleet-supervisor.js +64 -0
  201. package/dist/harness/index.d.ts +4 -0
  202. package/dist/harness/index.js +4 -0
  203. package/dist/harness/mission-runner.d.ts +3 -0
  204. package/dist/harness/mission-runner.js +53 -0
  205. package/dist/harness/mission-store.d.ts +93 -0
  206. package/dist/harness/mission-store.js +297 -0
  207. package/dist/harness/tool-harness.d.ts +56 -0
  208. package/dist/harness/tool-harness.js +153 -0
  209. package/dist/hooks/use-enhanced-input.js +48 -4
  210. package/dist/hooks/use-input-handler.d.ts +1 -0
  211. package/dist/hooks/use-input-handler.js +82 -22
  212. package/dist/hooks/use-input-history.js +1 -0
  213. package/dist/identity/lisa-introspection.js +5 -2
  214. package/dist/identity/operational-self-model.d.ts +2 -0
  215. package/dist/identity/operational-self-model.js +4 -0
  216. package/dist/index.js +52 -6
  217. package/dist/mcp/client.d.ts +11 -0
  218. package/dist/mcp/client.js +68 -3
  219. package/dist/mcp/config.d.ts +20 -0
  220. package/dist/mcp/config.js +121 -8
  221. package/dist/mcp/import-normalize.d.ts +12 -0
  222. package/dist/mcp/import-normalize.js +114 -0
  223. package/dist/mcp/mcp-oauth-constants.d.ts +8 -0
  224. package/dist/mcp/mcp-oauth-constants.js +9 -0
  225. package/dist/mcp/mcp-oauth-provider.d.ts +80 -0
  226. package/dist/mcp/mcp-oauth-provider.js +241 -0
  227. package/dist/mcp/mcp-oauth.d.ts +60 -1
  228. package/dist/mcp/mcp-oauth.js +241 -65
  229. package/dist/mcp/transports.d.ts +14 -5
  230. package/dist/mcp/transports.js +112 -101
  231. package/dist/mcp/types.d.ts +5 -0
  232. package/dist/media/comfyui-recipe-contract.d.ts +7 -7
  233. package/dist/memory/facts-memory.d.ts +11 -1
  234. package/dist/memory/facts-memory.js +47 -8
  235. package/dist/memory/memory-candidate-queue.d.ts +2 -2
  236. package/dist/memory/memory-candidate-queue.js +6 -6
  237. package/dist/memory/persistent-memory.d.ts +13 -8
  238. package/dist/memory/persistent-memory.js +77 -30
  239. package/dist/nodes/device-node.js +5 -9
  240. package/dist/observability/mobile-supervision-gateway-contract.js +20 -7
  241. package/dist/observability/run-event-writer.d.ts +21 -0
  242. package/dist/observability/run-event-writer.js +81 -0
  243. package/dist/observability/run-store.d.ts +5 -0
  244. package/dist/observability/run-store.js +18 -13
  245. package/dist/observability/run-trajectory.js +1 -1
  246. package/dist/optimization/prompt-cache.d.ts +19 -2
  247. package/dist/optimization/prompt-cache.js +41 -6
  248. package/dist/orchestration/orchestrator.d.ts +21 -0
  249. package/dist/orchestration/orchestrator.js +122 -15
  250. package/dist/persistence/session-content.d.ts +8 -0
  251. package/dist/persistence/session-content.js +76 -0
  252. package/dist/persistence/session-handoff.d.ts +64 -0
  253. package/dist/persistence/session-handoff.js +80 -0
  254. package/dist/persistence/session-history.d.ts +4 -0
  255. package/dist/persistence/session-history.js +38 -0
  256. package/dist/persistence/session-store.d.ts +9 -0
  257. package/dist/persistence/session-store.js +48 -20
  258. package/dist/personas/persona-manager.js +17 -1
  259. package/dist/plugins/code-explorer/CodeExplorerManager.d.ts +10 -4
  260. package/dist/plugins/code-explorer/CodeExplorerManager.js +69 -16
  261. package/dist/plugins/plugin-manager.js +10 -1
  262. package/dist/prompts/prompt-manager.js +11 -4
  263. package/dist/prompts/system-base.js +6 -2
  264. package/dist/protocols/a2a/codebuddy-executor.d.ts +11 -2
  265. package/dist/protocols/a2a/codebuddy-executor.js +9 -8
  266. package/dist/protocols/a2a/index.d.ts +1 -1
  267. package/dist/protocols/a2a/jsonrpc-v1.d.ts +41 -0
  268. package/dist/protocols/a2a/jsonrpc-v1.js +145 -0
  269. package/dist/protocols/a2a/peer-config.d.ts +20 -0
  270. package/dist/protocols/a2a/peer-config.js +55 -0
  271. package/dist/providers/codex-oauth.js +30 -6
  272. package/dist/providers/model-provider-compat.js +2 -1
  273. package/dist/sandbox/os-sandbox.d.ts +1 -1
  274. package/dist/scheduler/cron-scheduler.d.ts +15 -2
  275. package/dist/scheduler/cron-scheduler.js +35 -4
  276. package/dist/scheduler/job-notepad.d.ts +69 -0
  277. package/dist/scheduler/job-notepad.js +339 -0
  278. package/dist/security/audit-logger.d.ts +1 -1
  279. package/dist/security/compute-confinement.d.ts +4 -0
  280. package/dist/security/compute-confinement.js +89 -0
  281. package/dist/security/session-encryption.d.ts +4 -0
  282. package/dist/security/session-encryption.js +31 -0
  283. package/dist/sensory/voice-loop.d.ts +17 -0
  284. package/dist/sensory/voice-loop.js +72 -15
  285. package/dist/server/auth/device-session-context.d.ts +9 -0
  286. package/dist/server/auth/device-session-context.js +13 -0
  287. package/dist/server/auth/device-store.d.ts +89 -0
  288. package/dist/server/auth/device-store.js +261 -0
  289. package/dist/server/auth/device-token.d.ts +3 -0
  290. package/dist/server/auth/device-token.js +6 -0
  291. package/dist/server/auth/jwt.js +4 -0
  292. package/dist/server/index.d.ts +0 -10
  293. package/dist/server/index.js +28 -0
  294. package/dist/server/middleware/auth.js +3 -2
  295. package/dist/server/routes/a2a-jsonrpc.d.ts +9 -0
  296. package/dist/server/routes/a2a-jsonrpc.js +66 -0
  297. package/dist/server/routes/a2a-protocol.d.ts +12 -0
  298. package/dist/server/routes/a2a-protocol.js +1 -1
  299. package/dist/server/routes/device-auth.d.ts +4 -0
  300. package/dist/server/routes/device-auth.js +40 -0
  301. package/dist/server/types.d.ts +4 -0
  302. package/dist/server/websocket/handler.d.ts +13 -0
  303. package/dist/server/websocket/handler.js +102 -17
  304. package/dist/services/prompt-builder.d.ts +2 -0
  305. package/dist/services/prompt-builder.js +16 -6
  306. package/dist/services/runtime-settings-context.d.ts +81 -0
  307. package/dist/services/runtime-settings-context.js +79 -0
  308. package/dist/skills/executor.js +2 -0
  309. package/dist/skills/hub.js +16 -14
  310. package/dist/skills/local-inventory.d.ts +21 -0
  311. package/dist/skills/local-inventory.js +72 -0
  312. package/dist/skills/skill-loader.js +2 -2
  313. package/dist/skills/skill-usage-store.d.ts +49 -0
  314. package/dist/skills/skill-usage-store.js +121 -0
  315. package/dist/testing/ai-integration-tests.js +8 -5
  316. package/dist/themes/theme-manager.d.ts +12 -0
  317. package/dist/themes/theme-manager.js +29 -0
  318. package/dist/themes/theme-schema.d.ts +24 -24
  319. package/dist/tools/a2a-call-tool.d.ts +34 -0
  320. package/dist/tools/a2a-call-tool.js +75 -0
  321. package/dist/tools/bash/streaming-executor.d.ts +1 -0
  322. package/dist/tools/bash/streaming-executor.js +78 -90
  323. package/dist/tools/code-exec-preflight-runner.d.ts +41 -0
  324. package/dist/tools/code-exec-preflight-runner.js +254 -0
  325. package/dist/tools/code-exec-preflight.d.ts +44 -0
  326. package/dist/tools/code-exec-preflight.js +501 -0
  327. package/dist/tools/code-exec-tool.d.ts +48 -1
  328. package/dist/tools/code-exec-tool.js +191 -47
  329. package/dist/tools/comment-watcher.js +29 -12
  330. package/dist/tools/core-code-inspection.d.ts +56 -0
  331. package/dist/tools/core-code-inspection.js +86 -0
  332. package/dist/tools/create-skill-tool.d.ts +2 -3
  333. package/dist/tools/create-skill-tool.js +16 -75
  334. package/dist/tools/cronjob-tool.d.ts +1 -0
  335. package/dist/tools/cronjob-tool.js +1 -0
  336. package/dist/tools/env-doctor-tool.d.ts +2 -0
  337. package/dist/tools/env-doctor-tool.js +4 -3
  338. package/dist/tools/execute-code-rpc-invoker.d.ts +1 -1
  339. package/dist/tools/execute-code-rpc-invoker.js +35 -19
  340. package/dist/tools/execute-code-runner.d.ts +2 -0
  341. package/dist/tools/execute-code-runner.js +52 -17
  342. package/dist/tools/fleet-room-tool.d.ts +104 -0
  343. package/dist/tools/fleet-room-tool.js +124 -0
  344. package/dist/tools/integration-tool-hints.d.ts +4 -0
  345. package/dist/tools/integration-tool-hints.js +19 -0
  346. package/dist/tools/list-peers-tool.d.ts +3 -0
  347. package/dist/tools/list-peers-tool.js +11 -4
  348. package/dist/tools/media-generation-tool.d.ts +16 -1
  349. package/dist/tools/media-generation-tool.js +341 -9
  350. package/dist/tools/merge-conflict-tool.js +53 -25
  351. package/dist/tools/metadata.d.ts +1 -5
  352. package/dist/tools/metadata.js +30 -28
  353. package/dist/tools/peer-tool-invoke-tool.d.ts +61 -0
  354. package/dist/tools/peer-tool-invoke-tool.js +396 -0
  355. package/dist/tools/ragchat-tool.d.ts +39 -0
  356. package/dist/tools/ragchat-tool.js +105 -0
  357. package/dist/tools/registry/fleet-tools.d.ts +21 -3
  358. package/dist/tools/registry/fleet-tools.js +137 -4
  359. package/dist/tools/registry/index.d.ts +1 -1
  360. package/dist/tools/registry/index.js +2 -2
  361. package/dist/tools/registry/knowledge-tools.d.ts +2 -2
  362. package/dist/tools/registry/knowledge-tools.js +2 -2
  363. package/dist/tools/registry/lessons-tools.d.ts +7 -0
  364. package/dist/tools/registry/lessons-tools.js +43 -3
  365. package/dist/tools/registry/memory-tools.d.ts +1 -1
  366. package/dist/tools/registry/memory-tools.js +8 -8
  367. package/dist/tools/registry/research-tools.d.ts +1 -0
  368. package/dist/tools/registry/research-tools.js +4 -13
  369. package/dist/tools/registry/self-describe-tools.d.ts +1 -1
  370. package/dist/tools/registry/self-describe-tools.js +43 -30
  371. package/dist/tools/resource-catalog-tool.d.ts +38 -0
  372. package/dist/tools/resource-catalog-tool.js +46 -0
  373. package/dist/tools/skills-inspection-tool.js +30 -1
  374. package/dist/tools/stream-tool-output.d.ts +3 -0
  375. package/dist/tools/stream-tool-output.js +33 -0
  376. package/dist/tools/tool-call-scheduler.d.ts +10 -0
  377. package/dist/tools/tool-call-scheduler.js +58 -0
  378. package/dist/tools/tool-effect.d.ts +5 -0
  379. package/dist/tools/tool-effect.js +26 -0
  380. package/dist/tools/tool-search.d.ts +12 -11
  381. package/dist/tools/tool-search.js +67 -26
  382. package/dist/tools/types.d.ts +6 -0
  383. package/dist/ui/components/ChatHistory.d.ts +1 -1
  384. package/dist/ui/components/ChatHistory.js +2 -2
  385. package/dist/ui/components/ChatInput.d.ts +2 -1
  386. package/dist/ui/components/ChatInput.js +42 -88
  387. package/dist/ui/components/ChatInterface.d.ts +4 -2
  388. package/dist/ui/components/ChatInterface.js +27 -63
  389. package/dist/ui/components/CommandSuggestions.d.ts +0 -1
  390. package/dist/ui/components/CommandSuggestions.js +4 -6
  391. package/dist/ui/components/FileAutocomplete.js +1 -1
  392. package/dist/ui/components/KeyboardHelp.js +8 -6
  393. package/dist/ui/components/ModelSelection.js +6 -5
  394. package/dist/ui/components/StatusBar.d.ts +2 -1
  395. package/dist/ui/components/StatusBar.js +8 -7
  396. package/dist/ui/context/theme-context.js +3 -2
  397. package/dist/ui/index.d.ts +1 -1
  398. package/dist/ui/index.js +1 -1
  399. package/dist/ui/utils/markdown-renderer.js +4 -13
  400. package/dist/utils/atomic-write.d.ts +22 -0
  401. package/dist/utils/atomic-write.js +29 -0
  402. package/dist/utils/bounded-output.d.ts +16 -0
  403. package/dist/utils/bounded-output.js +63 -0
  404. package/dist/utils/config-validation/schema.d.ts +2 -2
  405. package/dist/utils/device-store-file.d.ts +4 -0
  406. package/dist/utils/device-store-file.js +40 -0
  407. package/dist/utils/export-manager.d.ts +9 -0
  408. package/dist/utils/export-manager.js +19 -0
  409. package/dist/utils/first-use-hints.d.ts +25 -0
  410. package/dist/utils/first-use-hints.js +58 -0
  411. package/dist/utils/graceful-shutdown.d.ts +8 -0
  412. package/dist/utils/graceful-shutdown.js +23 -7
  413. package/dist/utils/history-manager.js +1 -1
  414. package/dist/utils/update-notifier.js +16 -18
  415. package/dist/wizard/environment-detection.d.ts +5 -1
  416. package/dist/wizard/environment-detection.js +49 -7
  417. package/dist/wizard/onboarding.d.ts +5 -1
  418. package/dist/wizard/onboarding.js +90 -30
  419. package/dist/wizard/provider-onboarding.d.ts +2 -0
  420. package/dist/wizard/provider-onboarding.js +55 -17
  421. package/package.json +7 -3
@@ -1,3 +1,4 @@
1
+ import { bindFactsMemorySession } from '../../memory/facts-memory.js';
1
2
  /**
2
3
  * Agent Executor Module
3
4
  *
@@ -7,10 +8,12 @@
7
8
  *
8
9
  * @module agent/execution
9
10
  */
11
+ import { BoundedOutput } from '../../utils/bounded-output.js';
10
12
  import { resolveFirstTokenStallTimeoutMs, resolveStallTimeoutMs, withStallGuard } from "../../utils/stream-stall-guard.js";
11
13
  import { startHeadlessPromptProgress } from "../../cli/headless-prompt-progress.js";
12
14
  import { HEADLESS_LOCAL_COMPACT_ALWAYS_INCLUDE, HEADLESS_LOCAL_COMPACT_MAX_TOOLS, isHeadlessLocalPromptCompact, } from "../../config/headless-local-prompt.js";
13
15
  import { normalizeHallucinatedLocalToolCall } from "../tool-handler.js";
16
+ import { mergeAlwaysInclude } from "./tool-selection-strategy.js";
14
17
  import { ContextCompactionError } from "../../context/context-manager-v2.js";
15
18
  import { logger } from "../../utils/logger.js";
16
19
  import { getErrorMessage } from "../../errors/index.js";
@@ -27,6 +30,9 @@ import { compress as tokenJuice, isTokenJuiceEnabled, JUICE_MIN_CHARS } from "..
27
30
  import { formatToolResultForRecovery, getRestorableCompressor, } from "../../context/restorable-compression.js";
28
31
  import { recordCompactionFork } from "../../context/compaction-fork.js";
29
32
  import { getActiveRunStore } from "../../observability/run-store.js";
33
+ import { ToolLoopGuard } from "./tool-loop-guard.js";
34
+ import { getGlobalEventBus } from "../../events/event-bus.js";
35
+ import { takeFirstUseHint } from "../../utils/first-use-hints.js";
30
36
  import { getTurnMetricsRecorder } from '../../observability/turn-metrics.js';
31
37
  import { shouldCompactBeforeToolExec, estimateToolResultTokens } from "../../context/proactive-compaction.js";
32
38
  import { formatTokenUsage, estimateCost } from "../../utils/token-display.js";
@@ -35,7 +41,7 @@ import { getModelToolConfig } from "../../config/model-tools.js";
35
41
  import { getLatencyOptimizer, getStreamingOptimizer } from "../../optimization/latency-optimizer.js";
36
42
  import { buildTextEmotionalPresenceContext } from "../../companion/reply-augment.js";
37
43
  import { guardRelationshipReply, SAFE_RELATIONSHIP_REPAIR, } from "../../conversation/relationship-safety.js";
38
- import { classifyLisaIntrospection, guardLisaOperationalSelfInspectionReply, isLisaEvolutionRequest, renderLisaOperationalSelfResponse, } from '../../identity/lisa-introspection.js';
44
+ import { classifyLisaIntrospection, guardLisaOperationalSelfInspectionReply, isLisaEvolutionRequest, isLisaSubjectiveConsciousnessQuestion, renderLisaOperationalSelfResponse, } from '../../identity/lisa-introspection.js';
39
45
  import { withLlmStreamRetry } from '../../codebuddy/llm-retry.js';
40
46
  import { getStreamingAdapter } from '../../tools/streaming-adapter.js';
41
47
  import { notify } from '../proactive/notification-default-sink.js';
@@ -467,16 +473,28 @@ export class AgentExecutor {
467
473
  /** Execute one tool and buffer optional streaming-adapter output. */
468
474
  async executeToolForBatch(toolCall, executionExtra, signal, startedAt = Date.now()) {
469
475
  const streamChunks = [];
476
+ const streamPreview = new BoundedOutput(256 * 1024);
477
+ let collapsed = false;
478
+ const appendStreamChunk = (chunk) => {
479
+ if (!chunk || signal?.aborted)
480
+ return;
481
+ streamPreview.append(chunk);
482
+ collapsed ||= streamPreview.omittedBytes > 0 || streamChunks.length >= 1024;
483
+ if (collapsed)
484
+ streamChunks.splice(0, streamChunks.length, streamPreview.text());
485
+ else
486
+ streamChunks.push(chunk);
487
+ };
470
488
  const extraWithSignal = signal
471
489
  ? { ...(executionExtra ?? {}), abortSignal: signal }
472
490
  : executionExtra;
473
491
  const execute = async () => {
474
- const streamingTools = new Set(['bash', 'reason', 'generate_document']);
492
+ const streamingTools = new Set(['bash', 'reason', 'generate_document', 'code_exec']);
475
493
  if (streamingTools.has(toolCall.function.name)) {
476
494
  const generator = this.deps.toolHandler.executeToolStreaming(toolCall, extraWithSignal);
477
495
  let generated = await generator.next();
478
496
  while (!generated.done) {
479
- streamChunks.push(generated.value);
497
+ appendStreamChunk(generated.value);
480
498
  generated = await generator.next();
481
499
  }
482
500
  return {
@@ -486,7 +504,7 @@ export class AgentExecutor {
486
504
  }
487
505
  const streamingAdapter = getStreamingAdapter();
488
506
  if (streamingAdapter.supportsStreaming(toolCall.function.name)) {
489
- const result = await streamingAdapter.wrapWithStreaming(toolCall.function.name, () => this.executeToolViaLane(toolCall, extraWithSignal), (chunk) => streamChunks.push(chunk));
507
+ const result = await streamingAdapter.wrapWithStreaming(toolCall.function.name, () => this.executeToolViaLane(toolCall, extraWithSignal), appendStreamChunk);
490
508
  return { result, streamChunks };
491
509
  }
492
510
  return {
@@ -740,7 +758,7 @@ export class AgentExecutor {
740
758
  const operationId = getLatencyOptimizer().startOperation('assistant_turn', startedAt);
741
759
  let recordedFirstVisibleResponse = false;
742
760
  try {
743
- for await (const event of this.runTurnLoop(message, history, messages, abortController, transientContext, relationshipSafety, surface, introspectionText)) {
761
+ for await (const event of bindFactsMemorySession(this.deps.client, this.runTurnLoop(message, history, messages, abortController, transientContext, relationshipSafety, surface, introspectionText))) {
744
762
  if (!recordedFirstVisibleResponse &&
745
763
  (event.type === 'content' || event.type === 'reasoning' || event.type === 'tool_calls')) {
746
764
  recordedFirstVisibleResponse = true;
@@ -783,7 +801,9 @@ export class AgentExecutor {
783
801
  const introspectionIntent = classifyLisaIntrospection(introspectionTextForTurn);
784
802
  const readOnlySelfInspection = introspectionIntent === 'describe' || introspectionIntent === 'inspect';
785
803
  const guardGenerativeSelfInspection = introspectionIntent === 'improve';
786
- const isolatedSharedHost = surface === 'http';
804
+ const codeResearch = surface === 'cli' && introspectionIntent === 'inspect' &&
805
+ !isLisaSubjectiveConsciousnessQuestion(introspectionTextForTurn);
806
+ const isolatedSharedHost = surface === 'http' || codeResearch;
787
807
  const turnCwd = typeof this.deps.toolHandler.getWorkingDirectory === 'function'
788
808
  ? this.deps.toolHandler.getWorkingDirectory()
789
809
  : process.cwd();
@@ -806,8 +826,9 @@ export class AgentExecutor {
806
826
  let permissionMode;
807
827
  let providerName;
808
828
  let operationalRobotName;
809
- if (introspectionIntent !== null) {
810
- operationalRobotName = await this.getOperationalRobotName();
829
+ if (introspectionIntent !== null || surface === 'cli') {
830
+ if (introspectionIntent !== null)
831
+ operationalRobotName = await this.getOperationalRobotName();
811
832
  try {
812
833
  const { getPermissionModeManager } = await import('../../security/permission-modes.js');
813
834
  permissionMode = getPermissionModeManager().getMode();
@@ -822,7 +843,7 @@ export class AgentExecutor {
822
843
  // Legacy/test clients may not expose provider metadata.
823
844
  }
824
845
  }
825
- if (readOnlySelfInspection) {
846
+ if (readOnlySelfInspection && !codeResearch) {
826
847
  if (isLisaEvolutionRequest(introspectionTextForTurn)) {
827
848
  const { formatEvolutionNotesForVoice, queryEvolutionNotes, readEvolutionNotes, } = await import('../../self-model/evolution-notes.js');
828
849
  const since = introspectionTextForTurn.match(/\b20\d{2}-\d{2}-\d{2}\b/)?.[0];
@@ -850,6 +871,8 @@ export class AgentExecutor {
850
871
  // every strict describe/inspect turn. No provider, plugin or generic
851
872
  // workspace tool participates in this path.
852
873
  const { buildOperationalSelfModel } = await import('../../identity/operational-self-model.js');
874
+ const { getRuntimeSettingsSnapshot } = await import('../../services/runtime-settings-context.js');
875
+ const activeSettings = getRuntimeSettingsSnapshot({ surface, model: this.deps.client.getCurrentModel(), provider: providerName, maxToolRounds: this.config.maxToolRounds });
853
876
  const selfModel = buildOperationalSelfModel({
854
877
  cwd: turnCwd,
855
878
  focus: introspectionTextForTurn,
@@ -859,6 +882,7 @@ export class AgentExecutor {
859
882
  : {}),
860
883
  runtime: {
861
884
  providerInvoked: false,
885
+ ...(activeSettings.theme ? { theme: activeSettings.theme.active } : {}),
862
886
  ...(this.deps.client.getCurrentModel()
863
887
  ? { model: this.deps.client.getCurrentModel() }
864
888
  : {}),
@@ -900,6 +924,11 @@ export class AgentExecutor {
900
924
  };
901
925
  const maxToolRounds = this.config.maxToolRounds;
902
926
  let toolRounds = 0;
927
+ // One guard per task: warnings/stops never leak into the next user turn.
928
+ const loopGuard = new ToolLoopGuard();
929
+ let pendingLoopDecision = null;
930
+ let loopGuardStopped = false;
931
+ let observationShortened = false;
903
932
  let totalOutputTokens = 0;
904
933
  let totalInputTokensForCost = 0;
905
934
  let providerPromptTokens = 0;
@@ -1042,6 +1071,25 @@ export class AgentExecutor {
1042
1071
  alwaysInclude: ['view_file', 'bash', 'search'],
1043
1072
  };
1044
1073
  }
1074
+ if (!codeResearch) {
1075
+ const { runtimeInspectionTools, connectedFleetSurfaceTools } = await import('../../services/runtime-settings-context.js');
1076
+ const inspectionTools = [...new Set([
1077
+ ...(surface === 'cli' ? runtimeInspectionTools(turnQueryText) : []),
1078
+ ...connectedFleetSurfaceTools(),
1079
+ ])];
1080
+ if (inspectionTools.length) {
1081
+ // Fleet tools first (Qwen3.6 GGUF otherwise prefers bash and
1082
+ // shells list_peers) but they are ADDED to the guaranteed
1083
+ // default list, never substituted for it.
1084
+ const alwaysInclude = mergeAlwaysInclude(selectionOpts.alwaysInclude, inspectionTools);
1085
+ if (alwaysInclude) {
1086
+ selectionOpts = { ...selectionOpts, alwaysInclude };
1087
+ }
1088
+ }
1089
+ }
1090
+ if (codeResearch) {
1091
+ selectionOpts = { ...selectionOpts, alwaysInclude: ['self_describe'] };
1092
+ }
1045
1093
  const selectionPromise = this.deps.toolSelectionStrategy.selectToolsForQuery(turnQueryText, selectionOpts);
1046
1094
  // Build context in a scratch array while the prompt and tools are being
1047
1095
  // prepared. It is appended only after transcript preparation so the
@@ -1094,7 +1142,9 @@ export class AgentExecutor {
1094
1142
  incrementalTokenCounter.invalidate();
1095
1143
  logger.debug(`[agent-executor] system prompt rebuilt query-aware (${rebuiltSystemPrompt.length} chars)`);
1096
1144
  }
1097
- let tools = selectionResult.tools;
1145
+ let tools = codeResearch
1146
+ ? selectionResult.tools.filter(tool => tool.function.name === 'self_describe')
1147
+ : selectionResult.tools;
1098
1148
  let forcedChatOnlyToolRunModel = null;
1099
1149
  if (toolRounds === 0) {
1100
1150
  this.deps.toolSelectionStrategy.cacheTools(tools, activeModelName);
@@ -1109,7 +1159,7 @@ export class AgentExecutor {
1109
1159
  tools = [];
1110
1160
  }
1111
1161
  }
1112
- const turnExecutionExtra = introspectionIntent
1162
+ const turnExecutionExtra = introspectionIntent || surface === 'cli'
1113
1163
  ? {
1114
1164
  ...(activeModelName ? { model: activeModelName } : {}),
1115
1165
  ...(providerName ? { provider: providerName } : {}),
@@ -1118,6 +1168,7 @@ export class AgentExecutor {
1118
1168
  ...(operationalRobotName
1119
1169
  ? { robotName: operationalRobotName }
1120
1170
  : {}),
1171
+ maxToolRounds,
1121
1172
  exposedToolNames: tools.map((tool) => tool.function.name),
1122
1173
  introspectionIntent,
1123
1174
  }
@@ -1142,6 +1193,27 @@ export class AgentExecutor {
1142
1193
  throw error;
1143
1194
  }
1144
1195
  preparedMessages.push(...contextBlocks);
1196
+ if (codeResearch) {
1197
+ preparedMessages.push({ role: 'system', content: 'Research your actual implementation using self_describe: operation=list/read/search, relative src/ paths (source checkout) or dist/ paths (installed package). Search literal symbols, read relevant code, and cite paths and line numbers. Only this confined read-only tool is exposed for this turn. Do not mistake the user project for your implementation. Do not claim a code graph is available without evidence.' });
1198
+ }
1199
+ if (surface === 'cli') {
1200
+ const { formatRuntimeSettingsContext } = await import('../../services/runtime-settings-context.js');
1201
+ preparedMessages.push({ role: 'system', content: formatRuntimeSettingsContext({
1202
+ surface, model: activeModelName, provider: providerName, maxToolRounds,
1203
+ }) });
1204
+ }
1205
+ else {
1206
+ // P5: other surfaces have no runtime_settings block; only a non-default
1207
+ // code_exec policy needs its short guidance.
1208
+ const { resolveCodeExecPolicy, CODE_EXEC_PREFER_HINT, CODE_EXEC_OFF_NOTICE } = await import('../../config/code-exec-policy.js');
1209
+ const codeExecPolicy = resolveCodeExecPolicy(activeModelName ?? undefined);
1210
+ if (codeExecPolicy.policy !== 'offer') {
1211
+ preparedMessages.push({
1212
+ role: 'system',
1213
+ content: `<code_exec_policy policy="${codeExecPolicy.policy}" source="${codeExecPolicy.source}">\n${codeExecPolicy.policy === 'prefer' ? CODE_EXEC_PREFER_HINT : CODE_EXEC_OFF_NOTICE}\n</code_exec_policy>`,
1214
+ });
1215
+ }
1216
+ }
1145
1217
  if (emotionalPresenceContext) {
1146
1218
  preparedMessages.push({
1147
1219
  role: 'system',
@@ -1190,6 +1262,9 @@ export class AgentExecutor {
1190
1262
  const progress = startHeadlessPromptProgress();
1191
1263
  const streamFactory = () => withStallGuard(this.deps.client.chatStream(preparedMessages, tools, {
1192
1264
  streamRetry: false,
1265
+ // An explicit request to inspect implementation needs an observation
1266
+ // before an answer. Only the confined self_describe reader is exposed.
1267
+ ...(codeResearch && toolRounds === 0 && tools.length ? { tool_choice: 'required' } : {}),
1193
1268
  turnMetrics: {
1194
1269
  recorder: getTurnMetricsRecorder(),
1195
1270
  inputTokens,
@@ -1297,6 +1372,11 @@ export class AgentExecutor {
1297
1372
  }
1298
1373
  toolCalls = filteredToolCalls.length > 0 ? filteredToolCalls : undefined;
1299
1374
  }
1375
+ if (codeResearch && toolCalls?.some(call => call.function.name !== 'self_describe')) {
1376
+ yield { type: 'content', content: 'Code inspection is read-only; the requested tool is not available in this inspection turn.' };
1377
+ yield { type: 'done' };
1378
+ return;
1379
+ }
1300
1380
  if (relationshipSafety && Array.isArray(toolCalls)) {
1301
1381
  toolCalls = toolCalls.map(prepareRelationshipSafeInteractiveToolCall);
1302
1382
  }
@@ -1471,8 +1551,15 @@ export class AgentExecutor {
1471
1551
  estimatedTokens,
1472
1552
  contextWindow,
1473
1553
  });
1554
+ // The results of this round's calls are pushed after execution:
1555
+ // name them so repair does not close them with a synthetic
1556
+ // '[result lost during compaction]' that would then win over
1557
+ // the real output.
1474
1558
  const compacted = compactTurnMessagesInPlace(this.deps.contextManager, messages, {
1475
1559
  isolatedSharedHost,
1560
+ pendingToolCallIds: toolCalls
1561
+ .map((call) => call.id)
1562
+ .filter((id) => typeof id === 'string' && id.length > 0),
1476
1563
  });
1477
1564
  if (compacted)
1478
1565
  incrementalTokenCounter.invalidate();
@@ -1565,6 +1652,18 @@ export class AgentExecutor {
1565
1652
  };
1566
1653
  }
1567
1654
  }
1655
+ // --- Tool loop guard (P1): observe the native result before any
1656
+ // optimizer so "same call + same result" means no progress. The
1657
+ // decision is applied at the batch boundary so tool_call/tool_result
1658
+ // pairs are never split.
1659
+ const loopDecision = loopGuard.observe({
1660
+ name: toolCall.function.name,
1661
+ argumentsJson: toolCall.function.arguments || '{}',
1662
+ result,
1663
+ });
1664
+ if (loopDecision.action === 'stop' || (loopDecision.action === 'warn' && !pendingLoopDecision)) {
1665
+ pendingLoopDecision = loopDecision;
1666
+ }
1568
1667
  // Expand the current turn's cached schema after discovery or live
1569
1668
  // authoring. Without this, a newly created tool is dispatchable but
1570
1669
  // invisible to the model until the next user turn.
@@ -1705,6 +1804,8 @@ export class AgentExecutor {
1705
1804
  signal: abortController?.signal,
1706
1805
  });
1707
1806
  let modelStreamContent = optimization.content;
1807
+ if (optimization.optimized)
1808
+ observationShortened = true;
1708
1809
  // lm-resizer owns the semantic budget when available. Its absence or
1709
1810
  // an intentionally raw failure still receives a model-aware hard cap;
1710
1811
  // the exact observation remains available through restore_context.
@@ -1715,6 +1816,7 @@ export class AgentExecutor {
1715
1816
  this.deps.tokenCounter.countTokens(modelStreamContent) > budgetTokens) {
1716
1817
  const truncated = semanticTruncate(modelStreamContent, { maxChars: hardLimitChars });
1717
1818
  if (truncated.truncated) {
1819
+ observationShortened = true;
1718
1820
  const recoveryNote = toolCall.id
1719
1821
  ? `\n\n[Full exact observation: restore_context({"identifier":${JSON.stringify(toolCall.id)}})]`
1720
1822
  : '';
@@ -1850,6 +1952,68 @@ export class AgentExecutor {
1850
1952
  yield { type: "done" };
1851
1953
  return;
1852
1954
  }
1955
+ // First shortened observation in this profile: one user-facing tip (P4 first-use hints).
1956
+ if (observationShortened) {
1957
+ observationShortened = false;
1958
+ const tip = takeFirstUseHint('restore_context');
1959
+ if (tip)
1960
+ yield { type: "content", content: `\n💡 ${tip}\n` };
1961
+ }
1962
+ // Tool-call/result pairs are complete here: apply the loop guard.
1963
+ if (pendingLoopDecision) {
1964
+ const decision = pendingLoopDecision;
1965
+ pendingLoopDecision = null;
1966
+ const loopData = {
1967
+ action: decision.action,
1968
+ loopType: decision.kind,
1969
+ toolNames: decision.toolNames,
1970
+ repetitions: decision.repetitions,
1971
+ toolRounds,
1972
+ };
1973
+ try {
1974
+ getGlobalEventBus().emit('agent:loop_detected', {
1975
+ loopType: decision.kind,
1976
+ detail: decision.message,
1977
+ count: decision.repetitions,
1978
+ turnIndex: toolRounds,
1979
+ });
1980
+ }
1981
+ catch (err) {
1982
+ logger.debug('[loop-guard] event emission failed', { error: String(err) });
1983
+ }
1984
+ if (decision.action === 'warn') {
1985
+ logger.warn('[loop-guard] loop warning injected', loopData);
1986
+ yield { type: "content", content: `\n⚠️ ${decision.message}\n` };
1987
+ messages.push({
1988
+ role: 'system',
1989
+ content: `<context type="loop-guard">\n${decision.message}\n</context>`,
1990
+ });
1991
+ }
1992
+ else {
1993
+ logger.warn('[loop-guard] turn stopped', loopData);
1994
+ const runId = typeof this.deps.toolHandler.getRunId === 'function'
1995
+ ? this.deps.toolHandler.getRunId()
1996
+ : undefined;
1997
+ if (runId) {
1998
+ try {
1999
+ getActiveRunStore()?.emit(runId, {
2000
+ type: 'decision',
2001
+ data: { kind: 'loop_guard_stopped', ...loopData },
2002
+ });
2003
+ }
2004
+ catch { /* observability is optional */ }
2005
+ }
2006
+ yield {
2007
+ type: 'run_event',
2008
+ runEvent: { runId: runId ?? '', eventType: 'loop_guard_stopped', data: loopData },
2009
+ };
2010
+ history.push({ type: 'assistant', content: decision.message, timestamp: new Date() });
2011
+ messages.push({ role: 'assistant', content: decision.message });
2012
+ yield { type: "content", content: `\n\n${decision.message}` };
2013
+ loopGuardStopped = true;
2014
+ break;
2015
+ }
2016
+ }
1853
2017
  // Tool-call/result pairs are complete at this boundary, so a steer
1854
2018
  // that arrived while tools were running can now be injected safely.
1855
2019
  const deferredSteering = this.deps.messageQueue?.hasSteeringMessage()
@@ -1956,7 +2120,7 @@ export class AgentExecutor {
1956
2120
  break;
1957
2121
  }
1958
2122
  }
1959
- if (toolRounds >= maxToolRounds && !terminateDetectedStreaming) {
2123
+ if (toolRounds >= maxToolRounds && !terminateDetectedStreaming && !loopGuardStopped) {
1960
2124
  const limitMessage = 'Maximum tool execution rounds reached.';
1961
2125
  history.push({ type: 'assistant', content: limitMessage, timestamp: new Date() });
1962
2126
  messages.push({ role: 'assistant', content: limitMessage });
@@ -59,9 +59,18 @@ export declare function prepareIsolatedTurnMessages(messages: CodeBuddyMessage[]
59
59
  * transcript never shrank, the middleware 'compact' action did nothing, and
60
60
  * proactive compaction re-fired forever while the provider limit approached.
61
61
  * Returns true when the transcript actually changed.
62
+ *
63
+ * `pendingToolCallIds` names the calls of the round being executed. Their
64
+ * results are pushed AFTER this compaction, so repairing them here injected
65
+ * `[result lost during compaction]`; the real result then lost the first-wins
66
+ * duplicate arbitration and the model never saw the tool output. Those
67
+ * placeholders are removed while every other repair — historical orphans,
68
+ * id-less calls, duplicates, ordering — is kept. The provider frontier stays
69
+ * `prepareTurnMessages`, which still closes any call left unanswered.
62
70
  */
63
71
  export declare function compactTurnMessagesInPlace(contextManager: ContextManagerV2, messages: CodeBuddyMessage[], options?: {
64
72
  isolatedSharedHost?: boolean;
73
+ pendingToolCallIds?: Iterable<string>;
65
74
  }): boolean;
66
75
  export interface InitialContextDeps {
67
76
  message: string;
@@ -151,6 +151,52 @@ export function prepareIsolatedTurnMessages(messages) {
151
151
  ? repairToolCallPairs(messages)
152
152
  : [...messages];
153
153
  }
154
+ /** Tool calls of the in-flight round that have no result in `messages` yet. */
155
+ function unansweredPendingCalls(messages, pendingToolCallIds) {
156
+ const pending = new Set();
157
+ for (const id of pendingToolCallIds ?? [])
158
+ if (id)
159
+ pending.add(id);
160
+ if (pending.size === 0)
161
+ return pending;
162
+ for (const message of messages) {
163
+ if (message.role !== 'tool')
164
+ continue;
165
+ const callId = message.tool_call_id;
166
+ if (callId)
167
+ pending.delete(callId);
168
+ }
169
+ return pending;
170
+ }
171
+ /**
172
+ * Drop the synthetic `[result lost during compaction]` placeholders that
173
+ * transcript repair injected for calls that are merely still running.
174
+ *
175
+ * Returns null when a pending call did not survive compaction at all: the
176
+ * caller then keeps the transcript untouched rather than leaving a call whose
177
+ * real result would become an unpairable orphan.
178
+ */
179
+ function withoutPendingPlaceholders(compacted, pending) {
180
+ const survivingCallIds = new Set();
181
+ for (const message of compacted) {
182
+ const calls = message.tool_calls;
183
+ if (Array.isArray(calls))
184
+ for (const call of calls)
185
+ if (call?.id)
186
+ survivingCallIds.add(call.id);
187
+ }
188
+ for (const id of pending)
189
+ if (!survivingCallIds.has(id))
190
+ return null;
191
+ // `pending` only holds ids with no result in `original`, so every tool
192
+ // message carrying one of them was invented by repair just now.
193
+ return compacted.filter((message) => {
194
+ if (message.role !== 'tool')
195
+ return true;
196
+ const callId = message.tool_call_id;
197
+ return !(callId && pending.has(callId));
198
+ });
199
+ }
154
200
  /**
155
201
  * Compact + repair IN PLACE — for mid-loop compaction sites where `messages`
156
202
  * is a SHARED reference (the turn loop and its helpers keep pushing into it).
@@ -159,11 +205,27 @@ export function prepareIsolatedTurnMessages(messages) {
159
205
  * transcript never shrank, the middleware 'compact' action did nothing, and
160
206
  * proactive compaction re-fired forever while the provider limit approached.
161
207
  * Returns true when the transcript actually changed.
208
+ *
209
+ * `pendingToolCallIds` names the calls of the round being executed. Their
210
+ * results are pushed AFTER this compaction, so repairing them here injected
211
+ * `[result lost during compaction]`; the real result then lost the first-wins
212
+ * duplicate arbitration and the model never saw the tool output. Those
213
+ * placeholders are removed while every other repair — historical orphans,
214
+ * id-less calls, duplicates, ordering — is kept. The provider frontier stays
215
+ * `prepareTurnMessages`, which still closes any call left unanswered.
162
216
  */
163
217
  export function compactTurnMessagesInPlace(contextManager, messages, options = {}) {
164
- const compacted = prepareTurnMessages(contextManager, messages, options);
165
- if (compacted === messages)
218
+ const pending = unansweredPendingCalls(messages, options.pendingToolCallIds);
219
+ const prepared = prepareTurnMessages(contextManager, messages, options);
220
+ if (prepared === messages)
166
221
  return false;
222
+ let compacted = prepared;
223
+ if (pending.size > 0) {
224
+ const protectedMessages = withoutPendingPlaceholders(prepared, pending);
225
+ if (!protectedMessages)
226
+ return false; // pending call dropped: never risk its real result
227
+ compacted = protectedMessages;
228
+ }
167
229
  const changed = compacted.length !== messages.length || compacted.some((m, i) => m !== messages[i]);
168
230
  if (!changed)
169
231
  return false;
@@ -0,0 +1,76 @@
1
+ /**
2
+ * ToolLoopGuard — stops unproductive tool-call loops inside one agent task.
3
+ *
4
+ * New code written for Buddy (comparatif plan P1); nothing was copied from
5
+ * another project. It does not wire the older, unwired
6
+ * `src/agent/loop-detection-service.ts` for technical reasons: that service
7
+ * keys a loop on tool name + arguments only, so a polling call whose output
8
+ * changes (process status, job progress) is flagged as a loop; it has no
9
+ * per-tool exemption and no warn → stop escalation. This guard adds the result
10
+ * fingerprint, `repeatSafe` metadata and a one-warning-then-stop contract, and
11
+ * emits the same `agent:loop_detected` event, so the domain-event bridge and
12
+ * the `agent-loop-alert` rule template keep working unchanged.
13
+ *
14
+ * Contract:
15
+ * - "No progress" means the SAME tool, SAME canonical arguments AND the SAME
16
+ * result fingerprint. A polling call whose output changes is progress.
17
+ * - Tools whose metadata declares `repeatSafe: true` are never observed.
18
+ * - First detection in a task → `warn` (exactly once per task). Any loop
19
+ * pattern that persists for `stopAfterWarning` more observations after the
20
+ * warning → `stop`.
21
+ * - The guard is a validation, not a confirmation: nothing in the autonomy /
22
+ * YOLO configuration disables it. A new guard instance is created per task.
23
+ */
24
+ import type { ToolResult } from '../../types/index.js';
25
+ export type ToolLoopKind = 'repeated_call' | 'repeated_cycle';
26
+ export interface ToolLoopObservation {
27
+ name: string;
28
+ /** Raw JSON arguments string as sent by the model. */
29
+ argumentsJson: string;
30
+ result?: Pick<ToolResult, 'success' | 'output' | 'error'> | null;
31
+ }
32
+ export type ToolLoopDecision = {
33
+ action: 'none';
34
+ } | {
35
+ action: 'warn' | 'stop';
36
+ kind: ToolLoopKind;
37
+ toolNames: string[];
38
+ repetitions: number;
39
+ message: string;
40
+ };
41
+ export interface ToolLoopGuardOptions {
42
+ /** Identical no-progress observations (or cycle repetitions) before warning. Default 5. */
43
+ threshold?: number;
44
+ /** Additional looping observations after the warning before stopping. Default 3. */
45
+ stopAfterWarning?: number;
46
+ /** Longest cycle period inspected (A→B→A→B is period 2). Default 3. */
47
+ maxCyclePeriod?: number;
48
+ /** Override for tests; defaults to the `repeatSafe` flag in tool metadata. */
49
+ isRepeatSafe?: (toolName: string) => boolean;
50
+ }
51
+ export declare function isRepeatSafeTool(toolName: string): boolean;
52
+ /**
53
+ * Loop fingerprint of a successful `a2a_call`: every SendMessage creates a new remote task,
54
+ * so `taskId` / `contextId` change even when the peer returns the same text. For that tool
55
+ * only, and only when the output has exactly the task shape, those two technical ids are
56
+ * dropped; peer, state, text and instruction stay. Anything else (another tool, invalid JSON,
57
+ * another shape such as a message result) is fingerprinted raw, so changing output is progress.
58
+ */
59
+ export declare function loopFingerprintOutput(toolName: string, output: string): string;
60
+ export declare class ToolLoopGuard {
61
+ private readonly threshold;
62
+ private readonly stopAfterWarning;
63
+ private readonly maxCyclePeriod;
64
+ private readonly isRepeatSafe;
65
+ private history;
66
+ private warned;
67
+ private stopped;
68
+ constructor(options?: ToolLoopGuardOptions);
69
+ get hasWarned(): boolean;
70
+ get hasStopped(): boolean;
71
+ observe(observation: ToolLoopObservation): ToolLoopDecision;
72
+ /** Detect a repeated call or cycle repeated at least `repeats` times at the tail of history. */
73
+ private detect;
74
+ private warnMessage;
75
+ private stopMessage;
76
+ }