@phuetz/code-buddy 1.0.0 → 1.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 (312) hide show
  1. package/README.md +160 -164
  2. package/dist/agent/autonomous/agentic-coding-contract.d.ts +6 -6
  3. package/dist/agent/codebuddy-agent.d.ts +1 -1
  4. package/dist/agent/codebuddy-agent.js +101 -13
  5. package/dist/agent/execution/agent-executor.js +122 -12
  6. package/dist/agent/execution/tool-dependency-graph.js +7 -0
  7. package/dist/agent/execution/tool-hooks.d.ts +6 -0
  8. package/dist/agent/execution/tool-hooks.js +13 -5
  9. package/dist/agent/execution/tool-selection-strategy.d.ts +11 -2
  10. package/dist/agent/execution/tool-selection-strategy.js +67 -10
  11. package/dist/agent/extended-thinking.d.ts +8 -0
  12. package/dist/agent/extended-thinking.js +27 -0
  13. package/dist/agent/hermes-browser-backends.js +151 -11
  14. package/dist/agent/hermes-claw-migrate.d.ts +59 -0
  15. package/dist/agent/hermes-claw-migrate.js +427 -26
  16. package/dist/agent/hermes-parity-manifest.js +78 -71
  17. package/dist/agent/hermes-runtime-lifecycle.d.ts +67 -0
  18. package/dist/agent/hermes-runtime-lifecycle.js +468 -0
  19. package/dist/agent/lesson-auto-proposer.js +10 -0
  20. package/dist/agent/middleware/index.d.ts +1 -0
  21. package/dist/agent/middleware/index.js +1 -0
  22. package/dist/agent/middleware/session-duration.d.ts +36 -0
  23. package/dist/agent/middleware/session-duration.js +78 -0
  24. package/dist/agent/middleware/visual-validation-middleware.d.ts +17 -0
  25. package/dist/agent/middleware/visual-validation-middleware.js +67 -0
  26. package/dist/agent/model-benchmark.d.ts +77 -0
  27. package/dist/agent/model-benchmark.js +309 -0
  28. package/dist/agent/model-tier.d.ts +14 -0
  29. package/dist/agent/model-tier.js +71 -0
  30. package/dist/agent/multi-agent/agents/orchestrator-agent.js +17 -6
  31. package/dist/agent/multi-agent/multi-agent-system.js +3 -1
  32. package/dist/agent/multi-agent/session-fleet-bridge.d.ts +2 -0
  33. package/dist/agent/multi-agent/session-fleet-bridge.js +9 -1
  34. package/dist/agent/multi-agent/session-tools.js +22 -5
  35. package/dist/agent/reasoning/mcts.js +20 -6
  36. package/dist/agent/session-end-flush.d.ts +71 -0
  37. package/dist/agent/session-end-flush.js +233 -0
  38. package/dist/agent/streaming/streaming-handler.d.ts +7 -0
  39. package/dist/agent/streaming/streaming-handler.js +8 -1
  40. package/dist/agent/tool-executor.js +1 -0
  41. package/dist/agent/tool-handler.d.ts +5 -0
  42. package/dist/agent/tool-handler.js +130 -4
  43. package/dist/browser-automation/browser-operator-executor.js +15 -0
  44. package/dist/browser-automation/browser-use-runner.d.ts +96 -0
  45. package/dist/browser-automation/browser-use-runner.js +492 -0
  46. package/dist/browser-automation/camofox-runner.d.ts +107 -0
  47. package/dist/browser-automation/camofox-runner.js +287 -0
  48. package/dist/channels/dingtalk/index.js +3 -0
  49. package/dist/channels/discord/client.d.ts +9 -0
  50. package/dist/channels/discord/client.js +12 -0
  51. package/dist/channels/feishu/index.d.ts +167 -1
  52. package/dist/channels/feishu/index.js +432 -7
  53. package/dist/channels/gateway-lifecycle.d.ts +132 -0
  54. package/dist/channels/gateway-lifecycle.js +219 -0
  55. package/dist/channels/google-chat/index.d.ts +5 -1
  56. package/dist/channels/google-chat/index.js +5 -1
  57. package/dist/channels/imessage/index.d.ts +14 -0
  58. package/dist/channels/imessage/index.js +64 -17
  59. package/dist/channels/index.d.ts +4 -0
  60. package/dist/channels/index.js +4 -0
  61. package/dist/channels/irc/index.d.ts +73 -3
  62. package/dist/channels/irc/index.js +446 -11
  63. package/dist/channels/line/index.js +3 -0
  64. package/dist/channels/mattermost/index.d.ts +51 -4
  65. package/dist/channels/mattermost/index.js +303 -20
  66. package/dist/channels/nextcloud-talk/index.d.ts +89 -8
  67. package/dist/channels/nextcloud-talk/index.js +367 -16
  68. package/dist/channels/nostr/index.d.ts +121 -1
  69. package/dist/channels/nostr/index.js +396 -8
  70. package/dist/channels/ntfy/index.js +3 -0
  71. package/dist/channels/qq/index.js +3 -0
  72. package/dist/channels/slash-parity.d.ts +117 -0
  73. package/dist/channels/slash-parity.js +185 -0
  74. package/dist/channels/synology-chat/index.js +3 -0
  75. package/dist/channels/teams/index.d.ts +3 -0
  76. package/dist/channels/teams/index.js +3 -0
  77. package/dist/channels/telegram/client.d.ts +9 -0
  78. package/dist/channels/telegram/client.js +12 -0
  79. package/dist/channels/twilio-voice/index.js +3 -0
  80. package/dist/channels/webchat/index.d.ts +7 -1
  81. package/dist/channels/webchat/index.js +7 -1
  82. package/dist/channels/wecom/index.js +3 -0
  83. package/dist/channels/weixin/index.js +3 -0
  84. package/dist/channels/zalo/index.js +3 -0
  85. package/dist/cli/config-loader.js +1 -1
  86. package/dist/codebuddy/client.d.ts +30 -1
  87. package/dist/codebuddy/client.js +167 -20
  88. package/dist/codebuddy/providers/provider-chatgpt-responses.d.ts +5 -0
  89. package/dist/codebuddy/providers/provider-chatgpt-responses.js +1 -0
  90. package/dist/codebuddy/providers/provider-gemini-native.js +1 -1
  91. package/dist/codebuddy/providers/provider-openai-compat.d.ts +5 -0
  92. package/dist/codebuddy/providers/provider-openai-compat.js +82 -2
  93. package/dist/codebuddy/tool-definitions/computer-control-tools.js +32 -2
  94. package/dist/codebuddy/tool-definitions/core-tools.js +3 -3
  95. package/dist/codebuddy/tool-definitions/index.d.ts +2 -1
  96. package/dist/codebuddy/tool-definitions/index.js +2 -1
  97. package/dist/codebuddy/tool-definitions/multimodal-tools.d.ts +1 -0
  98. package/dist/codebuddy/tool-definitions/multimodal-tools.js +44 -1
  99. package/dist/codebuddy/tool-definitions/windows-tools.d.ts +2 -0
  100. package/dist/codebuddy/tool-definitions/windows-tools.js +34 -0
  101. package/dist/codebuddy/tools.d.ts +1 -1
  102. package/dist/codebuddy/tools.js +27 -24
  103. package/dist/commands/cli/hermes-commands.js +108 -0
  104. package/dist/commands/cli/native-engine-commands.js +107 -3
  105. package/dist/commands/cli/utility-commands.js +45 -0
  106. package/dist/commands/enhanced-command-handler.js +5 -0
  107. package/dist/commands/goal-cli.d.ts +71 -0
  108. package/dist/commands/goal-cli.js +280 -0
  109. package/dist/commands/handlers/goal-handler.d.ts +34 -0
  110. package/dist/commands/handlers/goal-handler.js +155 -0
  111. package/dist/commands/handlers/index.d.ts +1 -0
  112. package/dist/commands/handlers/index.js +2 -0
  113. package/dist/commands/handlers/infra-handlers.js +36 -0
  114. package/dist/commands/handlers/memory-handlers.js +96 -1
  115. package/dist/commands/headless-slash.d.ts +2 -0
  116. package/dist/commands/headless-slash.js +12 -0
  117. package/dist/commands/llm-provider-resolution.d.ts +5 -3
  118. package/dist/commands/llm-provider-resolution.js +87 -33
  119. package/dist/commands/ollama.d.ts +25 -0
  120. package/dist/commands/ollama.js +100 -0
  121. package/dist/commands/provider.d.ts +5 -0
  122. package/dist/commands/provider.js +106 -55
  123. package/dist/commands/slash/builtin-commands.js +20 -0
  124. package/dist/commands/spec-next.js +2 -1
  125. package/dist/commands/spec-plan.js +5 -16
  126. package/dist/commands/tunnel.d.ts +2 -0
  127. package/dist/commands/tunnel.js +48 -0
  128. package/dist/config/config-resolver.d.ts +2 -1
  129. package/dist/config/config-resolver.js +54 -41
  130. package/dist/config/constants.d.ts +28 -0
  131. package/dist/config/constants.js +7 -0
  132. package/dist/config/env-schema.js +770 -0
  133. package/dist/config/feature-flags.js +7 -0
  134. package/dist/config/model-tools.js +4 -4
  135. package/dist/config/toml-config.d.ts +16 -0
  136. package/dist/config/toml-config.js +3 -0
  137. package/dist/context/context-manager-v2.d.ts +39 -0
  138. package/dist/context/context-manager-v2.js +91 -0
  139. package/dist/daemon/agent-task-executor.js +12 -1
  140. package/dist/daemon/autonomous-daemon.d.ts +1 -1
  141. package/dist/daemon/autonomous-daemon.js +5 -3
  142. package/dist/daemon/autonomous-loop.d.ts +21 -1
  143. package/dist/daemon/autonomous-loop.js +69 -0
  144. package/dist/daemon/colab-goal.d.ts +38 -0
  145. package/dist/daemon/colab-goal.js +81 -0
  146. package/dist/daemon/cron-agent-bridge.d.ts +12 -3
  147. package/dist/daemon/cron-agent-bridge.js +25 -9
  148. package/dist/desktop/codebuddy-engine-adapter.d.ts +32 -0
  149. package/dist/desktop/codebuddy-engine-adapter.js +257 -80
  150. package/dist/desktop/engine-adapter.d.ts +14 -0
  151. package/dist/desktop-automation/automation-manager.js +16 -0
  152. package/dist/desktop-automation/omniparser-runner.d.ts +63 -0
  153. package/dist/desktop-automation/omniparser-runner.js +115 -0
  154. package/dist/desktop-automation/smart-snapshot.d.ts +11 -0
  155. package/dist/desktop-automation/smart-snapshot.js +72 -5
  156. package/dist/doctor/index.js +40 -23
  157. package/dist/events/types.d.ts +11 -0
  158. package/dist/fleet/colab-store.d.ts +21 -0
  159. package/dist/fleet/colab-store.js +28 -0
  160. package/dist/fleet/peer-session-bridge.d.ts +1 -1
  161. package/dist/fleet/peer-session-bridge.js +243 -2
  162. package/dist/fleet/peer-session-store.d.ts +3 -0
  163. package/dist/fleet/peer-tool-bridge.js +14 -0
  164. package/dist/fleet/privacy-lint.d.ts +8 -0
  165. package/dist/fleet/privacy-lint.js +22 -0
  166. package/dist/goals/goal-decomposer.d.ts +27 -0
  167. package/dist/goals/goal-decomposer.js +293 -0
  168. package/dist/goals/goal-judge-client.d.ts +15 -0
  169. package/dist/goals/goal-judge-client.js +45 -0
  170. package/dist/goals/goal-judge.d.ts +38 -0
  171. package/dist/goals/goal-judge.js +158 -0
  172. package/dist/goals/goal-loop.d.ts +37 -0
  173. package/dist/goals/goal-loop.js +101 -0
  174. package/dist/goals/goal-manager.d.ts +77 -0
  175. package/dist/goals/goal-manager.js +289 -0
  176. package/dist/goals/goal-state.d.ts +99 -0
  177. package/dist/goals/goal-state.js +315 -0
  178. package/dist/goals/goal-store.d.ts +28 -0
  179. package/dist/goals/goal-store.js +87 -0
  180. package/dist/goals/index.d.ts +5 -0
  181. package/dist/goals/index.js +6 -0
  182. package/dist/hooks/use-input-handler.js +36 -1
  183. package/dist/hooks/user-hooks.js +17 -3
  184. package/dist/index.js +506 -25
  185. package/dist/input/text-to-speech.d.ts +2 -6
  186. package/dist/input/text-to-speech.js +2 -27
  187. package/dist/integrations/screenpipe/screenpipe-client.d.ts +10 -0
  188. package/dist/integrations/screenpipe/screenpipe-client.js +12 -2
  189. package/dist/integrations/tailscale.d.ts +13 -0
  190. package/dist/integrations/tailscale.js +87 -34
  191. package/dist/knowledge/workspace-indexer.js +53 -9
  192. package/dist/mcp/client.js +1 -0
  193. package/dist/mcp/mcp-desktop-tools.d.ts +23 -0
  194. package/dist/mcp/mcp-desktop-tools.js +158 -0
  195. package/dist/mcp/mcp-server.js +2 -0
  196. package/dist/memory/enhanced-memory.d.ts +10 -0
  197. package/dist/memory/enhanced-memory.js +33 -2
  198. package/dist/memory/index.d.ts +3 -1
  199. package/dist/memory/index.js +4 -1
  200. package/dist/memory/memory-auto-proposer.d.ts +23 -0
  201. package/dist/memory/memory-auto-proposer.js +308 -0
  202. package/dist/memory/memory-candidate-queue.d.ts +92 -0
  203. package/dist/memory/memory-candidate-queue.js +261 -0
  204. package/dist/memory/persistent-memory.d.ts +45 -2
  205. package/dist/memory/persistent-memory.js +236 -40
  206. package/dist/ml/bayesian-qualifier.d.ts +1 -1
  207. package/dist/ml/bayesian-qualifier.js +35 -2
  208. package/dist/observability/run-store.d.ts +1 -1
  209. package/dist/openclaw/gateway-bridge.js +5 -0
  210. package/dist/plugins/marketplace.d.ts +1 -0
  211. package/dist/plugins/marketplace.js +7 -0
  212. package/dist/prompts/execution-discipline.d.ts +14 -0
  213. package/dist/prompts/execution-discipline.js +29 -0
  214. package/dist/prompts/variation-injector.js +9 -2
  215. package/dist/protocols/acp/acp-agentic-runner.d.ts +7 -6
  216. package/dist/protocols/acp/acp-agentic-runner.js +115 -18
  217. package/dist/protocols/acp/acp-session-store.d.ts +23 -0
  218. package/dist/protocols/acp/acp-session-store.js +77 -0
  219. package/dist/protocols/acp/acp-stdio-server.d.ts +7 -0
  220. package/dist/protocols/acp/acp-stdio-server.js +71 -16
  221. package/dist/providers/active-llm-registry.d.ts +37 -0
  222. package/dist/providers/active-llm-registry.js +186 -0
  223. package/dist/providers/auxiliary-provider.d.ts +25 -0
  224. package/dist/providers/auxiliary-provider.js +192 -0
  225. package/dist/providers/codex-oauth.d.ts +1 -1
  226. package/dist/providers/codex-oauth.js +27 -4
  227. package/dist/providers/index.d.ts +3 -0
  228. package/dist/providers/index.js +4 -0
  229. package/dist/providers/provider-catalog.d.ts +70 -0
  230. package/dist/providers/provider-catalog.js +738 -0
  231. package/dist/providers/provider-fallback.d.ts +35 -0
  232. package/dist/providers/provider-fallback.js +171 -0
  233. package/dist/providers/xai-oauth.d.ts +128 -0
  234. package/dist/providers/xai-oauth.js +735 -0
  235. package/dist/scheduler/cron-scheduler.d.ts +15 -3
  236. package/dist/scheduler/cron-scheduler.js +53 -7
  237. package/dist/scheduler/script-runner.d.ts +2 -0
  238. package/dist/scheduler/script-runner.js +19 -4
  239. package/dist/scheduler/watchdog-handlers.js +7 -20
  240. package/dist/search/usearch-index.js +7 -2
  241. package/dist/security/tool-policy/tool-groups.js +2 -0
  242. package/dist/server/index.js +17 -2
  243. package/dist/server/routes/mobile.d.ts +12 -4
  244. package/dist/server/routes/mobile.js +116 -1
  245. package/dist/server/tls-config.d.ts +35 -0
  246. package/dist/server/tls-config.js +142 -0
  247. package/dist/server/tunnel-manager.d.ts +20 -0
  248. package/dist/server/tunnel-manager.js +58 -0
  249. package/dist/server/websocket/fleet-bridge.d.ts +13 -1
  250. package/dist/server/websocket/fleet-bridge.js +16 -0
  251. package/dist/services/prompt-builder.d.ts +1 -0
  252. package/dist/services/prompt-builder.js +49 -8
  253. package/dist/shared/engine-types.d.ts +15 -1
  254. package/dist/sidecar/sidecar-bridge.d.ts +1 -0
  255. package/dist/sidecar/sidecar-bridge.js +21 -10
  256. package/dist/spec/spec-store.js +8 -1
  257. package/dist/talk-mode/providers/audioreader-tts.d.ts +1 -1
  258. package/dist/talk-mode/providers/audioreader-tts.js +8 -8
  259. package/dist/templates/project-scaffolding.js +3 -2
  260. package/dist/themes/theme-schema.d.ts +10 -10
  261. package/dist/tools/application-profiles.js +38 -0
  262. package/dist/tools/bash/bash-tool.d.ts +1 -0
  263. package/dist/tools/bash/bash-tool.js +16 -2
  264. package/dist/tools/bash/command-validator.js +3 -0
  265. package/dist/tools/bash/rtk-rewrite.d.ts +16 -0
  266. package/dist/tools/bash/rtk-rewrite.js +100 -0
  267. package/dist/tools/bash/streaming-executor.js +14 -1
  268. package/dist/tools/computer-control-tool.d.ts +16 -1
  269. package/dist/tools/computer-control-tool.js +317 -5
  270. package/dist/tools/document-generator.d.ts +14 -0
  271. package/dist/tools/document-generator.js +79 -0
  272. package/dist/tools/execute-code-rpc-invoker.d.ts +26 -2
  273. package/dist/tools/execute-code-rpc-invoker.js +46 -5
  274. package/dist/tools/metadata.js +28 -0
  275. package/dist/tools/office-macro-tool.d.ts +10 -0
  276. package/dist/tools/office-macro-tool.js +93 -0
  277. package/dist/tools/registry/index.d.ts +2 -1
  278. package/dist/tools/registry/index.js +2 -1
  279. package/dist/tools/registry/memory-tools.d.ts +20 -0
  280. package/dist/tools/registry/memory-tools.js +225 -3
  281. package/dist/tools/registry/multimodal-tools.js +2 -2
  282. package/dist/tools/registry/vision-tools.d.ts +33 -0
  283. package/dist/tools/registry/vision-tools.js +308 -6
  284. package/dist/tools/registry/windows-tools.d.ts +2 -0
  285. package/dist/tools/registry/windows-tools.js +47 -0
  286. package/dist/tools/route-peer-tool.js +14 -0
  287. package/dist/tools/screenshot-tool.js +14 -2
  288. package/dist/tools/text-to-speech-tool.d.ts +1 -1
  289. package/dist/tools/text-to-speech-tool.js +2 -12
  290. package/dist/tools/tool-selector.js +3 -59
  291. package/dist/utils/config-validation/schema.d.ts +40 -7
  292. package/dist/utils/config-validation/schema.js +130 -1
  293. package/dist/utils/cost-tracker.js +21 -1
  294. package/dist/utils/disk-guard.d.ts +180 -0
  295. package/dist/utils/disk-guard.js +404 -0
  296. package/dist/utils/interactive-setup.js +35 -2
  297. package/dist/utils/model-utils.js +11 -2
  298. package/dist/utils/output-sanitizer.js +11 -0
  299. package/dist/utils/provider-detector.d.ts +8 -7
  300. package/dist/utils/provider-detector.js +19 -77
  301. package/dist/utils/settings-manager.d.ts +7 -0
  302. package/dist/utils/token-display.d.ts +3 -3
  303. package/dist/utils/token-display.js +22 -4
  304. package/dist/wizard/onboarding.d.ts +39 -0
  305. package/dist/wizard/onboarding.js +433 -21
  306. package/dist/wizard/provider-onboarding.d.ts +8 -23
  307. package/dist/wizard/provider-onboarding.js +39 -158
  308. package/package.json +4 -2
  309. package/dist/plugins/provider-onboarding.d.ts +0 -23
  310. package/dist/plugins/provider-onboarding.js +0 -116
  311. package/dist/utils/kokoro-tts.d.ts +0 -8
  312. package/dist/utils/kokoro-tts.js +0 -67
