mixdog 0.9.3 → 0.9.5

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 (382) hide show
  1. package/README.md +112 -38
  2. package/package.json +10 -3
  3. package/scripts/bench/lead-review-tasks-r3.json +20 -0
  4. package/scripts/bench/lead-review-tasks.json +20 -0
  5. package/scripts/bench/r4-mixed-tasks.json +20 -0
  6. package/scripts/bench/r5-orchestrated-task.json +7 -0
  7. package/scripts/bench/review-tasks.json +20 -0
  8. package/scripts/bench/round-codex.json +114 -0
  9. package/scripts/bench/round-mixdog-lead-r3.json +269 -0
  10. package/scripts/bench/round-mixdog-lead.json +269 -0
  11. package/scripts/bench/round-mixdog.json +126 -0
  12. package/scripts/bench/round-r10-bigsample.json +679 -0
  13. package/scripts/bench/round-r11-codexalign.json +257 -0
  14. package/scripts/bench/round-r13-clientmeta.json +464 -0
  15. package/scripts/bench/round-r14-betafeatures.json +466 -0
  16. package/scripts/bench/round-r15-fulldefault.json +462 -0
  17. package/scripts/bench/round-r16-sessionid.json +466 -0
  18. package/scripts/bench/round-r17-wirebytes.json +456 -0
  19. package/scripts/bench/round-r18-prewarm.json +468 -0
  20. package/scripts/bench/round-r19-clean.json +472 -0
  21. package/scripts/bench/round-r20-prewarm-clean.json +475 -0
  22. package/scripts/bench/round-r21-delta-retry.json +473 -0
  23. package/scripts/bench/round-r22-full-probe.json +693 -0
  24. package/scripts/bench/round-r23-itemprobe.json +701 -0
  25. package/scripts/bench/round-r24-shapefix.json +677 -0
  26. package/scripts/bench/round-r25-serial.json +464 -0
  27. package/scripts/bench/round-r26-parallel3.json +671 -0
  28. package/scripts/bench/round-r27-parallel10.json +894 -0
  29. package/scripts/bench/round-r28-parallel10-stagger.json +882 -0
  30. package/scripts/bench/round-r29-parallel10-stagger166.json +886 -0
  31. package/scripts/bench/round-r30-instid.json +253 -0
  32. package/scripts/bench/round-r31-upgradeprobe.json +256 -0
  33. package/scripts/bench/round-r32-vs-codex-lead.json +254 -0
  34. package/scripts/bench/round-r33-vs-codex-codex.json +115 -0
  35. package/scripts/bench/round-r34-orchestrated.json +120 -0
  36. package/scripts/bench/round-r35-orchestrated-codex.json +61 -0
  37. package/scripts/bench/round-r36-orchestrated-capped.json +128 -0
  38. package/scripts/bench/round-r4-codex.json +114 -0
  39. package/scripts/bench/round-r4-mixed.json +225 -0
  40. package/scripts/bench/round-r5-gpt-lead.json +259 -0
  41. package/scripts/bench/round-r6-codex.json +114 -0
  42. package/scripts/bench/round-r6-solo.json +257 -0
  43. package/scripts/bench/round-r7-full.json +254 -0
  44. package/scripts/bench/round-r8-fulldefault.json +255 -0
  45. package/scripts/bench-run.mjs +251 -32
  46. package/scripts/freevar-smoke.mjs +95 -0
  47. package/scripts/internal-comms-bench.mjs +3 -4
  48. package/scripts/internal-comms-smoke.mjs +10 -9
  49. package/scripts/model-catalog-audit.mjs +209 -0
  50. package/scripts/model-list-sanitize-test.mjs +37 -0
  51. package/scripts/mouse-probe.mjs +45 -0
  52. package/scripts/output-style-bench.mjs +13 -6
  53. package/scripts/output-style-smoke.mjs +4 -4
  54. package/scripts/provider-toolcall-test.mjs +7 -3
  55. package/scripts/recall-bench.mjs +76 -13
  56. package/scripts/recall-quality-cases.json +12 -0
  57. package/scripts/recall-usecase-cases.json +18 -0
  58. package/scripts/session-bench.mjs +152 -6
  59. package/scripts/tool-smoke.mjs +25 -65
  60. package/scripts/tui-render-smoke.mjs +90 -0
  61. package/scripts/webhook-smoke.mjs +208 -0
  62. package/src/agents/debugger/AGENT.md +4 -1
  63. package/src/agents/heavy-worker/AGENT.md +9 -8
  64. package/src/agents/maintainer/AGENT.md +4 -0
  65. package/src/agents/reviewer/AGENT.md +2 -1
  66. package/src/agents/scheduler-task/AGENT.md +2 -3
  67. package/src/agents/webhook-handler/AGENT.md +2 -3
  68. package/src/agents/worker/AGENT.md +10 -7
  69. package/src/app.mjs +12 -1
  70. package/src/headless-role.mjs +7 -1
  71. package/src/lib/rules-builder.cjs +4 -0
  72. package/src/mixdog-session-runtime.mjs +647 -2056
  73. package/src/output-styles/default.md +30 -9
  74. package/src/output-styles/{oneline.md → extreme-minimal.md} +5 -4
  75. package/src/output-styles/minimal.md +8 -6
  76. package/src/output-styles/simple.md +21 -7
  77. package/src/rules/agent/00-common.md +6 -3
  78. package/src/rules/agent/30-explorer.md +16 -5
  79. package/src/rules/lead/01-general.md +5 -5
  80. package/src/rules/lead/lead-brief.md +15 -0
  81. package/src/rules/lead/lead-tool.md +6 -15
  82. package/src/rules/shared/01-tool.md +17 -21
  83. package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch.mjs +8 -3
  84. package/src/runtime/agent/orchestrator/agent-runtime/agent-loop-policy.mjs +25 -0
  85. package/src/runtime/agent/orchestrator/agent-runtime/cache-strategy.mjs +100 -23
  86. package/src/runtime/agent/orchestrator/agent-runtime/session-builder.mjs +6 -15
  87. package/src/runtime/agent/orchestrator/agent-trace-format.mjs +362 -0
  88. package/src/runtime/agent/orchestrator/agent-trace-io.mjs +410 -0
  89. package/src/runtime/agent/orchestrator/agent-trace.mjs +16 -735
  90. package/src/runtime/agent/orchestrator/config.mjs +69 -2
  91. package/src/runtime/agent/orchestrator/providers/anthropic-effort.mjs +62 -20
  92. package/src/runtime/agent/orchestrator/providers/anthropic-model-resolve.mjs +209 -0
  93. package/src/runtime/agent/orchestrator/providers/anthropic-oauth-credentials.mjs +489 -0
  94. package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +81 -1281
  95. package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +607 -0
  96. package/src/runtime/agent/orchestrator/providers/anthropic.mjs +32 -3
  97. package/src/runtime/agent/orchestrator/providers/codex-client-meta.mjs +81 -0
  98. package/src/runtime/agent/orchestrator/providers/gemini-cache.mjs +248 -0
  99. package/src/runtime/agent/orchestrator/providers/gemini-schema.mjs +303 -0
  100. package/src/runtime/agent/orchestrator/providers/gemini-stream.mjs +505 -0
  101. package/src/runtime/agent/orchestrator/providers/gemini.mjs +43 -1013
  102. package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +17 -3
  103. package/src/runtime/agent/orchestrator/providers/model-catalog.mjs +105 -11
  104. package/src/runtime/agent/orchestrator/providers/model-list-sanitize.mjs +356 -0
  105. package/src/runtime/agent/orchestrator/providers/openai-codex-model.mjs +108 -0
  106. package/src/runtime/agent/orchestrator/providers/openai-compat-trace.mjs +58 -0
  107. package/src/runtime/agent/orchestrator/providers/openai-compat-wire.mjs +368 -0
  108. package/src/runtime/agent/orchestrator/providers/openai-compat-xai.mjs +760 -0
  109. package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +40 -1143
  110. package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +740 -0
  111. package/src/runtime/agent/orchestrator/providers/openai-oauth-login.mjs +193 -0
  112. package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +349 -2131
  113. package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +143 -1002
  114. package/src/runtime/agent/orchestrator/providers/openai-ws-delta.mjs +229 -0
  115. package/src/runtime/agent/orchestrator/providers/openai-ws-events.mjs +67 -0
  116. package/src/runtime/agent/orchestrator/providers/openai-ws-pool.mjs +465 -0
  117. package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +1105 -0
  118. package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +2 -1
  119. package/src/runtime/agent/orchestrator/providers/provider-catalog-cache.mjs +80 -0
  120. package/src/runtime/agent/orchestrator/session/compact/budget.mjs +288 -0
  121. package/src/runtime/agent/orchestrator/session/compact/constants.mjs +85 -0
  122. package/src/runtime/agent/orchestrator/session/compact/engine.mjs +749 -0
  123. package/src/runtime/agent/orchestrator/session/compact/messages.mjs +82 -0
  124. package/src/runtime/agent/orchestrator/session/compact/summary-schema.mjs +315 -0
  125. package/src/runtime/agent/orchestrator/session/compact/summary.mjs +643 -0
  126. package/src/runtime/agent/orchestrator/session/compact/text-utils.mjs +326 -0
  127. package/src/runtime/agent/orchestrator/session/compact.mjs +40 -2282
  128. package/src/runtime/agent/orchestrator/session/loop/compact-policy.mjs +14 -2
  129. package/src/runtime/agent/orchestrator/session/loop/completion-guards.mjs +61 -0
  130. package/src/runtime/agent/orchestrator/session/loop/pre-dispatch-deny.mjs +1 -3
  131. package/src/runtime/agent/orchestrator/session/loop/recall-fasttrack.mjs +275 -0
  132. package/src/runtime/agent/orchestrator/session/loop/steering-ladder.mjs +173 -0
  133. package/src/runtime/agent/orchestrator/session/loop/termination.mjs +58 -0
  134. package/src/runtime/agent/orchestrator/session/loop/tool-exec.mjs +239 -0
  135. package/src/runtime/agent/orchestrator/session/loop.mjs +278 -402
  136. package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +471 -0
  137. package/src/runtime/agent/orchestrator/session/manager/context-meta.mjs +7 -4
  138. package/src/runtime/agent/orchestrator/session/manager/prompt-utils.mjs +12 -0
  139. package/src/runtime/agent/orchestrator/session/manager/runtime-liveness.mjs +406 -0
  140. package/src/runtime/agent/orchestrator/session/manager/status-telemetry.mjs +80 -0
  141. package/src/runtime/agent/orchestrator/session/manager/usage-metrics.mjs +210 -0
  142. package/src/runtime/agent/orchestrator/session/manager.mjs +166 -1087
  143. package/src/runtime/agent/orchestrator/session/store-summary-index.mjs +189 -0
  144. package/src/runtime/agent/orchestrator/session/store.mjs +74 -179
  145. package/src/runtime/agent/orchestrator/stall-policy.mjs +20 -1
  146. package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +70 -20
  147. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +22 -2
  148. package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +35 -44
  149. package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +40 -0
  150. package/src/runtime/agent/orchestrator/tools/builtin/rg-runner.mjs +29 -0
  151. package/src/runtime/agent/orchestrator/tools/builtin/search-builders.mjs +8 -0
  152. package/src/runtime/agent/orchestrator/tools/builtin/search-path-diagnostics.mjs +126 -0
  153. package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +81 -92
  154. package/src/runtime/agent/orchestrator/tools/builtin/shell-job-paths.mjs +161 -0
  155. package/src/runtime/agent/orchestrator/tools/builtin/shell-job-process.mjs +108 -0
  156. package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +28 -265
  157. package/src/runtime/agent/orchestrator/tools/builtin.mjs +0 -6
  158. package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +57 -3
  159. package/src/runtime/agent/orchestrator/tools/code-graph/keyword-match.mjs +82 -0
  160. package/src/runtime/agent/orchestrator/tools/code-graph/search.mjs +10 -122
  161. package/src/runtime/agent/orchestrator/tools/code-graph/text-columns.mjs +45 -0
  162. package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +6 -6
  163. package/src/runtime/agent/orchestrator/tools/graph-binary-fetcher.mjs +6 -3
  164. package/src/runtime/agent/orchestrator/tools/patch/constants.mjs +9 -0
  165. package/src/runtime/agent/orchestrator/tools/patch/dispatch.mjs +171 -0
  166. package/src/runtime/agent/orchestrator/tools/patch/matcher.mjs +471 -0
  167. package/src/runtime/agent/orchestrator/tools/patch/native-server.mjs +436 -0
  168. package/src/runtime/agent/orchestrator/tools/patch/orchestrator.mjs +342 -0
  169. package/src/runtime/agent/orchestrator/tools/patch/parsing.mjs +359 -0
  170. package/src/runtime/agent/orchestrator/tools/patch/paths.mjs +340 -0
  171. package/src/runtime/agent/orchestrator/tools/patch/v4a-convert.mjs +643 -0
  172. package/src/runtime/agent/orchestrator/tools/patch.mjs +36 -2959
  173. package/src/runtime/agent/orchestrator/tools/progress-message.mjs +0 -21
  174. package/src/runtime/agent/orchestrator/tools/shell-command.mjs +9 -72
  175. package/src/runtime/agent/orchestrator/tools/shell-powershell.mjs +77 -0
  176. package/src/runtime/agent/orchestrator/tools/shell-state.mjs +154 -0
  177. package/src/runtime/channels/backends/discord-access.mjs +32 -0
  178. package/src/runtime/channels/backends/discord-attachments.mjs +65 -0
  179. package/src/runtime/channels/backends/discord-gateway.mjs +233 -0
  180. package/src/runtime/channels/backends/discord.mjs +27 -318
  181. package/src/runtime/channels/backends/telegram.mjs +8 -12
  182. package/src/runtime/channels/index.mjs +247 -701
  183. package/src/runtime/channels/lib/backend-dispatch.mjs +46 -0
  184. package/src/runtime/channels/lib/config.mjs +37 -149
  185. package/src/runtime/channels/lib/event-pipeline.mjs +22 -5
  186. package/src/runtime/channels/lib/event-queue.mjs +78 -13
  187. package/src/runtime/channels/lib/inbound-routing.mjs +74 -0
  188. package/src/runtime/channels/lib/interaction-workflows.mjs +5 -113
  189. package/src/runtime/channels/lib/output-forwarder.mjs +1 -1
  190. package/src/runtime/channels/lib/owner-heartbeat.mjs +75 -0
  191. package/src/runtime/channels/lib/parent-bridge.mjs +88 -0
  192. package/src/runtime/channels/lib/runtime-paths.mjs +14 -4
  193. package/src/runtime/channels/lib/scheduler.mjs +27 -113
  194. package/src/runtime/channels/lib/session-discovery.mjs +56 -4
  195. package/src/runtime/channels/lib/tool-dispatch.mjs +158 -0
  196. package/src/runtime/channels/lib/tool-format.mjs +1 -1
  197. package/src/runtime/channels/lib/transcript-discovery.mjs +4 -4
  198. package/src/runtime/channels/lib/voice-runtime-fetcher.mjs +6 -3
  199. package/src/runtime/channels/lib/voice-transcription.mjs +179 -0
  200. package/src/runtime/channels/lib/webhook/deliveries.mjs +313 -0
  201. package/src/runtime/channels/lib/webhook/log.mjs +42 -0
  202. package/src/runtime/channels/lib/webhook/ngrok.mjs +181 -0
  203. package/src/runtime/channels/lib/webhook/signature.mjs +60 -0
  204. package/src/runtime/channels/lib/webhook.mjs +43 -616
  205. package/src/runtime/channels/tool-defs.mjs +11 -130
  206. package/src/runtime/memory/index.mjs +210 -1948
  207. package/src/runtime/memory/lib/core-memory-store.mjs +5 -1
  208. package/src/runtime/memory/lib/cycle-llm-adapters.mjs +58 -0
  209. package/src/runtime/memory/lib/cycle-scheduler.mjs +497 -0
  210. package/src/runtime/memory/lib/embedding-warmup.mjs +58 -0
  211. package/src/runtime/memory/lib/ko-morph.mjs +195 -0
  212. package/src/runtime/memory/lib/memory-config-flags.mjs +91 -0
  213. package/src/runtime/memory/lib/memory-cycle.mjs +1 -1
  214. package/src/runtime/memory/lib/memory-cycle2-gate.mjs +515 -0
  215. package/src/runtime/memory/lib/memory-cycle2-mutations.mjs +324 -0
  216. package/src/runtime/memory/lib/memory-cycle2-shared.mjs +18 -0
  217. package/src/runtime/memory/lib/memory-cycle2.mjs +24 -842
  218. package/src/runtime/memory/lib/memory-embed.mjs +149 -0
  219. package/src/runtime/memory/lib/memory-process-lock.mjs +162 -0
  220. package/src/runtime/memory/lib/memory-recall-store.mjs +69 -12
  221. package/src/runtime/memory/lib/memory-text-utils.mjs +46 -0
  222. package/src/runtime/memory/lib/pg/supervisor.mjs +1 -1
  223. package/src/runtime/memory/lib/query-handlers.mjs +802 -0
  224. package/src/runtime/memory/lib/recall-format.mjs +55 -0
  225. package/src/runtime/memory/lib/runtime-fetcher.mjs +8 -3
  226. package/src/runtime/memory/lib/transcript-ingest.mjs +425 -0
  227. package/src/runtime/memory/tool-defs.mjs +5 -13
  228. package/src/runtime/search/lib/http-fetch.mjs +274 -0
  229. package/src/runtime/search/lib/ssrf-guard.mjs +333 -0
  230. package/src/runtime/search/lib/web-tools.mjs +24 -602
  231. package/src/runtime/shared/atomic-file.mjs +26 -1
  232. package/src/runtime/shared/config.mjs +14 -4
  233. package/src/runtime/shared/launcher-control.mjs +2 -2
  234. package/src/runtime/shared/markdown-frontmatter.mjs +19 -0
  235. package/src/runtime/shared/schedules-store.mjs +13 -3
  236. package/src/runtime/shared/tool-execution-contract.mjs +2 -2
  237. package/src/runtime/shared/tool-primitives.mjs +308 -0
  238. package/src/runtime/shared/tool-result-summary.mjs +515 -0
  239. package/src/runtime/shared/tool-surface.mjs +80 -898
  240. package/src/runtime/shared/transcript-writer.mjs +23 -0
  241. package/src/runtime/shared/update-checker.mjs +7 -4
  242. package/src/session-runtime/config-helpers.mjs +119 -2
  243. package/src/session-runtime/config-lifecycle.mjs +232 -0
  244. package/src/session-runtime/cwd-plugins.mjs +226 -0
  245. package/src/session-runtime/mcp-glue.mjs +177 -0
  246. package/src/session-runtime/model-recency.mjs +111 -0
  247. package/src/session-runtime/native-search.mjs +247 -0
  248. package/src/session-runtime/output-styles.mjs +11 -9
  249. package/src/session-runtime/prewarm.mjs +142 -0
  250. package/src/session-runtime/provider-models.mjs +278 -0
  251. package/src/session-runtime/provider-usage.mjs +120 -0
  252. package/src/session-runtime/quick-model-rows.mjs +205 -0
  253. package/src/session-runtime/quick-search-models.mjs +47 -0
  254. package/src/session-runtime/session-hooks.mjs +93 -0
  255. package/src/session-runtime/settings-api.mjs +352 -0
  256. package/src/session-runtime/tool-catalog.mjs +29 -29
  257. package/src/session-runtime/tool-defs.mjs +84 -0
  258. package/src/session-runtime/warmup-schedulers.mjs +201 -0
  259. package/src/session-runtime/workflow.mjs +1 -1
  260. package/src/standalone/agent-tool/helpers.mjs +237 -0
  261. package/src/standalone/agent-tool/notify.mjs +107 -0
  262. package/src/standalone/agent-tool/provider-init.mjs +143 -0
  263. package/src/standalone/agent-tool/render.mjs +152 -0
  264. package/src/standalone/agent-tool/tool-def.mjs +55 -0
  265. package/src/standalone/agent-tool.mjs +138 -669
  266. package/src/standalone/channel-admin.mjs +102 -90
  267. package/src/standalone/channel-worker.mjs +4 -7
  268. package/src/standalone/explore-tool.mjs +64 -14
  269. package/src/standalone/hook-bus/config.mjs +207 -0
  270. package/src/standalone/hook-bus/constants.mjs +90 -0
  271. package/src/standalone/hook-bus/handlers.mjs +481 -0
  272. package/src/standalone/hook-bus/payload.mjs +31 -0
  273. package/src/standalone/hook-bus/rules.mjs +77 -0
  274. package/src/standalone/hook-bus.mjs +77 -870
  275. package/src/standalone/memory-runtime-proxy.mjs +7 -0
  276. package/src/standalone/opencode-go-login.mjs +5 -1
  277. package/src/standalone/provider-admin.mjs +1 -16
  278. package/src/standalone/usage-dashboard.mjs +3 -1
  279. package/src/tui/App.jsx +1059 -8110
  280. package/src/tui/app/app-format.mjs +213 -0
  281. package/src/tui/app/channel-pickers.mjs +508 -0
  282. package/src/tui/app/clipboard.mjs +67 -0
  283. package/src/tui/app/core-memory-picker.mjs +210 -0
  284. package/src/tui/app/extension-pickers.mjs +506 -0
  285. package/src/tui/app/input-parsers.mjs +193 -0
  286. package/src/tui/app/maintenance-pickers.mjs +356 -0
  287. package/src/tui/app/model-options.mjs +334 -0
  288. package/src/tui/app/model-picker.mjs +365 -0
  289. package/src/tui/app/onboarding-steps.mjs +400 -0
  290. package/src/tui/app/project-picker.mjs +247 -0
  291. package/src/tui/app/provider-setup-picker.mjs +580 -0
  292. package/src/tui/app/resume-picker.mjs +55 -0
  293. package/src/tui/app/route-pickers.mjs +419 -0
  294. package/src/tui/app/settings-picker.mjs +489 -0
  295. package/src/tui/app/slash-commands.mjs +101 -0
  296. package/src/tui/app/slash-dispatch.mjs +427 -0
  297. package/src/tui/app/text-layout.mjs +46 -0
  298. package/src/tui/app/theme-effort-pickers.mjs +154 -0
  299. package/src/tui/app/transcript-window.mjs +677 -0
  300. package/src/tui/app/use-mouse-input.mjs +460 -0
  301. package/src/tui/app/use-prompt-handlers.mjs +310 -0
  302. package/src/tui/app/use-transcript-scroll.mjs +512 -0
  303. package/src/tui/app/use-transcript-window.mjs +607 -0
  304. package/src/tui/components/ConfirmBar.jsx +10 -7
  305. package/src/tui/components/Picker.jsx +64 -15
  306. package/src/tui/components/PromptInput.jsx +33 -102
  307. package/src/tui/components/SlashCommandPalette.jsx +8 -1
  308. package/src/tui/components/StatusLine.jsx +69 -15
  309. package/src/tui/components/TextEntryPanel.jsx +11 -0
  310. package/src/tui/components/ToolExecution.jsx +52 -594
  311. package/src/tui/components/TranscriptItem.jsx +105 -0
  312. package/src/tui/components/UsagePanel.jsx +18 -4
  313. package/src/tui/components/prompt-input/edit-helpers.mjs +72 -0
  314. package/src/tui/components/prompt-input/voice-indicator.mjs +39 -0
  315. package/src/tui/components/tool-execution/ResultBody.jsx +56 -0
  316. package/src/tui/components/tool-execution/surface-detail.mjs +405 -0
  317. package/src/tui/components/tool-execution/text-format.mjs +161 -0
  318. package/src/tui/display-width.mjs +20 -3
  319. package/src/tui/dist/index.mjs +13553 -12384
  320. package/src/tui/engine/agent-job-feed.mjs +133 -0
  321. package/src/tui/engine/notification-plan.mjs +76 -0
  322. package/src/tui/engine/render-timing.mjs +17 -0
  323. package/src/tui/engine/tool-approval.mjs +94 -0
  324. package/src/tui/engine/tool-card-results.mjs +234 -0
  325. package/src/tui/engine/tool-result-status.mjs +135 -0
  326. package/src/tui/engine.mjs +170 -574
  327. package/src/tui/figures.mjs +5 -0
  328. package/src/tui/index.jsx +65 -1
  329. package/src/tui/input-editing.mjs +2 -2
  330. package/src/tui/markdown/format-token.mjs +4 -1
  331. package/src/tui/statusline-ansi-bridge.mjs +11 -3
  332. package/src/tui/theme.mjs +6 -0
  333. package/src/ui/statusline-agents.mjs +213 -0
  334. package/src/ui/statusline-format.mjs +146 -0
  335. package/src/ui/statusline-segments.mjs +148 -0
  336. package/src/ui/statusline.mjs +77 -501
  337. package/src/ui/tool-card.mjs +0 -1
  338. package/src/vendor/statusline/bin/statusline-route.mjs +15 -2
  339. package/src/workflows/default/WORKFLOW.md +16 -18
  340. package/src/workflows/sequential/WORKFLOW.md +16 -18
  341. package/vendor/ink/build/display-width.js +19 -3
  342. package/vendor/ink/build/ink.js +112 -7
  343. package/vendor/ink/build/log-update.js +17 -3
  344. package/vendor/ink/build/wrap-text.js +125 -0
  345. package/scripts/_test-folder-dialog.mjs +0 -30
  346. package/scripts/fix-brief-fn.mjs +0 -35
  347. package/scripts/fix-format-tool-surface.mjs +0 -24
  348. package/scripts/fix-tool-exec-visible.mjs +0 -42
  349. package/scripts/patch-agent-brief.mjs +0 -48
  350. package/scripts/patch-app.mjs +0 -21
  351. package/scripts/patch-app2.mjs +0 -18
  352. package/scripts/patch-dist-brief.mjs +0 -96
  353. package/scripts/patch-tool-exec.mjs +0 -70
  354. package/src/examples/schedules/SCHEDULE.example.md +0 -32
  355. package/src/examples/webhooks/WEBHOOK.example.md +0 -40
  356. package/src/runtime/agent/orchestrator/session/manager.reactive-persist.test.mjs +0 -107
  357. package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.test.mjs +0 -143
  358. package/src/runtime/agent/orchestrator/tools/builtin/diagnostics-tool.mjs +0 -285
  359. package/src/runtime/agent/orchestrator/tools/builtin/external-tool-adapters.test.mjs +0 -162
  360. package/src/runtime/agent/orchestrator/tools/builtin/open-config-tool.mjs +0 -26
  361. package/src/runtime/channels/lib/holidays.mjs +0 -138
  362. package/src/runtime/shared/channel-notification-routing.test.mjs +0 -45
  363. package/src/runtime/shared/task-notification-envelope.test.mjs +0 -107
  364. package/src/runtime/shared/tool-execution-contract.test.mjs +0 -183
  365. package/src/standalone/agent-task-status.test.mjs +0 -76
  366. package/src/tui/components/tool-output-format.test.mjs +0 -399
  367. package/src/tui/display-width.test.mjs +0 -35
  368. package/src/tui/engine-runtime-notification.test.mjs +0 -115
  369. package/src/tui/engine-tool-result-text.test.mjs +0 -75
  370. package/src/tui/input-editing.selection.test.mjs +0 -75
  371. package/src/tui/markdown/format-token.test.mjs +0 -354
  372. package/src/tui/markdown/render-ansi.test.mjs +0 -108
  373. package/src/tui/markdown/stream-fence.test.mjs +0 -26
  374. package/src/tui/markdown/streaming-markdown.test.mjs +0 -70
  375. package/src/tui/paste-fix.test.mjs +0 -119
  376. package/src/tui/prompt-history-store.test.mjs +0 -52
  377. package/src/tui/statusline-ansi-bridge.test.mjs +0 -159
  378. package/src/tui/transcript-tool-failures.test.mjs +0 -111
  379. package/src/ui/markdown.test.mjs +0 -70
  380. package/src/ui/statusline-context-label.test.mjs +0 -15
  381. package/src/vendor/statusline/bin/statusline-lib.mjs +0 -186
  382. package/src/vendor/statusline/bin/statusline-route.test.mjs +0 -80
@@ -1,4 +1,4 @@
1
- import { GoogleGenerativeAI, SchemaType } from '@google/generative-ai';
1
+ import { GoogleGenerativeAI } from '@google/generative-ai';
2
2
  import { loadConfig } from '../config.mjs';
3
3
  import { makeModelCache } from './model-cache.mjs';
4
4
  import { withRetry } from './retry-classifier.mjs';
@@ -14,9 +14,43 @@ import {
14
14
  createPassthroughSignal,
15
15
  } from '../stall-policy.mjs';
16
16
  import { getLlmDispatcher, preconnect } from '../../../shared/llm/http-agent.mjs';
17
- import { traceHash, stableTraceStringify, summarizeTraceTools, traceTextShape } from './trace-utils.mjs';
18
- import { normalizeContentForGeminiParts, splitToolContentForGemini } from './media-normalization.mjs';
19
- import { scanLeakedToolCalls } from './anthropic-leaked-toolcall.mjs';
17
+ import {
18
+ GEMINI_FIRST_BYTE_TIMEOUT_MS,
19
+ geminiTimeoutError,
20
+ createGeminiTextLeakGuard,
21
+ consumeGeminiRestStreamResponse,
22
+ consumeGeminiSdkStream,
23
+ } from './gemini-stream.mjs';
24
+ import {
25
+ toGeminiTools,
26
+ toGeminiNativeTools,
27
+ toGeminiToolConfig,
28
+ toGeminiContents,
29
+ parseToolCalls,
30
+ emitGeminiToolCalls,
31
+ collectGeminiGroundingSources,
32
+ } from './gemini-schema.mjs';
33
+ import {
34
+ _estimateGeminiCacheTokens,
35
+ _geminiCacheMinTokens,
36
+ _geminiCachePrefixCount,
37
+ _geminiCachePrefixContents,
38
+ _geminiCachePrefixHash,
39
+ _geminiGlobalCacheKey,
40
+ _getGeminiGlobalCache,
41
+ _setGeminiGlobalCache,
42
+ _geminiGlobalCacheNameIsLive,
43
+ _attachGeminiCacheState,
44
+ _resolveGeminiCacheUsage,
45
+ writeGeminiCacheTrace,
46
+ geminiGlobalCacheCreates,
47
+ GEMINI_GLOBAL_CACHE_DELETE_GRACE_MS,
48
+ } from './gemini-cache.mjs';
49
+
50
+ // Legacy import path: tests + gemini-stream import these from gemini.mjs.
51
+ // Re-export the extracted symbols so existing importers resolve unchanged.
52
+ export { createGeminiTextLeakGuard };
53
+ export { parseToolCalls, emitGeminiToolCalls, collectGeminiGroundingSources };
20
54
 