@@ -368,21 +368,36 @@ export class SessionToolExecutor {
368
368
  const limit = this.parseSessionSearchLimit(args.limit);
369
369
  const { getSessionStore } = await import('../../persistence/session-store.js');
370
370
  const sessions = (await getSessionStore().searchSessions(query)).slice(0, limit);
371
+ const citations = [];
371
372
  const formatted = sessions.map(session => {
372
373
  const metadata = session.metadata && typeof session.metadata === 'object'
373
374
  ? session.metadata
374
375
  : {};
376
+ const messageId = typeof metadata.searchMessageId === 'number' ? metadata.searchMessageId : undefined;
377
+ const role = typeof metadata.searchRole === 'string' ? metadata.searchRole : undefined;
378
+ const snippet = typeof metadata.searchSnippet === 'string' ? metadata.searchSnippet : '';
379
+ const citation = {
380
+ sessionId: session.id,
381
+ snippet,
382
+ label: messageId ? `session:${session.id}#message:${messageId}` : `session:${session.id}`,
383
+ };
384
+ if (messageId !== undefined)
385
+ citation.messageId = messageId;
386
+ if (role)
387
+ citation.role = role;
375
388
  const match = {
376
- snippet: metadata.searchSnippet ?? '',
389
+ snippet,
390
+ citation,
377
391
  };
378
- if (metadata.searchRole)
379
- match.role = metadata.searchRole;
392
+ if (role)
393
+ match.role = role;
380
394
  if (typeof metadata.searchScore === 'number')
381
395
  match.score = metadata.searchScore;
382
- if (typeof metadata.searchMessageId === 'number')
383
- match.messageId = metadata.searchMessageId;
396
+ if (messageId !== undefined)
397
+ match.messageId = messageId;
384
398
  if (metadata.parentSessionId)
385
399
  match.parentSessionId = metadata.parentSessionId;
400
+ citations.push(citation);
386
401
  return {
387
402
  id: session.id,
388
403
  name: session.name,
@@ -391,6 +406,7 @@ export class SessionToolExecutor {
391
406
  messageCount: Array.isArray(session.messages) ? session.messages.length : 0,
392
407
  createdAt: session.createdAt.toISOString(),
393
408
  lastAccessedAt: session.lastAccessedAt.toISOString(),
409
+ citation,
394
410
  match,
395
411
  };
396
412
  });
@@ -398,6 +414,7 @@ export class SessionToolExecutor {
398
414
  query,
399
415
  total: formatted.length,
400
416
  sessions: formatted,
417
+ citations,
401
418
  };
402
419
  return {
403
420
  success: true,
@@ -487,21 +487,35 @@ export class MCTS {
487
487
  findBestSolution() {
488
488
  if (!this.root)
489
489
  return null;
490
+ // Preferred tier: the best-scoring terminal-typed node (a concrete
491
+ // `implementation` or an explicit `conclusion`). Fallback tier: the
492
+ // best-scoring *derived* node of any type — many problems/models yield only
493
+ // analysis/verification/hypothesis thoughts, and a high-scoring one of those
494
+ // is still the best answer the search found. Without the fallback, such a
495
+ // search returns null even when a node scored 1.0. Both tiers exclude pruned
496
+ // nodes and the root (the problem statement, depth 0 — not a derived solution).
490
497
  let best = null;
491
498
  let bestScore = -1;
499
+ let fallback = null;
500
+ let fallbackScore = -1;
492
501
  const traverse = (node) => {
493
- if (node.state !== 'pruned' &&
494
- (node.type === 'implementation' || node.type === 'conclusion') &&
495
- node.score > bestScore) {
496
- bestScore = node.score;
497
- best = node;
502
+ if (node.state !== 'pruned' && node.depth > 0) {
503
+ if ((node.type === 'implementation' || node.type === 'conclusion') &&
504
+ node.score > bestScore) {
505
+ bestScore = node.score;
506
+ best = node;
507
+ }
508
+ if (node.score > fallbackScore) {
509
+ fallbackScore = node.score;
510
+ fallback = node;
511
+ }
498
512
  }
499
513
  for (const child of node.children) {
500
514
  traverse(child);
501
515
  }
502
516
  };
503
517
  traverse(this.root);
504
- return best;
518
+ return best ?? fallback;
505
519
  }
506
520
  /**
507
521
  * Get the path from root to a node
@@ -0,0 +1,71 @@
1
+ /**
2
+ * Session-end flush (WS3-T1 — Mémoire & continuité du run).
3
+ *
4
+ * When a session ends, persist what the next run needs to not start from
5
+ * zero:
6
+ * 1. a short HANDOFF file (`.codebuddy/HANDOFF.md`) — last goal, last
7
+ * state, files touched, open risks — written synchronously so it also
8
+ * works from process `exit` handlers;
9
+ * 2. reusable lesson candidates via the existing review-gated
10
+ * auto-proposer (PENDING only — a human approves them into lessons.md,
11
+ * which the per-turn `<lessons_context>` injection then re-injects on
12
+ * future runs);
13
+ * 3. declarative long-term memory candidates via the review-gated memory
14
+ * queue. These do not enter prompt-injected persistent memory until a
15
+ * human accepts them.
16
+ *
17
+ * Guard-rails: trivial sessions are skipped (no LLM call, no file), and
18
+ * everything written here passes the privacy lint first (PII/secret spans
19
+ * are redacted from the handoff; tainted lesson candidates are dropped by
20
+ * the proposer itself).
21
+ *
22
+ * @module agent/session-end-flush
23
+ */
24
+ import type { CodeBuddyClient } from '../codebuddy/client.js';
25
+ import type { ChatEntry } from './types.js';
26
+ export interface SessionEndFlushInput {
27
+ chatHistory: ChatEntry[];
28
+ workDir?: string;
29
+ /** Reuse the session's LLM client for the lesson proposal (no re-auth). */
30
+ client?: CodeBuddyClient;
31
+ sessionId?: string;
32
+ /** Session start (ms epoch) — used for the duration line of the handoff. */
33
+ startedAt?: number;
34
+ }
35
+ export interface SessionEndFlushResult {
36
+ /** Lesson candidates enqueued for review (`buddy lessons` to approve). */
37
+ proposedLessons: number;
38
+ /** Declarative memory candidates enqueued for review (`/memory candidates`). */
39
+ proposedMemories: number;
40
+ /** Absolute path of the handoff file, when one was (re)written. */
41
+ handoffPath?: string;
42
+ openRisks: string[];
43
+ skipped?: 'disabled' | 'trivial';
44
+ }
45
+ /**
46
+ * Heuristic open-risk extraction: error-ish tool results, deduped per tool,
47
+ * newest kept. No LLM involved — deterministic and free.
48
+ */
49
+ export declare function extractOpenRisks(history: ChatEntry[]): string[];
50
+ /** Files touched through write-ish tool calls, for the handoff. */
51
+ export declare function extractTouchedFiles(history: ChatEntry[]): string[];
52
+ export interface HandoffOptions {
53
+ sessionId?: string;
54
+ startedAt?: number;
55
+ /** Force a write even below the size threshold (used by tests). */
56
+ force?: boolean;
57
+ }
58
+ /**
59
+ * Write `.codebuddy/HANDOFF.md` synchronously. Returns the path when
60
+ * written, undefined when the session didn't warrant one. Idempotent per
61
+ * history array so the async flush and a process-exit fallback don't both
62
+ * write.
63
+ */
64
+ export declare function writeHandoffSync(history: ChatEntry[], workDir?: string, options?: HandoffOptions): string | undefined;
65
+ /**
66
+ * Run the complete session-end flush. Safe to call from any exit path:
67
+ * no-ops on trivial sessions, never throws.
68
+ */
69
+ export declare function runSessionEndFlush(input: SessionEndFlushInput): Promise<SessionEndFlushResult>;
70
+ /** Test hook: reset the per-history idempotence latch. */
71
+ export declare function resetSessionEndFlushState(): void;
@@ -0,0 +1,233 @@
1
+ /**
2
+ * Session-end flush (WS3-T1 — Mémoire & continuité du run).
3
+ *
4
+ * When a session ends, persist what the next run needs to not start from
5
+ * zero:
6
+ * 1. a short HANDOFF file (`.codebuddy/HANDOFF.md`) — last goal, last
7
+ * state, files touched, open risks — written synchronously so it also
8
+ * works from process `exit` handlers;
9
+ * 2. reusable lesson candidates via the existing review-gated
10
+ * auto-proposer (PENDING only — a human approves them into lessons.md,
11
+ * which the per-turn `<lessons_context>` injection then re-injects on
12
+ * future runs);
13
+ * 3. declarative long-term memory candidates via the review-gated memory
14
+ * queue. These do not enter prompt-injected persistent memory until a
15
+ * human accepts them.
16
+ *
17
+ * Guard-rails: trivial sessions are skipped (no LLM call, no file), and
18
+ * everything written here passes the privacy lint first (PII/secret spans
19
+ * are redacted from the handoff; tainted lesson candidates are dropped by
20
+ * the proposer itself).
21
+ *
22
+ * @module agent/session-end-flush
23
+ */
24
+ import * as fs from 'fs';
25
+ import * as path from 'path';
26
+ import { isFeatureEnabled } from '../config/feature-flags.js';
27
+ import { redactSecrets } from '../fleet/privacy-lint.js';
28
+ import { logger } from '../utils/logger.js';
29
+ // ============================================================================
30
+ // Gates & heuristics
31
+ // ============================================================================
32
+ /** Below this many assistant turns the session is too trivial to flush. */
33
+ const MIN_ASSISTANT_TURNS = 2;
34
+ /** Handoff is only worth writing past this transcript size (chars) … */
35
+ const HANDOFF_MIN_TRANSCRIPT_CHARS = 8_000;
36
+ /** … unless open risks were detected, which always deserve a handoff. */
37
+ const RISK_PATTERN = /\b(error|failed|failure|exception|denied|timeout|timed out|blocked|fatal|refused)\b/i;
38
+ const MAX_RISKS = 5;
39
+ const SNIPPET_MAX = 240;
40
+ function meaningfulTurns(history) {
41
+ let assistant = 0;
42
+ let tools = 0;
43
+ for (const e of history) {
44
+ if (e.type === 'assistant' && e.content.trim())
45
+ assistant++;
46
+ else if (e.type === 'tool_result')
47
+ tools++;
48
+ }
49
+ return { assistant, tools };
50
+ }
51
+ function firstLine(text) {
52
+ const line = text.trim().split('\n')[0] ?? '';
53
+ return line.length > SNIPPET_MAX ? `${line.slice(0, SNIPPET_MAX)}…` : line;
54
+ }
55
+ /**
56
+ * Heuristic open-risk extraction: error-ish tool results, deduped per tool,
57
+ * newest kept. No LLM involved — deterministic and free.
58
+ */
59
+ export function extractOpenRisks(history) {
60
+ const byKey = new Map();
61
+ for (const e of history) {
62
+ if (e.type !== 'tool_result')
63
+ continue;
64
+ if (!RISK_PATTERN.test(e.content))
65
+ continue;
66
+ const tool = e.toolCall?.function?.name || 'tool';
67
+ byKey.set(tool, `\`${tool}\`: ${firstLine(redactSecrets(e.content))}`);
68
+ }
69
+ return [...byKey.values()].slice(-MAX_RISKS);
70
+ }
71
+ /** Files touched through write-ish tool calls, for the handoff. */
72
+ export function extractTouchedFiles(history) {
73
+ const WRITE_TOOLS = new Set([
74
+ 'str_replace', 'str_replace_editor', 'create_file', 'write_file',
75
+ 'apply_patch', 'edit_file', 'text_editor',
76
+ ]);
77
+ const files = new Set();
78
+ for (const e of history) {
79
+ const calls = e.toolCalls ?? (e.toolCall ? [e.toolCall] : []);
80
+ for (const call of calls) {
81
+ if (!call?.function?.name || !WRITE_TOOLS.has(call.function.name))
82
+ continue;
83
+ try {
84
+ const args = JSON.parse(call.function.arguments || '{}');
85
+ const p = args.path ?? args.file_path ?? args.filePath;
86
+ if (typeof p === 'string' && p.trim())
87
+ files.add(p.trim());
88
+ }
89
+ catch { /* unparseable args — skip */ }
90
+ }
91
+ }
92
+ return [...files].slice(0, 20);
93
+ }
94
+ // ============================================================================
95
+ // Handoff (sync — usable from process exit handlers)
96
+ // ============================================================================
97
+ let handoffWrittenFor = null;
98
+ /**
99
+ * Write `.codebuddy/HANDOFF.md` synchronously. Returns the path when
100
+ * written, undefined when the session didn't warrant one. Idempotent per
101
+ * history array so the async flush and a process-exit fallback don't both
102
+ * write.
103
+ */
104
+ export function writeHandoffSync(history, workDir = process.cwd(), options = {}) {
105
+ if (handoffWrittenFor === history)
106
+ return undefined;
107
+ const { assistant } = meaningfulTurns(history);
108
+ if (assistant < MIN_ASSISTANT_TURNS)
109
+ return undefined;
110
+ const transcriptChars = history.reduce((n, e) => n + e.content.length, 0);
111
+ const risks = extractOpenRisks(history);
112
+ if (!options.force && transcriptChars < HANDOFF_MIN_TRANSCRIPT_CHARS && risks.length === 0) {
113
+ return undefined;
114
+ }
115
+ const lastUser = [...history].reverse().find((e) => e.type === 'user' && e.content.trim());
116
+ const lastAssistant = [...history].reverse().find((e) => e.type === 'assistant' && e.content.trim());
117
+ const touched = extractTouchedFiles(history);
118
+ const startedAt = options.startedAt;
119
+ const durationMin = startedAt ? Math.round((Date.now() - startedAt) / 60_000) : undefined;
120
+ const lines = [
121
+ '# Session Handoff',
122
+ '',
123
+ `> Auto-generated at session end — latest session wins. Read this before resuming work.`,
124
+ '',
125
+ `- Date: ${new Date().toISOString()}`,
126
+ ...(options.sessionId ? [`- Session: ${options.sessionId}`] : []),
127
+ ...(durationMin !== undefined ? [`- Durée: ~${durationMin} min`] : []),
128
+ `- Échanges: ${meaningfulTurns(history).assistant} réponses assistant, ${meaningfulTurns(history).tools} résultats d'outils`,
129
+ '',
130
+ '## Dernier objectif (utilisateur)',
131
+ '',
132
+ lastUser ? firstLine(redactSecrets(lastUser.content)) : '(aucun message utilisateur)',
133
+ '',
134
+ '## Dernier état (assistant)',
135
+ '',
136
+ lastAssistant ? redactSecrets(lastAssistant.content).trim().slice(0, 800) : '(aucune réponse)',
137
+ '',
138
+ ];
139
+ if (touched.length > 0) {
140
+ lines.push('## Fichiers touchés', '', ...touched.map((f) => `- \`${f}\``), '');
141
+ }
142
+ lines.push('## Risques ouverts', '');
143
+ if (risks.length > 0) {
144
+ lines.push(...risks.map((r) => `- ${r}`));
145
+ }
146
+ else {
147
+ lines.push('- Aucun détecté.');
148
+ }
149
+ lines.push('', '## Reprise', '', risks.length > 0
150
+ ? '- Commencer par lever les risques ouverts ci-dessus.'
151
+ : '- Continuer le dernier objectif, ou `buddy --continue` pour recharger la session.', '- Leçons en attente de revue : `buddy lessons` (candidats proposés en fin de session).', '- Mémoires en attente de revue : `/memory candidates` puis `/memory accept <id> <reviewer>`.', '');
152
+ const content = redactSecrets(lines.join('\n'));
153
+ const dir = path.join(workDir, '.codebuddy');
154
+ const target = path.join(dir, 'HANDOFF.md');
155
+ try {
156
+ fs.mkdirSync(dir, { recursive: true });
157
+ fs.writeFileSync(target, content, 'utf8');
158
+ handoffWrittenFor = history;
159
+ return target;
160
+ }
161
+ catch (err) {
162
+ logger.debug('[session-end-flush] handoff write failed', { err: String(err) });
163
+ return undefined;
164
+ }
165
+ }
166
+ // ============================================================================
167
+ // Full async flush (handoff + lesson proposal)
168
+ // ============================================================================
169
+ /**
170
+ * Run the complete session-end flush. Safe to call from any exit path:
171
+ * no-ops on trivial sessions, never throws.
172
+ */
173
+ export async function runSessionEndFlush(input) {
174
+ const empty = { proposedLessons: 0, proposedMemories: 0, openRisks: [] };
175
+ try {
176
+ if (!isFeatureEnabled('SESSION_END_FLUSH')) {
177
+ return { ...empty, skipped: 'disabled' };
178
+ }
179
+ const history = input.chatHistory ?? [];
180
+ const { assistant } = meaningfulTurns(history);
181
+ if (assistant < MIN_ASSISTANT_TURNS) {
182
+ return { ...empty, skipped: 'trivial' };
183
+ }
184
+ const workDir = input.workDir ?? process.cwd();
185
+ const openRisks = extractOpenRisks(history);
186
+ const handoffPath = writeHandoffSync(history, workDir, {
187
+ ...(input.sessionId !== undefined ? { sessionId: input.sessionId } : {}),
188
+ ...(input.startedAt !== undefined ? { startedAt: input.startedAt } : {}),
189
+ });
190
+ // Lesson proposal last — it is the only step that may call an LLM, so a
191
+ // killed process still leaves the handoff behind.
192
+ let proposedLessons = 0;
193
+ try {
194
+ const { proposeLessonsFromSession } = await import('./lesson-auto-proposer.js');
195
+ const proposed = await proposeLessonsFromSession(history, workDir, input.client);
196
+ proposedLessons = proposed.length;
197
+ }
198
+ catch (err) {
199
+ logger.debug('[session-end-flush] lesson proposal failed', { err: String(err) });
200
+ }
201
+ let proposedMemories = 0;
202
+ if (process.env.CODEBUDDY_MEMORY_AUTO_PROPOSE !== 'false') {
203
+ try {
204
+ const { proposeMemoryCandidatesFromSession } = await import('../memory/memory-auto-proposer.js');
205
+ const proposed = await proposeMemoryCandidatesFromSession(history, workDir, input.client, input.sessionId);
206
+ proposedMemories = proposed.length;
207
+ }
208
+ catch (err) {
209
+ logger.debug('[session-end-flush] memory proposal failed', { err: String(err) });
210
+ }
211
+ }
212
+ if (proposedLessons > 0 || proposedMemories > 0 || handoffPath) {
213
+ logger.info(`[session-end-flush] ${proposedLessons} lesson candidate(s) proposed` +
214
+ `, ${proposedMemories} memory candidate(s) proposed` +
215
+ (handoffPath ? `, handoff written to ${path.relative(workDir, handoffPath)}` : ''));
216
+ }
217
+ return {
218
+ proposedLessons,
219
+ proposedMemories,
220
+ ...(handoffPath ? { handoffPath } : {}),
221
+ openRisks,
222
+ };
223
+ }
224
+ catch (err) {
225
+ logger.debug('[session-end-flush] flush failed', { err: String(err) });
226
+ return empty;
227
+ }
228
+ }
229
+ /** Test hook: reset the per-history idempotence latch. */
230
+ export function resetSessionEndFlushState() {
231
+ handoffWrittenFor = null;
232
+ }
233
+ //# sourceMappingURL=session-end-flush.js.map
@@ -118,6 +118,12 @@ export interface AccumulatedMessage {
118
118
  content?: string;
119
119
  /** Accumulated tool calls */
120
120
  tool_calls?: CodeBuddyToolCall[];
121
+ /**
122
+ * The terminal `finish_reason`/`stop_reason` reported by the provider on the
123
+ * final chunk (`'stop'` | `'length'` | `'tool_calls'` | …). Used by the
124
+ * executor's in-loop recovery to detect a length-truncated turn.
125
+ */
126
+ finishReason?: string | null;
121
127
  /** Any additional fields from the API */
122
128
  [key: string]: unknown;
123
129
  }
@@ -153,6 +159,7 @@ export declare class StreamingHandler {
153
159
  private accumulatedMessage;
154
160
  private accumulatedRawContent;
155
161
  private accumulatedReasoningContent;
162
+ private accumulatedFinishReason;
156
163
  private tokenCounter;
157
164
  private lastTokenUpdate;
158
165
  private toolCallsYielded;
@@ -42,6 +42,7 @@ export class StreamingHandler {
42
42
  accumulatedMessage = {};
43
43
  accumulatedRawContent = '';
44
44
  accumulatedReasoningContent = '';
45
+ accumulatedFinishReason = null;
45
46
  tokenCounter = null;
46
47
  lastTokenUpdate = 0;
47
48
  toolCallsYielded = false;
@@ -86,6 +87,11 @@ export class StreamingHandler {
86
87
  }
87
88
  // Accumulate the message using reducer
88
89
  this.accumulatedMessage = reduceStreamChunk(this.accumulatedMessage, chunk);
90
+ // Capture the terminal finish_reason (present on the final chunk). The last
91
+ // non-null value wins so a trailing usage-only chunk can't clear it.
92
+ if (chunk.choices[0].finish_reason) {
93
+ this.accumulatedFinishReason = chunk.choices[0].finish_reason;
94
+ }
89
95
  // Extract content delta
90
96
  const rawContentDelta = chunk.choices[0].delta?.content || '';
91
97
  this.accumulatedRawContent += rawContentDelta;
@@ -208,7 +214,7 @@ export class StreamingHandler {
208
214
  * @returns The accumulated message with all processed content and tool calls
209
215
  */
210
216
  getAccumulatedMessage() {
211
- return { ...this.accumulatedMessage };
217
+ return { ...this.accumulatedMessage, finishReason: this.accumulatedFinishReason };
212
218
  }
213
219
  /**
214
220
  * Gets the raw accumulated content before sanitization.
@@ -261,6 +267,7 @@ export class StreamingHandler {
261
267
  this.accumulatedMessage = {};
262
268
  this.accumulatedRawContent = '';
263
269
  this.accumulatedReasoningContent = '';
270
+ this.accumulatedFinishReason = null;
264
271
  this.lastTokenUpdate = 0;
265
272
  this.toolCallsYielded = false;
266
273
  }
@@ -97,6 +97,7 @@ const TOOL_CATEGORY_MAP = {
97
97
  // Media
98
98
  screenshot: 'media',
99
99
  camera_snapshot: 'media',
100
+ camera_analyze: 'media',
100
101
  audio: 'media',
101
102
  video: 'media',
102
103
  ocr: 'media',
@@ -37,6 +37,11 @@ export interface ToolHandlerDependencies {
37
37
  /** Coordinates auto-repair attempts for failed commands */
38
38
  repairCoordinator: RepairCoordinator;
39
39
  }
40
+ export interface NormalizedHallucinatedToolCall {
41
+ toolName: string;
42
+ args: Record<string, unknown>;
43
+ }
44
+ export declare function normalizeHallucinatedLocalToolCall(toolName: string, args: Record<string, unknown>): NormalizedHallucinatedToolCall | null;
40
45
  /**
41
46
  * ToolHandler manages tool instantiation and execution
42
47
  *
@@ -16,7 +16,7 @@
16
16
  * Tool instances are lazy-loaded on first access for optimal startup time.
17
17
  */
18
18
  import { TextEditorTool, MorphEditorTool, ImageTool, BashTool, } from "../tools/index.js";
19
- import { getFormalToolRegistry, createTextEditorTools, createBashTools, createLsTools, createSearchTools, createWebTools, createTodoTools, createCronjobTools, createDockerTools, createKubernetesTools, createGitTools, createMiscTools, createBrowserTools, createProcessTools, createVisionTools, createScriptTools, createPlanTools, createKnowledgeTools, createRelationshipIntelligenceTools, createInternetScoutTools, createLeadScoutTools, createBrowserOperatorTools, createMemoryTools, createParallelTools, createAttentionTools, createSkillsInspectionTools, createLessonsTools, createUserModelTools, createAliasTools, createMultimodalTools, createAdvancedTools, createCanvasTools, createLspTools, createMergeConflictTools, createVulnScannerTools, createCodebaseReplaceTools, createAdvisorTools, createFleetTools, createAskUserQuestionTools, createExitPlanModeTools, createGuiTools, createSessionTools, createGitNexusTools, createScreenpipeTools, } from "../tools/registry/index.js";
19
+ import { getFormalToolRegistry, createTextEditorTools, createBashTools, createLsTools, createSearchTools, createWebTools, createTodoTools, createCronjobTools, createDockerTools, createKubernetesTools, createGitTools, createMiscTools, createBrowserTools, createProcessTools, createVisionTools, createScriptTools, createPlanTools, createKnowledgeTools, createRelationshipIntelligenceTools, createInternetScoutTools, createLeadScoutTools, createBrowserOperatorTools, createWindowsTools, createMemoryTools, createParallelTools, createAttentionTools, createSkillsInspectionTools, createLessonsTools, createUserModelTools, createAliasTools, createMultimodalTools, createAdvancedTools, createCanvasTools, createLspTools, createMergeConflictTools, createVulnScannerTools, createCodebaseReplaceTools, createAdvisorTools, createFleetTools, createAskUserQuestionTools, createExitPlanModeTools, createGuiTools, createSessionTools, createGitNexusTools, createScreenpipeTools, } from "../tools/registry/index.js";
20
20
  import { getMCPManager } from "../codebuddy/tools.js";
21
21
  import { getErrorMessage } from "../errors/index.js";
22
22
  import { logger } from "../utils/logger.js";
@@ -44,6 +44,67 @@ const READ_ONLY_FILE_TOOLS = new Set([
44
44
  'find_definition',
45
45
  'search_multi',
46
46
  ]);
47
+ export function normalizeHallucinatedLocalToolCall(toolName, args) {
48
+ const normalizedName = toolName.toLowerCase().replace(/\s+/g, '');
49
+ const thoughtToolCallSuffix = normalizedName.match(/^thought_tool_call_\d+(?:_\d+)*\.(bash|create_file|write_file|view_file|read_file)$/)?.[1];
50
+ if (thoughtToolCallSuffix === 'bash') {
51
+ const command = firstStringValue(args.command, args.cmd, args.shell_command, args.command_to_execute, args.code);
52
+ return {
53
+ toolName: 'bash',
54
+ args: command ? { ...args, command } : args,
55
+ };
56
+ }
57
+ if (thoughtToolCallSuffix === 'create_file' || thoughtToolCallSuffix === 'write_file') {
58
+ const path = firstStringValue(args.path, args.file_path, args.target_file, args.filename, args.file);
59
+ const content = firstStringValue(args.content, args.file_content, args.contents, args.text);
60
+ return {
61
+ toolName: 'create_file',
62
+ args: {
63
+ ...args,
64
+ ...(path ? { path } : {}),
65
+ ...(content !== undefined ? { content } : {}),
66
+ },
67
+ };
68
+ }
69
+ if (thoughtToolCallSuffix === 'view_file' || thoughtToolCallSuffix === 'read_file') {
70
+ const path = firstStringValue(args.path, args.file_path, args.target_file, args.filename, args.file);
71
+ return {
72
+ toolName: 'view_file',
73
+ args: path ? { ...args, path } : args,
74
+ };
75
+ }
76
+ if (normalizedName.includes('bash_tool:execute') ||
77
+ normalizedName.includes('thought-tool:execute_command') ||
78
+ normalizedName === 'execute_command') {
79
+ const command = firstStringValue(args.command, args.cmd, args.shell_command, args.command_to_execute, args.code);
80
+ return {
81
+ toolName: 'bash',
82
+ args: command ? { ...args, command } : args,
83
+ };
84
+ }
85
+ if (normalizedName.includes('bash_tool:write_file') ||
86
+ normalizedName.includes('thought-tool:write_file') ||
87
+ normalizedName.includes('call:bash_tool:write_file')) {
88
+ const path = firstStringValue(args.path, args.file_path, args.target_file, args.filename, args.file);
89
+ const content = firstStringValue(args.content, args.file_content, args.contents, args.text);
90
+ return {
91
+ toolName: 'create_file',
92
+ args: {
93
+ ...args,
94
+ ...(path ? { path } : {}),
95
+ ...(content !== undefined ? { content } : {}),
96
+ },
97
+ };
98
+ }
99
+ return null;
100
+ }
101
+ function firstStringValue(...values) {
102
+ for (const value of values) {
103
+ if (typeof value === 'string' && value.trim())
104
+ return value;
105
+ }
106
+ return undefined;
107
+ }
47
108
  /**
48
109
  * ToolHandler manages tool instantiation and execution
49
110
  *
@@ -156,6 +217,7 @@ export class ToolHandler {
156
217
  ...createInternetScoutTools(),
157
218
  ...createLeadScoutTools(),
158
219
  ...createBrowserOperatorTools(),
220
+ ...(process.platform === 'win32' ? createWindowsTools() : []),
159
221
  ...createMemoryTools(),
160
222
  ...createParallelTools(),
161
223
  ...createAttentionTools(),
@@ -244,8 +306,25 @@ export class ToolHandler {
244
306
  const startTime = Date.now();
245
307
  const hooksManager = getToolHooksManager();
246
308
  try {
247
- const args = JSON.parse(toolCall.function.arguments);
248
- const toolName = toolCall.function.name;
309
+ let args = JSON.parse(toolCall.function.arguments);
310
+ let toolName = toolCall.function.name;
311
+ const normalized = normalizeHallucinatedLocalToolCall(toolName, args);
312
+ if (normalized) {
313
+ logger.debug('Normalized hallucinated local tool name', {
314
+ from: toolName,
315
+ to: normalized.toolName,
316
+ });
317
+ toolName = normalized.toolName;
318
+ args = normalized.args;
319
+ toolCall = {
320
+ ...toolCall,
321
+ function: {
322
+ ...toolCall.function,
323
+ name: toolName,
324
+ arguments: JSON.stringify(args),
325
+ },
326
+ };
327
+ }
249
328
  // Create hook context
250
329
  let hookContext = {
251
330
  toolName,
@@ -693,6 +772,12 @@ export class ToolHandler {
693
772
  */
694
773
  async executeBashWithHooks(args, context) {
695
774
  const command = args.command;
775
+ if (typeof command !== 'string' || command.trim() === '') {
776
+ return {
777
+ success: false,
778
+ error: 'bash: missing a non-empty "command" string argument. Put the shell command to run in the "command" field.',
779
+ };
780
+ }
696
781
  // Execute pre-bash hooks
697
782
  try {
698
783
  await this.deps.hooksManager.executeHooks("pre-bash", { command });
@@ -754,6 +839,15 @@ export class ToolHandler {
754
839
  const args = JSON.parse(toolCall.function.arguments);
755
840
  const command = args.command;
756
841
  const timeout = args.timeout || 30000;
842
+ // A weaker model can emit a `bash` call without a `command` (or a
843
+ // non-string one). Fail with a clear, recoverable message instead of
844
+ // crashing downstream on `command.trim()` (auto-repair can then re-prompt).
845
+ if (typeof command !== 'string' || command.trim() === '') {
846
+ return {
847
+ success: false,
848
+ error: 'bash: missing a non-empty "command" string argument. Put the shell command to run in the "command" field.',
849
+ };
850
+ }
757
851
  const gen = this.bash.executeStreaming(command, timeout);
758
852
  let result = await gen.next();
759
853
  while (!result.done) {
@@ -778,8 +872,15 @@ export class ToolHandler {
778
872
  mode: mode,
779
873
  });
780
874
  reasoner.setMode(mode);
875
+ const problem = args.problem;
876
+ if (typeof problem !== 'string' || problem.trim() === '') {
877
+ return {
878
+ success: false,
879
+ error: 'reason: missing a non-empty "problem" string argument. Put the problem to reason about in the "problem" field.',
880
+ };
881
+ }
781
882
  const gen = reasoner.solveStreaming({
782
- description: args.problem,
883
+ description: problem,
783
884
  context: args.context,
784
885
  constraints: args.constraints,
785
886
  });
@@ -796,6 +897,31 @@ export class ToolHandler {
796
897
  return { success: false, error: `Reasoning error: ${getErrorMessage(error)}` };
797
898
  }
798
899
  }
900
+ // generate_document: stream the build phases (prepare → parse → build →
901
+ // write → verify → done) so the UI shows the steps unfold one by one
902
+ // instead of a single opaque "Created X" at the end.
903
+ if (toolName === 'generate_document') {
904
+ try {
905
+ const args = JSON.parse(toolCall.function.arguments);
906
+ const { executeGenerateDocumentStreaming } = await import('../tools/document-generator.js');
907
+ const gen = executeGenerateDocumentStreaming({
908
+ type: args.type,
909
+ title: args.title,
910
+ content: args.content,
911
+ outputPath: args.outputPath ?? args.output_path,
912
+ theme: args.theme,
913
+ });
914
+ let r = await gen.next();
915
+ while (!r.done) {
916
+ yield r.value;
917
+ r = await gen.next();
918
+ }
919
+ return r.value ?? { success: false, error: 'Document tool returned no result' };
920
+ }
921
+ catch (error) {
922
+ return { success: false, error: `Document streaming error: ${getErrorMessage(error)}` };
923
+ }
924
+ }
799
925
  // Fallback: non-streaming execution
800
926
  return await this.executeTool(toolCall);
801
927
  }