21
55
  const MODELS = [
22
56
  { id: 'gemini-3-flash-preview', name: 'Gemini 3 Flash Preview', provider: 'gemini', contextWindow: 1048576 },
@@ -94,1014 +128,9 @@ export async function ensureLatestGeminiModel(provider) {
94
128
  throw new Error('[gemini] model catalog unavailable after warmup — cannot resolve default model');
95
129
  }
96
130
 
97
- const GEMINI_FIRST_BYTE_TIMEOUT_MS = resolveTimeoutMs(
98
- 'MIXDOG_GEMINI_FIRST_BYTE_TIMEOUT_MS',
99
- PROVIDER_FIRST_BYTE_TIMEOUT_MS,
100
- { minMs: 30_000, maxMs: PROVIDER_MAX_BEFORE_WARN_MS },
101
- );
102
-
103
- function summarizeTracePart(part) {
104
- if (!part || typeof part !== 'object') return { type: typeof part };
105
- if ('text' in part) {
106
- return { type: 'text', ...traceTextShape(part.text) };
107
- }
108
- if (part.functionCall) {
109
- return {
110
- type: 'functionCall',
111
- name: part.functionCall.name || null,
112
- argsHash: traceHash(stableTraceStringify(part.functionCall.args || {})),
113
- };
114
- }
115
- if (part.functionResponse) {
116
- const response = part.functionResponse.response || {};
117
- const responseShape = stableTraceStringify(response);
118
- return {
119
- type: 'functionResponse',
120
- name: part.functionResponse.name || null,
121
- responseChars: responseShape.length,
122
- responseHash: traceHash(responseShape),
123
- };
124
- }
125
- return { type: Object.keys(part).sort().join('|') || 'unknown' };
126
- }
127
-
128
- function summarizeTraceContents(contents) {
129
- const summaries = (contents || []).map((content, index) => ({
130
- index,
131
- role: content?.role || null,
132
- parts: Array.isArray(content?.parts) ? content.parts.map(summarizeTracePart) : [],
133
- }));
134
- if (summaries.length <= 12) return summaries;
135
- return [
136
- ...summaries.slice(0, 8),
137
- { omittedTurns: summaries.length - 12 },
138
- ...summaries.slice(-4),
139
- ];
140
- }
141
-
142
- // Gemini cachedContents API rejects prefixes below the model-specific minimum
143
- // (Gemini 2.0/2.5 = 2048 tokens, Gemini 3.x = 4096 tokens) with HTTP 400
144
- // "Cached content is too small". Estimating chars/4 ≈ tokens lets us skip the
145
- // roundtrip when the prefix cannot satisfy the invariant. Estimate undercount
146
- // is harmless (we attempt and get the same 400 we would have skipped); overcount
147
- // is harmless (we skip, fall back to implicit cache).
148
- function _estimateGeminiCacheTokens(systemInstruction, geminiTools, contents) {
149
- let chars = 0;
150
- if (typeof systemInstruction === 'string') chars += systemInstruction.length;
151
- if (Array.isArray(geminiTools) && geminiTools.length) {
152
- try { chars += JSON.stringify(geminiTools).length; } catch {}
153
- }
154
- if (Array.isArray(contents) && contents.length > 1) {
155
- for (let i = 0; i < contents.length - 1; i++) {
156
- try { chars += JSON.stringify(contents[i]?.parts ?? '').length; } catch {}
157
- }
158
- }
159
- return Math.ceil(chars / 4);
160
- }
161
-
162
- function _geminiCacheMinTokens(model) {
163
- return /^gemini-3/i.test(String(model || '')) ? 4096 : 2048;
164
- }
165
-
166
- function _geminiCachePrefixCount(contents) {
167
- return Array.isArray(contents) && contents.length > 1 ? contents.length - 1 : 0;
168
- }
169
-
170
- function _geminiCachePrefixContents(contents, prefixCount) {
171
- if (!Array.isArray(contents) || prefixCount <= 0) return [];
172
- return contents.slice(0, prefixCount).map(c => {
173
- const r = c?.role;
174
- const safeRole = (r === 'model' || r === 'user') ? r : 'user';
175
- return {
176
- role: safeRole,
177
- parts: Array.isArray(c?.parts) ? c.parts : [],
178
- };
179
- });
180
- }
181
-
182
- function _geminiCachePrefixHash({ model, systemInstruction, geminiTools, contents, prefixCount }) {
183
- return traceHash(stableTraceStringify({
184
- model: model || null,
185
- systemInstruction: systemInstruction || '',
186
- tools: geminiTools || [],
187
- contents: _geminiCachePrefixContents(contents, prefixCount),
188
- }));
189
- }
190
-
191
- const GEMINI_GLOBAL_CACHE_MIN_LIVE_MS = 6 * 60 * 1000;
192
- const GEMINI_GLOBAL_CACHE_MAX_ENTRIES = 128;
193
- // Grace window before deleting a superseded cachedContents name (see the
194
- // cross-session race note at the L1341-1372 call site). Long enough that a
195
- // concurrent session still mid-flight on the old name has time to finish.
196
- const GEMINI_GLOBAL_CACHE_DELETE_GRACE_MS = 2 * 60 * 1000;
197
- const geminiGlobalCaches = new Map();
198
- const geminiGlobalCacheCreates = new Map();
199
-
200
- function _geminiGlobalCacheKey({ apiKey, model, cachePrefixHash, cachePrefixContentCount }) {
201
- return traceHash(stableTraceStringify({
202
- apiKeyHash: traceHash(apiKey || ''),
203
- model: model || null,
204
- cachePrefixHash,
205
- cachePrefixContentCount,
206
- }));
207
- }
208
-
209
- function _pruneGeminiGlobalCaches(now = Date.now()) {
210
- for (const [key, entry] of geminiGlobalCaches) {
211
- if (!entry?.cacheName || Number(entry.cacheExpiresAt || 0) <= now) {
212
- geminiGlobalCaches.delete(key);
213
- }
214
- }
215
- while (geminiGlobalCaches.size > GEMINI_GLOBAL_CACHE_MAX_ENTRIES) {
216
- const oldest = geminiGlobalCaches.keys().next().value;
217
- if (oldest === undefined) break;
218
- geminiGlobalCaches.delete(oldest);
219
- }
220
- }
221
-
222
- function _getGeminiGlobalCache(key, now = Date.now()) {
223
- _pruneGeminiGlobalCaches(now);
224
- const entry = geminiGlobalCaches.get(key);
225
- if (!entry?.cacheName) return null;
226
- if ((entry.cacheExpiresAt || 0) - now <= GEMINI_GLOBAL_CACHE_MIN_LIVE_MS) {
227
- geminiGlobalCaches.delete(key);
228
- return null;
229
- }
230
- entry.lastUsedAt = now;
231
- return entry;
232
- }
233
-
234
- function _setGeminiGlobalCache(key, entry) {
235
- if (!key || !entry?.cacheName) return;
236
- geminiGlobalCaches.set(key, {
237
- ...entry,
238
- lastUsedAt: Date.now(),
239
- });
240
- _pruneGeminiGlobalCaches();
241
- }
242
-
243
- function _geminiGlobalCacheNameIsLive(cacheName, now = Date.now()) {
244
- if (!cacheName) return false;
245
- for (const entry of geminiGlobalCaches.values()) {
246
- if (entry?.cacheName === cacheName && Number(entry.cacheExpiresAt || 0) > now) return true;
247
- }
248
- return false;
249
- }
250
-
251
- function _attachGeminiCacheState(opts, entry, currentIter) {
252
- if (!entry?.cacheName) return;
253
- opts.providerState = {
254
- ...(opts.providerState || {}),
255
- gemini: {
256
- cacheName: entry.cacheName,
257
- cacheCreatedAt: entry.cacheCreatedAt,
258
- cacheCreatedAtIter: currentIter,
259
- cacheExpiresAt: entry.cacheExpiresAt,
260
- cacheModel: entry.cacheModel,
261
- cacheTokenSize: entry.cacheTokenSize,
262
- cachePrefixContentCount: entry.cachePrefixContentCount,
263
- cachePrefixHash: entry.cachePrefixHash,
264
- },
265
- };
266
- }
267
-
268
- function _resolveGeminiCacheUsage({ usageMetadata, cachedContent, providerState }) {
269
- const inputTokens = Number(usageMetadata?.promptTokenCount || usageMetadata?.totalTokenCount || 0) || 0;
270
- const reportedCachedTokens = Number(usageMetadata?.cachedContentTokenCount || 0) || 0;
271
- const cachedFallbackTokens = cachedContent
272
- ? Number(providerState?.gemini?.cacheTokenSize || 0) || 0
273
- : 0;
274
- const rawCachedTokens = reportedCachedTokens > 0 ? reportedCachedTokens : cachedFallbackTokens;
275
- const cachedTokens = inputTokens > 0 ? Math.min(rawCachedTokens, inputTokens) : rawCachedTokens;
276
- const cacheTokenSource = reportedCachedTokens > 0
277
- ? 'usage_metadata'
278
- : (cachedFallbackTokens > 0 ? 'cache_create_fallback' : 'none');
279
- return {
280
- inputTokens,
281
- reportedCachedTokens,
282
- cachedFallbackTokens,
283
- cachedTokens,
284
- cacheTokenSource,
285
- };
286
- }
287
-
288
- function writeGeminiCacheTrace({ opts, model, systemInstruction, tools, contents, usageMetadata, cachedContent }) {
289
- if (process.env.MIXDOG_GEMINI_CACHE_TRACE !== '1') return;
290
- try {
291
- const session = opts?.session || {};
292
- const {
293
- inputTokens,
294
- reportedCachedTokens,
295
- cachedFallbackTokens,
296
- cachedTokens,
297
- cacheTokenSource,
298
- } = _resolveGeminiCacheUsage({
299
- usageMetadata,
300
- cachedContent,
301
- providerState: opts?.providerState,
302
- });
303
- const toolShape = summarizeTraceTools(tools);
304
- const trace = {
305
- event: 'generate',
306
- provider: 'gemini',
307
- model,
308
- owner: session.owner || null,
309
- agent: session.agent || null,
310
- permission: session.permission || null,
311
- toolPermission: session.toolPermission || null,
312
- profileId: session.profileId || null,
313
- sourceType: session.sourceType || null,
314
- sourceName: session.sourceName || null,
315
- sessionIdHash: opts?.sessionId ? traceHash(opts.sessionId) : null,
316
- providerCacheKeyHash: opts?.providerCacheKey ? traceHash(opts.providerCacheKey) : null,
317
- promptCacheKeyHash: opts?.promptCacheKey ? traceHash(opts.promptCacheKey) : null,
318
- systemChars: systemInstruction ? systemInstruction.length : 0,
319
- systemHash: systemInstruction ? traceHash(systemInstruction) : null,
320
- toolCount: Array.isArray(tools) ? tools.length : 0,
321
- toolSchemaHash: traceHash(stableTraceStringify(toolShape)),
322
- contentTurnCount: Array.isArray(contents) ? contents.length : 0,
323
- contents: summarizeTraceContents(contents),
324
- inputTokens,
325
- cachedTokens,
326
- reportedCachedTokens,
327
- cachedFallbackTokens,
328
- cacheTokenSource,
329
- cacheAttached: !!cachedContent,
330
- cachePrefixContentCount: opts?.providerState?.gemini?.cachePrefixContentCount ?? null,
331
- cacheHitRate: inputTokens > 0 ? Number((cachedTokens / inputTokens).toFixed(6)) : null,
332
- };
333
- process.stderr.write(`[gemini-cache-trace] ${JSON.stringify(trace)}\n`);
334
- } catch (err) {
335
- process.stderr.write(`[gemini-cache-trace] failed: ${err?.message || err}\n`);
336
- }
337
- }
338
-
339
- function geminiTimeoutError(label, timeoutMs) {
340
- const err = providerTimeoutError(label, timeoutMs);
341
- err.name = 'GeminiTimeoutError';
342
- err.code = 'EGEMINITIMEOUT';
343
- return err;
344
- }
345
-
346
- function geminiTruncatedStreamError(message) {
347
- return Object.assign(
348
- new Error(message),
349
- { name: 'TruncatedStreamError', code: 'TRUNCATED_STREAM', truncatedStream: true },
350
- );
351
- }
352
-
353
- /**
354
- * Aggregate streamed GenerateContentResponse chunks into one response object
355
- * (same shape as a non-streaming generateContent JSON body).
356
- * Mirrors @google/generative-ai aggregateResponses().
357
- */
358
- function aggregateGeminiStreamChunks(responses) {
359
- const lastResponse = responses[responses.length - 1];
360
- const aggregatedResponse = {
361
- promptFeedback: lastResponse?.promptFeedback,
362
- };
363
- for (const response of responses) {
364
- if (response?.candidates) {
365
- let candidateIndex = 0;
366
- for (const candidate of response.candidates) {
367
- if (!aggregatedResponse.candidates) aggregatedResponse.candidates = [];
368
- if (!aggregatedResponse.candidates[candidateIndex]) {
369
- aggregatedResponse.candidates[candidateIndex] = { index: candidateIndex };
370
- }
371
- const aggCand = aggregatedResponse.candidates[candidateIndex];
372
- aggCand.citationMetadata = candidate.citationMetadata;
373
- aggCand.groundingMetadata = candidate.groundingMetadata;
374
- aggCand.finishReason = candidate.finishReason;
375
- aggCand.finishMessage = candidate.finishMessage;
376
- aggCand.safetyRatings = candidate.safetyRatings;
377
- if (candidate.content?.parts) {
378
- if (!aggCand.content) {
379
- aggCand.content = {
380
- role: candidate.content.role || 'user',
381
- parts: [],
382
- };
383
- }
384
- for (const part of candidate.content.parts) {
385
- const newPart = {};
386
- if (part.text) newPart.text = part.text;
387
- if (part.functionCall) newPart.functionCall = part.functionCall;
388
- if (part.thoughtSignature) newPart.thoughtSignature = part.thoughtSignature;
389
- if (part.thought_signature) newPart.thought_signature = part.thought_signature;
390
- if (part.executableCode) newPart.executableCode = part.executableCode;
391
- if (part.codeExecutionResult) newPart.codeExecutionResult = part.codeExecutionResult;
392
- if (Object.keys(newPart).length === 0) newPart.text = '';
393
- aggCand.content.parts.push(newPart);
394
- }
395
- }
396
- candidateIndex++;
397
- }
398
- }
399
- if (response?.usageMetadata) aggregatedResponse.usageMetadata = response.usageMetadata;
400
- }
401
- return aggregatedResponse;
402
- }
403
-
404
- function assertGeminiStreamCompleted({ sawStreamChunk, finishReason, label }) {
405
- if (!sawStreamChunk) {
406
- throw geminiTruncatedStreamError(`${label} truncated: empty stream`);
407
- }
408
- if (!finishReason) {
409
- throw geminiTruncatedStreamError(`${label} truncated: no finishReason`);
410
- }
411
- }
412
-
413
- // Concatenate the text parts of a single streamed Gemini chunk. Used to feed
414
- // the gateway live-text relay (onTextDelta) with the incremental text payload
415
- // as each SSE/SDK chunk arrives. Returns '' for tool-call / thought-only /
416
- // malformed chunks so the caller can skip empty emits.
417
- function geminiChunkText(chunk) {
418
- const parts = chunk?.candidates?.[0]?.content?.parts;
419
- if (!Array.isArray(parts)) return '';
420
- let text = '';
421
- for (const p of parts) {
422
- if (p && typeof p.text === 'string') text += p.text;
423
- }
424
- return text;
425
- }
426
-
427
- function relayGeminiStreamText(t, { onTextDelta, textLeakGuard }) {
428
- if (!t) return;
429
- if (textLeakGuard) textLeakGuard.feedText(t);
430
- else if (onTextDelta) { try { onTextDelta(t); } catch {} }
431
- }
432
-
433
- /**
434
- * Rolling scanner for tool calls leaked as plain XML/antml tags inside Gemini
435
- * `part.text` streams. Mirrors the Anthropic OAuth guard: suppress tags from
436
- * visible text, synthesize known-tool calls, dispatch via onToolCall.
437
- */
438
- export function createGeminiTextLeakGuard({ knownToolNames, onTextDelta, onToolCall, onStreamDelta }) {
439
- const _knownTools = knownToolNames instanceof Set
440
- ? knownToolNames
441
- : new Set(Array.isArray(knownToolNames) ? knownToolNames : []);
442
- const _enabled = _knownTools.size > 0;
443
- const _isKnownTool = (name) => _knownTools.has(name);
444
- let leakBuffer = '';
445
- const leakedCalls = [];
446
- const dispatchedFingerprints = new Set();
447
-
448
- const toolCallFingerprint = (name, args) => {
449
- let a = args;
450
- if (a === null || typeof a !== 'object' || Array.isArray(a)) a = {};
451
- return traceHash(stableTraceStringify({ name: name || '', args: a }));
452
- };
453
-
454
- const dispatchLeakedCall = (recovered) => {
455
- let args = recovered?.arguments;
456
- if (args === null || typeof args !== 'object' || Array.isArray(args)) args = {};
457
- const fp = toolCallFingerprint(recovered.name, args);
458
- if (dispatchedFingerprints.has(fp)) return;
459
- dispatchedFingerprints.add(fp);
460
- const idHash = traceHash(stableTraceStringify({
461
- name: recovered.name,
462
- args,
463
- leak: true,
464
- })).slice(0, 16);
465
- const call = {
466
- id: `gemini_leaked_${idHash}`,
467
- name: recovered.name,
468
- arguments: args,
469
- };
470
- leakedCalls.push(call);
471
- try { onToolCall?.(call); } catch {}
472
- try { onStreamDelta?.(); } catch {}
473
- };
474
-
475
- const pumpLeakBuffer = (final) => {
476
- if (!_enabled) return;
477
- if (!leakBuffer && !final) return;
478
- const { emit, calls, rest } = scanLeakedToolCalls(leakBuffer, { isKnownTool: _isKnownTool, final });
479
- leakBuffer = rest;
480
- if (emit && onTextDelta) {
481
- try { onTextDelta(emit); } catch {}
482
- }
483
- for (const c of calls) dispatchLeakedCall(c);
484
- };
485
-
486
- return {
487
- get enabled() { return _enabled; },
488
- feedText(text) {
489
- if (!text) return;
490
- if (!_enabled) {
491
- try { onTextDelta?.(text); } catch {}
492
- return;
493
- }
494
- leakBuffer += text;
495
- pumpLeakBuffer(false);
496
- },
497
- finalize() {
498
- pumpLeakBuffer(true);
499
- },
500
- scrubAssistantText(raw) {
501
- if (!raw) return '';
502
- if (!_enabled) return raw;
503
- const { emit, calls, rest } = scanLeakedToolCalls(raw, { isKnownTool: _isKnownTool, final: true });
504
- for (const c of calls) dispatchLeakedCall(c);
505
- return emit + rest;
506
- },
507
- filterNativeToolCalls(nativeCalls) {
508
- if (!_enabled || !nativeCalls?.length) return nativeCalls;
509
- const kept = [];
510
- for (const call of nativeCalls) {
511
- const fp = toolCallFingerprint(call?.name, call?.arguments);
512
- if (dispatchedFingerprints.has(fp)) continue;
513
- dispatchedFingerprints.add(fp);
514
- kept.push(call);
515
- }
516
- return kept.length ? kept : undefined;
517
- },
518
- getLeakedToolCalls() {
519
- return leakedCalls.length ? [...leakedCalls] : [];
520
- },
521
- };
522
- }
523
-
524
- async function consumeGeminiRestStreamResponse(response, { signal, onStreamDelta, onTextDelta, textLeakGuard, label }) {
525
- if (!response?.body) throw new Error(`${label}: missing response body`);
526
- const reader = response.body.getReader();
527
- const decoder = new TextDecoder();
528
- let buffer = '';
529
- const allChunks = [];
530
- let sawStreamChunk = false;
531
- let idleTimedOut = false;
532
- let idleTimer = null;
533
- let idleReject = null;
534
-
535
- let firstByteTimer = setTimeout(() => {
536
- try { reader.cancel('first byte timeout'); } catch {}
537
- if (idleReject) {
538
- const e = geminiTimeoutError(`${label} first byte`, GEMINI_FIRST_BYTE_TIMEOUT_MS);
539
- const r = idleReject; idleReject = null; r(e);
540
- }
541
- }, GEMINI_FIRST_BYTE_TIMEOUT_MS);
542
- if (firstByteTimer.unref) firstByteTimer.unref();
543
-
544
- const clearFirstByteTimer = () => {
545
- if (firstByteTimer) {
546
- clearTimeout(firstByteTimer);
547
- firstByteTimer = null;
548
- }
549
- };
550
-
551
- const resetIdleTimer = () => {
552
- if (!PROVIDER_SSE_IDLE_WATCHDOG_ENABLED) return;
553
- if (idleTimer) clearTimeout(idleTimer);
554
- idleTimer = setTimeout(() => {
555
- idleTimedOut = true;
556
- try { reader.cancel('SSE idle timeout'); } catch {}
557
- if (idleReject) {
558
- const e = geminiTimeoutError(`${label} SSE idle`, PROVIDER_SSE_IDLE_TIMEOUT_MS);
559
- const r = idleReject; idleReject = null; r(e);
560
- }
561
- }, PROVIDER_SSE_IDLE_TIMEOUT_MS);
562
- if (idleTimer.unref) idleTimer.unref();
563
- };
564
-
565
- const onAbort = () => {
566
- try {
567
- const c = reader.cancel('aborted');
568
- if (c && typeof c.catch === 'function') c.catch(() => {});
569
- } catch {}
570
- };
571
-
572
- if (signal) {
573
- if (signal.aborted) {
574
- const reason = signal.reason;
575
- throw reason instanceof Error ? reason : new Error(`${label} aborted`);
576
- }
577
- signal.addEventListener('abort', onAbort, { once: true });
578
- }
579
-
580
- try {
581
- resetIdleTimer();
582
- while (true) {
583
- let chunk;
584
- try {
585
- chunk = await new Promise((resolve, reject) => {
586
- idleReject = reject;
587
- reader.read().then(resolve, reject);
588
- });
589
- } catch (err) {
590
- if (idleTimedOut) {
591
- throw geminiTimeoutError(`${label} SSE idle`, PROVIDER_SSE_IDLE_TIMEOUT_MS);
592
- }
593
- if (signal?.aborted) {
594
- const reason = signal.reason;
595
- throw reason instanceof Error ? reason : new Error(`${label} aborted`);
596
- }
597
- throw err;
598
- } finally {
599
- idleReject = null;
600
- }
601
- const { done, value } = chunk;
602
- if (done) break;
603
- resetIdleTimer();
604
- buffer += decoder.decode(value, { stream: true });
605
- let lineEnd;
606
- while ((lineEnd = buffer.indexOf('\n')) >= 0) {
607
- let line = buffer.slice(0, lineEnd);
608
- buffer = buffer.slice(lineEnd + 1);
609
- line = line.replace(/\r$/, '');
610
- if (!line.startsWith('data: ')) continue;
611
- const data = line.slice(6).trim();
612
- if (!data || data === '[DONE]') continue;
613
- let parsed;
614
- try { parsed = JSON.parse(data); } catch { continue; }
615
- if (!sawStreamChunk) {
616
- sawStreamChunk = true;
617
- clearFirstByteTimer();
618
- }
619
- allChunks.push(parsed);
620
- try { onStreamDelta?.(); } catch {}
621
- if (onTextDelta || textLeakGuard) {
622
- const t = geminiChunkText(parsed);
623
- relayGeminiStreamText(t, { onTextDelta, textLeakGuard });
624
- }
625
- }
626
- }
627
- if (buffer.trim()) {
628
- const line = buffer.trim().replace(/\r$/, '');
629
- if (line.startsWith('data: ')) {
630
- const data = line.slice(6).trim();
631
- if (data && data !== '[DONE]') {
632
- try {
633
- const parsed = JSON.parse(data);
634
- if (!sawStreamChunk) {
635
- sawStreamChunk = true;
636
- clearFirstByteTimer();
637
- }
638
- allChunks.push(parsed);
639
- try { onStreamDelta?.(); } catch {}
640
- if (onTextDelta || textLeakGuard) {
641
- const t = geminiChunkText(parsed);
642
- relayGeminiStreamText(t, { onTextDelta, textLeakGuard });
643
- }
644
- } catch { /* skip malformed tail */ }
645
- }
646
- }
647
- }
648
- } finally {
649
- clearFirstByteTimer();
650
- if (idleTimer) clearTimeout(idleTimer);
651
- if (signal) signal.removeEventListener('abort', onAbort);
652
- try { reader.releaseLock(); } catch {}
653
- try { textLeakGuard?.finalize(); } catch {}
654
- }
655
-
656
- const aggregated = aggregateGeminiStreamChunks(allChunks);
657
- const finishReason = aggregated.candidates?.[0]?.finishReason || null;
658
- assertGeminiStreamCompleted({ sawStreamChunk, finishReason, label });
659
- return aggregated;
660
- }
661
-
662
- async function consumeGeminiSdkStream(streamResult, { signal, onStreamDelta, onTextDelta, textLeakGuard, label }) {
663
- let sawStreamChunk = false;
664
- let idleTimedOut = false;
665
- let idleTimer = null;
666
- let firstByteReject = null;
667
- let firstByteTimer = null;
668
- let inFlightReject = null;
669
-
670
- const armFirstByteTimer = () => {
671
- if (firstByteTimer) clearTimeout(firstByteTimer);
672
- firstByteTimer = setTimeout(() => {
673
- if (firstByteReject) {
674
- const e = geminiTimeoutError(`${label} first byte`, GEMINI_FIRST_BYTE_TIMEOUT_MS);
675
- const r = firstByteReject; firstByteReject = null; r(e);
676
- }
677
- }, GEMINI_FIRST_BYTE_TIMEOUT_MS);
678
- if (firstByteTimer.unref) firstByteTimer.unref();
679
- };
680
-
681
- const clearFirstByteTimer = () => {
682
- if (firstByteTimer) {
683
- clearTimeout(firstByteTimer);
684
- firstByteTimer = null;
685
- }
686
- firstByteReject = null;
687
- };
688
-
689
- const resetIdleTimer = () => {
690
- if (!PROVIDER_SSE_IDLE_WATCHDOG_ENABLED) return;
691
- if (idleTimer) clearTimeout(idleTimer);
692
- idleTimer = setTimeout(() => {
693
- idleTimedOut = true;
694
- if (inFlightReject) {
695
- const e = geminiTimeoutError(`${label} SSE idle`, PROVIDER_SSE_IDLE_TIMEOUT_MS);
696
- const r = inFlightReject; inFlightReject = null; r(e);
697
- }
698
- }, PROVIDER_SSE_IDLE_TIMEOUT_MS);
699
- if (idleTimer.unref) idleTimer.unref();
700
- };
701
-
702
- if (signal?.aborted) {
703
- const reason = signal.reason;
704
- throw reason instanceof Error ? reason : new Error(`${label} aborted`);
705
- }
706
-
707
- const iterator = streamResult.stream[Symbol.asyncIterator]();
708
-
709
- // Wire the abort signal to actually CANCEL iteration (mirror of the REST
710
- // consumer's onAbort -> reader.cancel). Without this, a parent / client /
711
- // gateway abort that fires AFTER the first byte — with the SSE idle
712
- // watchdog off by default — would leave iterator.next() hanging: the loop
713
- // below only reads `signal` for error translation, never to interrupt the
714
- // pending read. Rejecting the in-flight promise and returning the iterator
715
- // releases it promptly even if the SDK is slow to propagate the underlying
716
- // request abort.
717
- let onSignalAbort = null;
718
- if (signal) {
719
- onSignalAbort = () => {
720
- const reason = signal.reason;
721
- const err = reason instanceof Error ? reason : new Error(`${label} aborted`);
722
- if (inFlightReject) {
723
- const r = inFlightReject;
724
- inFlightReject = null;
725
- firstByteReject = null;
726
- r(err);
727
- }
728
- try {
729
- const ret = iterator.return?.();
730
- if (ret && typeof ret.catch === 'function') ret.catch(() => {});
731
- } catch {}
732
- };
733
- signal.addEventListener('abort', onSignalAbort, { once: true });
734
- }
735
-
736
- try {
737
- armFirstByteTimer();
738
- resetIdleTimer();
739
- while (true) {
740
- if (idleTimedOut) {
741
- throw geminiTimeoutError(`${label} SSE idle`, PROVIDER_SSE_IDLE_TIMEOUT_MS);
742
- }
743
- let step;
744
- try {
745
- step = await new Promise((resolve, reject) => {
746
- inFlightReject = reject;
747
- if (!sawStreamChunk) firstByteReject = reject;
748
- iterator.next().then(
749
- (value) => {
750
- inFlightReject = null;
751
- firstByteReject = null;
752
- resolve(value);
753
- },
754
- (err) => {
755
- inFlightReject = null;
756
- firstByteReject = null;
757
- reject(err);
758
- },
759
- );
760
- });
761
- } catch (err) {
762
- if (idleTimedOut) {
763
- throw geminiTimeoutError(`${label} SSE idle`, PROVIDER_SSE_IDLE_TIMEOUT_MS);
764
- }
765
- if (signal?.aborted) {
766
- const reason = signal.reason;
767
- throw reason instanceof Error ? reason : new Error(`${label} aborted`);
768
- }
769
- throw err;
770
- }
771
- if (step.done) break;
772
- if (!sawStreamChunk) {
773
- sawStreamChunk = true;
774
- clearFirstByteTimer();
775
- }
776
- resetIdleTimer();
777
- try { onStreamDelta?.(); } catch {}
778
- if (onTextDelta || textLeakGuard) {
779
- const t = geminiChunkText(step.value);
780
- relayGeminiStreamText(t, { onTextDelta, textLeakGuard });
781
- }
782
- }
783
- if (idleTimedOut) {
784
- throw geminiTimeoutError(`${label} SSE idle`, PROVIDER_SSE_IDLE_TIMEOUT_MS);
785
- }
786
- } catch (err) {
787
- clearFirstByteTimer();
788
- if (signal?.aborted) {
789
- const reason = signal.reason;
790
- throw reason instanceof Error ? reason : new Error(`${label} aborted`);
791
- }
792
- throw err;
793
- } finally {
794
- clearFirstByteTimer();
795
- if (idleTimer) clearTimeout(idleTimer);
796
- if (signal && onSignalAbort) {
797
- try { signal.removeEventListener('abort', onSignalAbort); } catch {}
798
- }
799
- try { textLeakGuard?.finalize(); } catch {}
800
- }
801
-
802
- let response;
803
- try {
804
- response = await streamResult.response;
805
- } catch (err) {
806
- if (signal?.aborted) {
807
- const reason = signal.reason;
808
- throw reason instanceof Error ? reason : new Error(`${label} aborted`);
809
- }
810
- throw err;
811
- }
812
- const raw = response?.candidates ? response : (response?.response || response);
813
- const finishReason = raw?.candidates?.[0]?.finishReason || null;
814
- assertGeminiStreamCompleted({ sawStreamChunk, finishReason, label });
815
- return raw;
816
- }
817
-
818
- /**
819
- * Convert JSON Schema type string to Gemini SchemaType.
820
- * Gemini SDK uses its own enum instead of plain strings.
821
- */
822
- function toSchemaType(t) {
823
- const map = {
824
- string: SchemaType.STRING,
825
- number: SchemaType.NUMBER,
826
- integer: SchemaType.INTEGER,
827
- boolean: SchemaType.BOOLEAN,
828
- array: SchemaType.ARRAY,
829
- object: SchemaType.OBJECT,
830
- };
831
- return map[t] ?? SchemaType.STRING;
832
- }
833
-
834
- /**
835
- * Recursively convert a JSON Schema object to Gemini's FunctionDeclarationSchema.
836
- * Gemini requires `type` to be a SchemaType enum, not a plain string, and
837
- * rejects several JSON Schema fields the API does not understand
838
- * (additionalProperties, $schema, $ref, const, examples, definitions,
839
- * patternProperties). We strip those at every level.
840
- */
841
- const GEMINI_SCHEMA_STRIP = new Set([
842
- 'additionalProperties',
843
- '$schema',
844
- '$ref',
845
- 'const',
846
- 'examples',
847
- 'definitions',
848
- 'patternProperties',
849
- ]);
850
- function convertSchema(schema) {
851
- if (!schema || typeof schema !== 'object') return schema;
852
- const result = {};
853
- for (const [k, v] of Object.entries(schema)) {
854
- if (GEMINI_SCHEMA_STRIP.has(k)) continue;
855
- result[k] = v;
856
- }
857
- // Gemini's Schema validator requires every `enum` entry to be a string,
858
- // even when the parent `type` is integer/number/boolean. Drop the enum in
859
- // that case rather than emit an invalid typed enum — `type` plus the
860
- // description still guides the model, and the tool handler revalidates.
861
- const rawType = typeof result.type === 'string' ? result.type : undefined;
862
- if (Array.isArray(result.enum) && (rawType === 'integer' || rawType === 'number' || rawType === 'boolean')) {
863
- if (result.enum.some((item) => typeof item !== 'string')) {
864
- delete result.enum;
865
- }
866
- }
867
- // Gemini rejects array schemas that omit `items`; fill a permissive
868
- // default so the declaration validates.
869
- if (rawType === 'array' && (!result.items || typeof result.items !== 'object')) {
870
- result.items = { type: 'string' };
871
- }
872
- if (typeof result.type === 'string') {
873
- result.type = toSchemaType(result.type);
874
- }
875
- if (result.properties && typeof result.properties === 'object') {
876
- const props = {};
877
- for (const [key, val] of Object.entries(result.properties)) {
878
- props[key] = convertSchema(val);
879
- }
880
- result.properties = props;
881
- }
882
- if (result.items && typeof result.items === 'object') {
883
- result.items = convertSchema(result.items);
884
- }
885
- // Recurse into JSON Schema combinator keys so disallowed fields
886
- // (additionalProperties, $schema, etc.) get stripped at every nesting
887
- // level. Without this, schemas using anyOf/oneOf/allOf/not pass the
888
- // shallow strip but fail Gemini validation at depth.
889
- //
890
- // Two Gemini-specific normalizations are also applied per combinator
891
- // subschema:
892
- // 1. Inject `type: OBJECT` when a subschema uses object-only keys
893
- // (`required` / `properties`) without an explicit type — Gemini
894
- // rejects `required` outside of OBJECT type.
895
- // 2. Materialize a local `properties` map from the parent's properties
896
- // when the subschema only carries `required: [names]` — Gemini
897
- // validates that every name in `required` exists in *this*
898
- // subschema's `properties` (it does not inherit from the parent
899
- // the way JSON Schema's compositional model does).
900
- for (const combinator of ['anyOf', 'oneOf', 'allOf']) {
901
- if (Array.isArray(result[combinator])) {
902
- result[combinator] = result[combinator].map((s) => {
903
- const sub = convertSchema(s);
904
- if (sub && typeof sub === 'object') {
905
- const usesObjectKeys = sub.required !== undefined || sub.properties !== undefined;
906
- if (usesObjectKeys && sub.type === undefined) {
907
- sub.type = toSchemaType('object');
908
- }
909
- if (Array.isArray(sub.required) && !sub.properties && result.properties) {
910
- const projected = {};
911
- for (const k of sub.required) {
912
- if (result.properties[k]) projected[k] = result.properties[k];
913
- }
914
- if (Object.keys(projected).length > 0) sub.properties = projected;
915
- }
916
- }
917
- return sub;
918
- });
919
- }
920
- }
921
- if (result.not && typeof result.not === 'object') {
922
- result.not = convertSchema(result.not);
923
- }
924
- return result;
925
- }
926
-
927
- function toGeminiTools(tools) {
928
- return {
929
- functionDeclarations: tools.map((t) => ({
930
- name: t.name,
931
- description: t.description,
932
- parameters: convertSchema(t.inputSchema),
933
- })),
934
- };
935
- }
936
- function toGeminiNativeTools(nativeTools) {
937
- if (!Array.isArray(nativeTools)) return [];
938
- const out = [];
939
- for (const tool of nativeTools) {
940
- const type = String(tool?.type || '').trim().toLowerCase();
941
- if (type === 'google_search' || type === 'google_search_retrieval') {
942
- out.push({ googleSearch: {} });
943
- }
944
- }
945
- return out;
946
- }
947
-
948
- function collectGeminiGroundingSources(candidate) {
949
- const out = [];
950
- const seen = new Set();
951
- const add = (source) => {
952
- if (!source || typeof source !== 'object') return;
953
- const web = source.web && typeof source.web === 'object' ? source.web : source;
954
- const url = String(web.uri || web.url || source.uri || source.url || '').trim();
955
- if (!url || seen.has(url)) return;
956
- seen.add(url);
957
- out.push({
958
- title: String(web.title || source.title || url).trim(),
959
- url,
960
- snippet: '',
961
- source: 'gemini-grounding',
962
- provider: 'gemini',
963
- });
964
- };
965
- const grounding = candidate?.groundingMetadata || {};
966
- for (const chunk of Array.isArray(grounding.groundingChunks) ? grounding.groundingChunks : []) add(chunk);
967
- const citationMetadata = candidate?.citationMetadata || {};
968
- for (const source of Array.isArray(citationMetadata.citationSources) ? citationMetadata.citationSources : []) add(source);
969
- return out;
970
- }
971
-
972
- // Map the orchestrator-level toolChoice to Gemini's functionCallingConfig.
973
- // auto -> AUTO
974
- // required -> ANY
975
- // none -> NONE
976
- // { name } -> ANY + allowedFunctionNames:[name] (specific tool)
977
- function toGeminiToolConfig(toolChoice) {
978
- if (toolChoice == null) return undefined;
979
- if (typeof toolChoice === 'string') {
980
- if (toolChoice === 'auto') return { functionCallingConfig: { mode: 'AUTO' } };
981
- if (toolChoice === 'required') return { functionCallingConfig: { mode: 'ANY' } };
982
- if (toolChoice === 'none') return { functionCallingConfig: { mode: 'NONE' } };
983
- return undefined;
984
- }
985
- if (typeof toolChoice === 'object') {
986
- const name = toolChoice.name || toolChoice.function?.name;
987
- if (typeof name === 'string' && name) {
988
- return { functionCallingConfig: { mode: 'ANY', allowedFunctionNames: [name] } };
989
- }
990
- }
991
- return undefined;
992
- }
993
-
994
- function toGeminiContent(message, toolNameByCallId) {
995
- if (!message || message.role === 'system') return null;
996
- if (message.role === 'assistant' && message.toolCalls?.length) {
997
- const parts = [];
998
- if (message.content) parts.push(...normalizeContentForGeminiParts(message.content));
999
- for (const tc of message.toolCalls) {
1000
- // Gemini 3 thinking models require the original thoughtSignature
1001
- // echoed back on every prior functionCall so the cached thinking
1002
- // prefix stays valid. v1beta places the field at the Part level
1003
- // (sibling of functionCall) — putting it inside functionCall returns
1004
- // 400 "Unknown name". Older models / first turn have no signature.
1005
- const part = { functionCall: { name: tc.name, args: tc.arguments } };
1006
- if (tc.thoughtSignature) part.thoughtSignature = tc.thoughtSignature;
1007
- parts.push(part);
1008
- }
1009
- return { role: 'model', parts };
1010
- }
1011
- if (message.role === 'tool') {
1012
- // Tool result content stays byte-identical for cache prefix stability.
1013
- // Gemini accepts functionResponse parts under role 'user' (per docs).
1014
- // Using 'user' keeps tool_result entries byte-identical between
1015
- // cachedContents.create (which rejects role:'function') and
1016
- // generateContent, so the cached prefix actually matches at runtime.
1017
- // functionResponse.name must be the FUNCTION name, not the synthetic
1018
- // toolCallId. Resolve it from the toolCallId->functionName map built
1019
- // from prior assistant tool_calls; fall back to the raw id only when
1020
- // no mapping exists.
1021
- const functionName = (toolNameByCallId && toolNameByCallId.get(message.toolCallId))
1022
- || message.toolCallId
1023
- || '';
1024
- const { response, mediaParts } = splitToolContentForGemini(message.content);
1025
- const parts = [{ functionResponse: { name: functionName, response } }];
1026
- if (mediaParts.length) parts.push(...mediaParts);
1027
- return {
1028
- role: 'user',
1029
- parts,
1030
- };
1031
- }
1032
- return {
1033
- role: message.role === 'assistant' ? 'model' : 'user',
1034
- parts: normalizeContentForGeminiParts(message.content),
1035
- };
1036
- }
1037
-
1038
- function toGeminiContents(messages) {
1039
- const contents = [];
1040
- // Map synthetic toolCallId -> function name from prior assistant
1041
- // tool_calls so each functionResponse part carries the real function name.
1042
- const toolNameByCallId = new Map();
1043
- for (const m of messages) {
1044
- if (m?.role === 'assistant' && Array.isArray(m.toolCalls)) {
1045
- for (const tc of m.toolCalls) {
1046
- if (tc?.id && tc?.name) toolNameByCallId.set(tc.id, tc.name);
1047
- }
1048
- }
1049
- }
1050
- for (const message of messages) {
1051
- const content = toGeminiContent(message, toolNameByCallId);
1052
- if (content) contents.push(content);
1053
- }
1054
- return contents;
1055
- }
1056
-
1057
- export function parseToolCalls(parts) {
1058
- const calls = parts.filter((p) => 'functionCall' in p && !!p.functionCall);
1059
- if (!calls.length)
1060
- return undefined;
1061
- // The @google/generative-ai 0.24.1 SDK predates Gemini 3 thinking — its
1062
- // FunctionCall type only declares { name, args }. The runtime object,
1063
- // however, retains whatever the wire response carried, which means the
1064
- // signature may sit under any of:
1065
- // • part.functionCall.thoughtSignature (camelCase, expected)
1066
- // • part.functionCall.thought_signature (snake_case, raw protobuf)
1067
- // • part.thoughtSignature / part.thought_signature (sibling on Part)
1068
- // Read all four and use the first non-empty hit. Set MIXDOG_DEBUG_GEMINI=1
1069
- // to dump the raw parts so we can confirm the actual key location on the
1070
- // next session and harden the parser.
1071
- if (process.env.MIXDOG_DEBUG_GEMINI === '1') {
1072
- try { process.stderr.write(`[gemini fc raw] ${JSON.stringify(parts)}\n`); } catch {}
1073
- }
1074
- return calls.map((p, i) => {
1075
- const fc = p.functionCall;
1076
- const sig = fc.thoughtSignature
1077
- || fc.thought_signature
1078
- || p.thoughtSignature
1079
- || p.thought_signature
1080
- || null;
1081
- const idHash = traceHash(stableTraceStringify({
1082
- index: i,
1083
- name: fc.name || '',
1084
- args: fc.args ?? {},
1085
- })).slice(0, 16);
1086
- const call = {
1087
- id: `gemini_${idHash}`,
1088
- name: fc.name,
1089
- arguments: (fc.args ?? {}),
1090
- };
1091
- if (sig) call.thoughtSignature = sig;
1092
- return call;
1093
- });
1094
- }
1095
-
1096
- function emitGeminiToolCalls(toolCalls, onToolCall) {
1097
- if (typeof onToolCall !== 'function' || !Array.isArray(toolCalls)) return;
1098
- const emitted = new Set();
1099
- for (const call of toolCalls) {
1100
- if (!call?.id || !call?.name || emitted.has(call.id)) continue;
1101
- emitted.add(call.id);
1102
- try { onToolCall(call); } catch {}
1103
- }
1104
- }
131
+ // --- Cache accounting/trace: extracted to gemini-cache.mjs ---
132
+ // --- Stream consumption/guards: extracted to gemini-stream.mjs ---
133
+ // --- Schema/content/tool-call mapping: extracted to gemini-schema.mjs ---
1105
134
 
1106
135
  export class GeminiProvider {
1107
136
  // promptTokenCount is the total (cachedContentTokenCount is a subset), so
@@ -1851,7 +880,8 @@ export class GeminiProvider {
1851
880
  _markLatestGemini(normalized);
1852
881
  // LiteLLM catalog overlays pricing and updated metadata.
1853
882
  const { enrichModels } = await import('./model-catalog.mjs');
1854
- const enriched = await enrichModels(normalized);
883
+ const { sanitizeModelList } = await import('./model-list-sanitize.mjs');
884
+ const enriched = sanitizeModelList(await enrichModels(normalized), { provider: 'gemini' });
1855
885
  _modelCache.save(enriched);
1856
886
  return enriched;
1857
887
  }