@herbertgao/pi-extensions 2026.9.2 → 2026.9.4

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 (255) hide show
  1. package/README.md +4 -2
  2. package/THIRD_PARTY_NOTICES.md +49 -0
  3. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/compact-mode.ts +3 -2
  4. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/default-mode.ts +16 -11
  5. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/diff-renderer.ts +20 -6
  6. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/grouping.ts +12 -7
  7. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/result.ts +98 -0
  8. package/node_modules/@herbertgao/pi-cc-extensions/package.json +3 -3
  9. package/node_modules/@herbertgao/sol-pi/LICENSE +19 -0
  10. package/node_modules/@herbertgao/sol-pi/README.md +159 -0
  11. package/node_modules/@herbertgao/sol-pi/SECURITY.md +26 -0
  12. package/node_modules/@herbertgao/sol-pi/THIRD_PARTY_NOTICES.md +19 -0
  13. package/node_modules/@herbertgao/sol-pi/agents-install.md +150 -0
  14. package/node_modules/@herbertgao/sol-pi/assets/sol-pi-hero.png +0 -0
  15. package/node_modules/@herbertgao/sol-pi/docs/compatibility.md +69 -0
  16. package/node_modules/@herbertgao/sol-pi/docs/configuration.md +75 -0
  17. package/node_modules/@herbertgao/sol-pi/package.json +76 -0
  18. package/node_modules/@herbertgao/sol-pi/scripts/check-pi-compat.mjs +32 -0
  19. package/node_modules/@herbertgao/sol-pi/scripts/check-sol-pi-config.mjs +120 -0
  20. package/node_modules/@herbertgao/sol-pi/sol-pi.example.json +10 -0
  21. package/node_modules/@herbertgao/sol-pi/src/sol-pi/config.ts +135 -0
  22. package/node_modules/@herbertgao/sol-pi/src/sol-pi/extensions/action-fusion/file-queue.ts +74 -0
  23. package/node_modules/@herbertgao/sol-pi/src/sol-pi/extensions/action-fusion/index.ts +185 -0
  24. package/node_modules/@herbertgao/sol-pi/src/sol-pi/extensions/action-fusion/then-run.ts +128 -0
  25. package/node_modules/@herbertgao/sol-pi/src/sol-pi/extensions/evidence-preserving-reducer/archive.ts +53 -0
  26. package/node_modules/@herbertgao/sol-pi/src/sol-pi/extensions/evidence-preserving-reducer/candidate.ts +101 -0
  27. package/node_modules/@herbertgao/sol-pi/src/sol-pi/extensions/evidence-preserving-reducer/config.ts +71 -0
  28. package/node_modules/@herbertgao/sol-pi/src/sol-pi/extensions/evidence-preserving-reducer/index.ts +220 -0
  29. package/node_modules/@herbertgao/sol-pi/src/sol-pi/extensions/evidence-preserving-reducer/journal.ts +25 -0
  30. package/node_modules/@herbertgao/sol-pi/src/sol-pi/extensions/evidence-preserving-reducer/provider.ts +164 -0
  31. package/node_modules/@herbertgao/sol-pi/src/sol-pi/extensions/evidence-preserving-reducer/receipt.ts +177 -0
  32. package/node_modules/@herbertgao/sol-pi/src/sol-pi/extensions/observation-pack/index.ts +227 -0
  33. package/node_modules/@herbertgao/sol-pi/src/sol-pi/extensions/observation-pack/ledger.ts +20 -0
  34. package/node_modules/@herbertgao/sol-pi/src/sol-pi/extensions/observation-pack/observation.ts +252 -0
  35. package/node_modules/@herbertgao/sol-pi/src/sol-pi/extensions/online-context-compact/economics.ts +237 -0
  36. package/node_modules/@herbertgao/sol-pi/src/sol-pi/extensions/online-context-compact/extension.ts +455 -0
  37. package/node_modules/@herbertgao/sol-pi/src/sol-pi/extensions/online-context-compact/index.ts +49 -0
  38. package/node_modules/@herbertgao/sol-pi/src/sol-pi/extensions/online-context-compact/plan.ts +79 -0
  39. package/node_modules/@herbertgao/sol-pi/src/sol-pi/extensions/online-context-compact/state.ts +208 -0
  40. package/node_modules/@herbertgao/sol-pi/src/sol-pi/extensions/online-context-compact/tools.ts +100 -0
  41. package/node_modules/@herbertgao/sol-pi/src/sol-pi/index.ts +42 -0
  42. package/node_modules/@herbertgao/sol-pi/src/sol-pi/runtime-paths.ts +17 -0
  43. package/node_modules/@herbertgao/sol-pi/src/sol-pi/tui.ts +71 -0
  44. package/node_modules/@narumitw/pi-btw/dist/index.ts +12 -2
  45. package/node_modules/@narumitw/pi-btw/dist/index.ts.map +2 -2
  46. package/node_modules/@narumitw/pi-btw/package.json +1 -1
  47. package/node_modules/@narumitw/pi-btw/src/btw.ts +13 -2
  48. package/node_modules/pi-antigravity/LICENSE +21 -0
  49. package/node_modules/pi-antigravity/README.md +194 -0
  50. package/node_modules/pi-antigravity/package.json +67 -0
  51. package/node_modules/pi-antigravity/src/auth/index.ts +14 -0
  52. package/node_modules/pi-antigravity/src/auth/oauth.ts +442 -0
  53. package/node_modules/pi-antigravity/src/client/client.ts +561 -0
  54. package/node_modules/pi-antigravity/src/client/index.ts +1 -0
  55. package/node_modules/pi-antigravity/src/diagnostics/diagnostics.ts +96 -0
  56. package/node_modules/pi-antigravity/src/diagnostics/index.ts +1 -0
  57. package/node_modules/pi-antigravity/src/image/image.ts +336 -0
  58. package/node_modules/pi-antigravity/src/image/index.ts +1 -0
  59. package/node_modules/pi-antigravity/src/index.ts +280 -0
  60. package/node_modules/pi-antigravity/src/models/discovery.ts +154 -0
  61. package/node_modules/pi-antigravity/src/models/grouping.ts +424 -0
  62. package/node_modules/pi-antigravity/src/models/index.ts +3 -0
  63. package/node_modules/pi-antigravity/src/models/models.ts +500 -0
  64. package/node_modules/pi-antigravity/src/stream/index.ts +1 -0
  65. package/node_modules/pi-antigravity/src/stream/stream.ts +1460 -0
  66. package/node_modules/pi-antigravity/src/types/enums.ts +42 -0
  67. package/node_modules/pi-antigravity/src/types/index.ts +2 -0
  68. package/node_modules/pi-antigravity/src/types/types.ts +292 -0
  69. package/node_modules/pi-antigravity/src/usage/index.ts +1 -0
  70. package/node_modules/pi-antigravity/src/usage/usage.ts +371 -0
  71. package/node_modules/pi-antigravity/src/utils/http.ts +91 -0
  72. package/node_modules/pi-antigravity/src/utils/index.ts +3 -0
  73. package/node_modules/pi-antigravity/src/utils/security.ts +73 -0
  74. package/node_modules/pi-antigravity/src/utils/util.ts +132 -0
  75. package/node_modules/pi-antigravity/tsconfig.json +21 -0
  76. package/node_modules/pi-lens/CHANGELOG.md +176 -0
  77. package/node_modules/pi-lens/README.md +13 -8
  78. package/node_modules/pi-lens/config/dependency-cruiser-eager-allowlist.json +3 -1
  79. package/node_modules/pi-lens/dist/clients/analysed-root.js +1 -0
  80. package/node_modules/pi-lens/dist/clients/ast-grep-tool-logger.js +1 -1
  81. package/node_modules/pi-lens/dist/clients/biome-client.js +13 -2
  82. package/node_modules/pi-lens/dist/clients/bounded-telemetry.js +2 -0
  83. package/node_modules/pi-lens/dist/clients/cache-observability.js +122 -1
  84. package/node_modules/pi-lens/dist/clients/complexity-client.js +5 -0
  85. package/node_modules/pi-lens/dist/clients/config-diagnostic-codes.js +2 -0
  86. package/node_modules/pi-lens/dist/clients/config-resolve.js +6 -3
  87. package/node_modules/pi-lens/dist/clients/config-schema.js +17 -0
  88. package/node_modules/pi-lens/dist/clients/config-warn.js +2 -2
  89. package/node_modules/pi-lens/dist/clients/dead-code-client.js +8 -1
  90. package/node_modules/pi-lens/dist/clients/dependency-checker.js +5 -1
  91. package/node_modules/pi-lens/dist/clients/dispatch/dispatcher.js +38 -15
  92. package/node_modules/pi-lens/dist/clients/dispatch/facts/function-facts.js +1 -1
  93. package/node_modules/pi-lens/dist/clients/dispatch/facts/import-facts.js +5 -1
  94. package/node_modules/pi-lens/dist/clients/dispatch/runners/actionlint.js +2 -1
  95. package/node_modules/pi-lens/dist/clients/dispatch/runners/biome-check.js +2 -3
  96. package/node_modules/pi-lens/dist/clients/dispatch/runners/cpp-check.js +4 -3
  97. package/node_modules/pi-lens/dist/clients/dispatch/runners/credo.js +2 -1
  98. package/node_modules/pi-lens/dist/clients/dispatch/runners/cue-vet.js +2 -1
  99. package/node_modules/pi-lens/dist/clients/dispatch/runners/dart-analyze.js +2 -1
  100. package/node_modules/pi-lens/dist/clients/dispatch/runners/detekt.js +2 -1
  101. package/node_modules/pi-lens/dist/clients/dispatch/runners/dotnet-build.js +2 -1
  102. package/node_modules/pi-lens/dist/clients/dispatch/runners/elixir-check.js +2 -1
  103. package/node_modules/pi-lens/dist/clients/dispatch/runners/eslint.js +2 -1
  104. package/node_modules/pi-lens/dist/clients/dispatch/runners/fish-indent.js +2 -1
  105. package/node_modules/pi-lens/dist/clients/dispatch/runners/gleam-check.js +2 -1
  106. package/node_modules/pi-lens/dist/clients/dispatch/runners/go-vet.js +2 -1
  107. package/node_modules/pi-lens/dist/clients/dispatch/runners/golangci-lint.js +2 -1
  108. package/node_modules/pi-lens/dist/clients/dispatch/runners/hadolint.js +2 -1
  109. package/node_modules/pi-lens/dist/clients/dispatch/runners/helm-lint.js +3 -1
  110. package/node_modules/pi-lens/dist/clients/dispatch/runners/helm-render.js +2 -1
  111. package/node_modules/pi-lens/dist/clients/dispatch/runners/htmlhint.js +2 -1
  112. package/node_modules/pi-lens/dist/clients/dispatch/runners/javac.js +2 -1
  113. package/node_modules/pi-lens/dist/clients/dispatch/runners/ktlint.js +2 -1
  114. package/node_modules/pi-lens/dist/clients/dispatch/runners/lsp.js +19 -1
  115. package/node_modules/pi-lens/dist/clients/dispatch/runners/markdownlint.js +3 -2
  116. package/node_modules/pi-lens/dist/clients/dispatch/runners/mypy.js +2 -1
  117. package/node_modules/pi-lens/dist/clients/dispatch/runners/oxlint.js +2 -1
  118. package/node_modules/pi-lens/dist/clients/dispatch/runners/php-lint.js +2 -1
  119. package/node_modules/pi-lens/dist/clients/dispatch/runners/phpstan.js +2 -1
  120. package/node_modules/pi-lens/dist/clients/dispatch/runners/prisma-validate.js +2 -1
  121. package/node_modules/pi-lens/dist/clients/dispatch/runners/psscriptanalyzer.js +12 -12
  122. package/node_modules/pi-lens/dist/clients/dispatch/runners/pyright.js +2 -1
  123. package/node_modules/pi-lens/dist/clients/dispatch/runners/rubocop.js +2 -1
  124. package/node_modules/pi-lens/dist/clients/dispatch/runners/ruff.js +2 -2
  125. package/node_modules/pi-lens/dist/clients/dispatch/runners/rust-clippy.js +12 -11
  126. package/node_modules/pi-lens/dist/clients/dispatch/runners/shellcheck.js +2 -1
  127. package/node_modules/pi-lens/dist/clients/dispatch/runners/shfmt.js +2 -1
  128. package/node_modules/pi-lens/dist/clients/dispatch/runners/spellcheck.js +2 -2
  129. package/node_modules/pi-lens/dist/clients/dispatch/runners/spotbugs.js +2 -1
  130. package/node_modules/pi-lens/dist/clients/dispatch/runners/sqlfluff.js +3 -2
  131. package/node_modules/pi-lens/dist/clients/dispatch/runners/stylelint.js +3 -2
  132. package/node_modules/pi-lens/dist/clients/dispatch/runners/swiftlint.js +2 -1
  133. package/node_modules/pi-lens/dist/clients/dispatch/runners/taplo.js +2 -1
  134. package/node_modules/pi-lens/dist/clients/dispatch/runners/terragrunt.js +2 -1
  135. package/node_modules/pi-lens/dist/clients/dispatch/runners/tflint.js +2 -1
  136. package/node_modules/pi-lens/dist/clients/dispatch/runners/trivy-config.js +2 -1
  137. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/availability-policy.js +5 -0
  138. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/candidate-probe.js +2 -2
  139. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/runner-helpers.js +8 -2
  140. package/node_modules/pi-lens/dist/clients/dispatch/runners/vale.js +2 -1
  141. package/node_modules/pi-lens/dist/clients/dispatch/runners/yamllint.js +4 -3
  142. package/node_modules/pi-lens/dist/clients/dispatch/runners/zig-check.js +2 -1
  143. package/node_modules/pi-lens/dist/clients/effective-config.js +12 -2
  144. package/node_modules/pi-lens/dist/clients/extension-log.js +2 -0
  145. package/node_modules/pi-lens/dist/clients/file-role.js +18 -1
  146. package/node_modules/pi-lens/dist/clients/finding-delivery-gate.js +4 -0
  147. package/node_modules/pi-lens/dist/clients/formatters.js +54 -188
  148. package/node_modules/pi-lens/dist/clients/generation-guard.js +4 -0
  149. package/node_modules/pi-lens/dist/clients/gitleaks-client.js +42 -6
  150. package/node_modules/pi-lens/dist/clients/govulncheck-client.js +48 -7
  151. package/node_modules/pi-lens/dist/clients/installer/index.js +196 -52
  152. package/node_modules/pi-lens/dist/clients/jscpd-client.js +9 -1
  153. package/node_modules/pi-lens/dist/clients/knip-client.js +9 -2
  154. package/node_modules/pi-lens/dist/clients/language-profile.js +12 -2
  155. package/node_modules/pi-lens/dist/clients/latency-logger.js +2 -0
  156. package/node_modules/pi-lens/dist/clients/lens-config.js +4 -0
  157. package/node_modules/pi-lens/dist/clients/lsp/config.js +36 -7
  158. package/node_modules/pi-lens/dist/clients/lsp/diagnostic-binding.js +24 -1
  159. package/node_modules/pi-lens/dist/clients/lsp/document-drift.js +78 -0
  160. package/node_modules/pi-lens/dist/clients/lsp/index.js +281 -45
  161. package/node_modules/pi-lens/dist/clients/lsp/server.js +62 -15
  162. package/node_modules/pi-lens/dist/clients/lsp/workspace-diagnostics-cache.js +37 -4
  163. package/node_modules/pi-lens/dist/clients/mcp/analyze.js +3 -0
  164. package/node_modules/pi-lens/dist/clients/mcp/session.js +2 -0
  165. package/node_modules/pi-lens/dist/clients/opengrep-client.js +2 -0
  166. package/node_modules/pi-lens/dist/clients/package-manager.js +2 -1
  167. package/node_modules/pi-lens/dist/clients/php-cs-fixer-config.js +8 -3
  168. package/node_modules/pi-lens/dist/clients/pipeline.js +3 -2
  169. package/node_modules/pi-lens/dist/clients/project-diagnostics/fresh-fetch.js +38 -10
  170. package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/gitleaks.js +10 -9
  171. package/node_modules/pi-lens/dist/clients/project-lens-config.js +3 -0
  172. package/node_modules/pi-lens/dist/clients/read-guard-logger.js +2 -0
  173. package/node_modules/pi-lens/dist/clients/review-graph-logger.js +2 -0
  174. package/node_modules/pi-lens/dist/clients/ruff-client.js +18 -9
  175. package/node_modules/pi-lens/dist/clients/runtime-agent-end.js +9 -0
  176. package/node_modules/pi-lens/dist/clients/runtime-coordinator.js +5 -0
  177. package/node_modules/pi-lens/dist/clients/runtime-session.js +13 -4
  178. package/node_modules/pi-lens/dist/clients/runtime-tool-result.js +4 -1
  179. package/node_modules/pi-lens/dist/clients/runtime-turn.js +48 -1
  180. package/node_modules/pi-lens/dist/clients/security-scan-client.js +2 -2
  181. package/node_modules/pi-lens/dist/clients/session-event-guard.js +69 -1
  182. package/node_modules/pi-lens/dist/clients/sg-runner.js +2 -1
  183. package/node_modules/pi-lens/dist/clients/situational-tool-telemetry.js +108 -0
  184. package/node_modules/pi-lens/dist/clients/test-runner-client.js +291 -76
  185. package/node_modules/pi-lens/dist/clients/tool-config.js +199 -0
  186. package/node_modules/pi-lens/dist/clients/tool-cwd.js +299 -0
  187. package/node_modules/pi-lens/dist/clients/tool-probe.js +53 -0
  188. package/node_modules/pi-lens/dist/clients/tool-set-policy.js +42 -0
  189. package/node_modules/pi-lens/dist/clients/trivy-client.js +9 -1
  190. package/node_modules/pi-lens/dist/clients/turn-context.js +52 -0
  191. package/node_modules/pi-lens/dist/clients/widget-state.js +4 -3
  192. package/node_modules/pi-lens/dist/index.js +21969 -20005
  193. package/node_modules/pi-lens/dist/mcp/analyze-cli.js +3 -2
  194. package/node_modules/pi-lens/dist/mcp/server.js +135 -159
  195. package/node_modules/pi-lens/dist/tools/activate-tools.js +10 -7
  196. package/node_modules/pi-lens/dist/tools/ast-grep-outline.js +2 -13
  197. package/node_modules/pi-lens/dist/tools/ast-grep-replace.js +8 -13
  198. package/node_modules/pi-lens/dist/tools/ast-grep-search.js +83 -39
  199. package/node_modules/pi-lens/dist/tools/effective-config.js +4 -3
  200. package/node_modules/pi-lens/dist/tools/lens-diagnostic-mark.js +1 -10
  201. package/node_modules/pi-lens/dist/tools/lens-diagnostics.js +353 -85
  202. package/node_modules/pi-lens/dist/tools/lsp-diagnostics.js +21 -110
  203. package/node_modules/pi-lens/dist/tools/lsp-navigation.js +20 -44
  204. package/node_modules/pi-lens/dist/tools/module-report.js +5 -9
  205. package/node_modules/pi-lens/dist/tools/project-report.js +9 -17
  206. package/node_modules/pi-lens/dist/tools/render-compact.js +280 -1
  207. package/node_modules/pi-lens/dist/tools/symbol-search.js +2 -2
  208. package/node_modules/pi-lens/docs/agent-guide.md +3 -4
  209. package/node_modules/pi-lens/docs/agent-tools.md +46 -22
  210. package/node_modules/pi-lens/docs/configuration.md +9 -0
  211. package/node_modules/pi-lens/docs/environment-variables.md +0 -5
  212. package/node_modules/pi-lens/docs/features.md +5 -5
  213. package/node_modules/pi-lens/docs/globalconfig.md +11 -1
  214. package/node_modules/pi-lens/docs/mcp.md +13 -1
  215. package/node_modules/pi-lens/docs/pi-lens-fixer.md +71 -0
  216. package/node_modules/pi-lens/docs/pi-lens-investigator.md +15 -0
  217. package/node_modules/pi-lens/docs/pi-lens-monitor.md +88 -0
  218. package/node_modules/pi-lens/docs/pi-lens-reviewer.md +35 -0
  219. package/node_modules/pi-lens/docs/pi-lens-subagent.md +12 -4
  220. package/node_modules/pi-lens/docs/public-api-stability.md +1 -0
  221. package/node_modules/pi-lens/docs/real-harness.md +46 -0
  222. package/node_modules/pi-lens/docs/release-qa-baseline.md +5 -1
  223. package/node_modules/pi-lens/docs/servercapabilities.md +1 -6
  224. package/node_modules/pi-lens/docs/settings.md +37 -2
  225. package/node_modules/pi-lens/docs/tools_improvement2.md +4 -4
  226. package/node_modules/pi-lens/docs/tree-sitter_rules_catalog.md +1 -1
  227. package/node_modules/pi-lens/docs/usage.md +13 -3
  228. package/node_modules/pi-lens/package.json +10 -1
  229. package/node_modules/pi-lens/rules/tree-sitter-queries/typescript/sql-injection.yml +7 -3
  230. package/node_modules/pi-lens/skills/pi-lens-ast-grep/SKILL.md +8 -6
  231. package/node_modules/pi-lens/skills/pi-lens-lsp-navigation/SKILL.md +19 -9
  232. package/node_modules/pi-web-access/CHANGELOG.md +29 -0
  233. package/node_modules/pi-web-access/README.md +33 -22
  234. package/node_modules/pi-web-access/abortable.ts +17 -0
  235. package/node_modules/pi-web-access/crawl4ai.ts +204 -0
  236. package/node_modules/pi-web-access/credential-source.ts +1 -0
  237. package/node_modules/pi-web-access/curator-page.ts +20 -2
  238. package/node_modules/pi-web-access/curator-run.ts +44 -0
  239. package/node_modules/pi-web-access/curator-server.ts +3 -1
  240. package/node_modules/pi-web-access/duckduckgo.ts +1 -1
  241. package/node_modules/pi-web-access/extract.ts +57 -12
  242. package/node_modules/pi-web-access/fetch-params.ts +1 -1
  243. package/node_modules/pi-web-access/gemini-search.ts +9 -5
  244. package/node_modules/pi-web-access/index.ts +25 -18
  245. package/node_modules/pi-web-access/openai-search.ts +35 -13
  246. package/node_modules/pi-web-access/package.json +2 -2
  247. package/node_modules/pi-web-access/page-query.ts +5 -2
  248. package/node_modules/pi-web-access/query-rewrite.ts +5 -2
  249. package/node_modules/pi-web-access/serpapi.ts +220 -0
  250. package/node_modules/pi-web-access/ssrf-protection.ts +5 -1
  251. package/node_modules/pi-web-access/storage.ts +10 -0
  252. package/node_modules/pi-web-access/summary-review.ts +34 -21
  253. package/node_modules/pi-web-access/utils.ts +6 -4
  254. package/package.json +11 -5
  255. package/node_modules/pi-lens/dist/tools/ast-dump.js +0 -103
@@ -0,0 +1,1460 @@
1
+ import {
2
+ calculateCost,
3
+ createAssistantMessageEventStream,
4
+ type Api,
5
+ type AssistantMessage,
6
+ type AssistantMessageEventStream,
7
+ type Context,
8
+ type Model,
9
+ type TextContent,
10
+ type Tool,
11
+ type ToolCall,
12
+ } from "@earendil-works/pi-ai";
13
+ import {
14
+ antigravityHeaders,
15
+ endpointCandidates,
16
+ fetchAvailableRuntimeModel,
17
+ formatRequestDiagnostics,
18
+ jsonOrTextError,
19
+ loadCodeAssist,
20
+ parseApiKey,
21
+ resolveProjectId,
22
+ } from "../client/client.js";
23
+ import {
24
+ getCurrentEndpoint,
25
+ runWithDiagnostics,
26
+ setLastEndpoint,
27
+ setLastError,
28
+ setLastLatencyMs,
29
+ setLastProjectId,
30
+ setLastResolvedRuntimeModel,
31
+ setLastStatus,
32
+ setLastToolSchemaWarnings,
33
+ } from "../diagnostics/diagnostics.js";
34
+ import {
35
+ AntigravityRequestType,
36
+ AntigravityUserAgent,
37
+ GeminiRole,
38
+ GeminiToolCallingMode,
39
+ StopReason,
40
+ ToolChoice,
41
+ } from "../types/enums.js";
42
+ import {
43
+ getCurrentAntigravityRouting,
44
+ getMaxOutputTokens,
45
+ getAntigravityRequestModelId,
46
+ getFallbackRuntimeModel,
47
+ getThinkingConfig,
48
+ PROVIDER_ID,
49
+ } from "../models/models.js";
50
+ import { redactSecrets, safeError } from "../utils/security.js";
51
+ import {
52
+ ANTIGRAVITY_API,
53
+ type ActiveBlock,
54
+ type AntigravityGenerateRequest,
55
+ type AntigravityStreamOptions,
56
+ type ContentBlock,
57
+ type GeminiContent,
58
+ type GeminiFunctionDeclaration,
59
+ type GeminiFunctionResponsePart,
60
+ type GeminiGenerationConfig,
61
+ type GeminiInlineDataPart,
62
+ type GeminiPart,
63
+ type GeminiRequestBody,
64
+ type GeminiTextPart,
65
+ type StreamChunk,
66
+ } from "../types/types.js";
67
+ import {
68
+ antigravityEnv,
69
+ antigravityRequestEnvelope,
70
+ isRecord,
71
+ resolveSessionTrajectory,
72
+ sanitizeText,
73
+ } from "../utils/util.js";
74
+ import { antigravityFetch } from "../utils/http.js";
75
+
76
+ export { ANTIGRAVITY_API };
77
+
78
+ const ANTIGRAVITY_SYSTEM_INSTRUCTION =
79
+ "You are Antigravity, a powerful agentic AI coding assistant designed by Google DeepMind. " +
80
+ "You are pair programming with a user to solve coding tasks. Be concise, practical, and tool-aware.";
81
+
82
+ const ANTIGRAVITY_NO_PREAMBLE_INSTRUCTION =
83
+ 'CRITICAL: NEVER output rule checks, formatting guidelines, constraint checklists (e.g. "No emdashes"), or your thinking/personality preambles in the final response. Output only the final response.';
84
+
85
+ let toolCallCounter = 0;
86
+
87
+ function sanitizeToolCallId(id: string, fallbackName?: string): string {
88
+ const cleaned = id.replace(/[^a-zA-Z0-9_-]/g, "_");
89
+ const capped = cleaned.slice(0, 64);
90
+ return capped || `${fallbackName || "tool"}_${++toolCallCounter}`;
91
+ }
92
+
93
+ function toolCallIdNeeded(modelId: string, runtimeModel: string): boolean {
94
+ return (
95
+ modelId.startsWith("claude-") ||
96
+ modelId.startsWith("gpt-oss-") ||
97
+ runtimeModel.startsWith("claude-") ||
98
+ runtimeModel.startsWith("gpt-oss-")
99
+ );
100
+ }
101
+
102
+ const base64SignaturePattern = /^[A-Za-z0-9+/]+={0,2}$/;
103
+ function isValidThoughtSignature(signature?: string): boolean {
104
+ if (!signature || typeof signature !== "string" || signature.length === 0) return false;
105
+ if (signature.length % 4 !== 0) return false;
106
+ return base64SignaturePattern.test(signature);
107
+ }
108
+
109
+ function geminiRequiresThoughtSignature(runtimeModel: string): boolean {
110
+ if (!runtimeModel.startsWith("gemini-")) return false;
111
+ const match = runtimeModel.match(/^gemini-(\d+)/);
112
+ if (match) {
113
+ const major = Number.parseInt(match[1], 10);
114
+ return major >= 3;
115
+ }
116
+ return true;
117
+ }
118
+
119
+ function parseImageData(raw: string, explicitMime?: string): { data: string; mimeType: string } {
120
+ const match = raw.match(/^data:([^;]+);base64,(.+)$/s);
121
+ if (match) {
122
+ return {
123
+ mimeType: explicitMime || match[1] || "image/png",
124
+ data: match[2].trim(),
125
+ };
126
+ }
127
+ return {
128
+ mimeType: explicitMime || "image/png",
129
+ data: raw.trim(),
130
+ };
131
+ }
132
+
133
+ const SKILL_BLOCK_PATTERN = /<skill\b[^>]*>[\s\S]*?<\/skill\s*>/gi;
134
+
135
+ function skillBlocks(content: unknown): string[] {
136
+ const texts =
137
+ typeof content === "string"
138
+ ? [content]
139
+ : Array.isArray(content)
140
+ ? content.flatMap((item) =>
141
+ isRecord(item) && item.type === "text" && typeof item.text === "string"
142
+ ? [item.text]
143
+ : [],
144
+ )
145
+ : [];
146
+ return texts.flatMap((text) => text.match(SKILL_BLOCK_PATTERN) || []);
147
+ }
148
+
149
+ function withoutSkillBlocks(text: string): string {
150
+ return text.replace(SKILL_BLOCK_PATTERN, "");
151
+ }
152
+
153
+ function asTextParts(content: unknown): Array<GeminiTextPart | GeminiInlineDataPart> {
154
+ const textPart = (text: string): GeminiTextPart[] => {
155
+ const userText = withoutSkillBlocks(text);
156
+ return userText.trim() ? [{ text: sanitizeText(userText) }] : [];
157
+ };
158
+ if (typeof content === "string") return textPart(content);
159
+ if (!Array.isArray(content)) return [];
160
+ return content.flatMap((item): Array<GeminiTextPart | GeminiInlineDataPart> => {
161
+ if (!isRecord(item)) return [];
162
+ const block = item as ContentBlock;
163
+ if (block.type === "text") return textPart(block.text);
164
+ if (block.type === "image") {
165
+ const rawData = block.data || block.source?.data;
166
+ if (!rawData) return [];
167
+ const explicitMime = block.mimeType || block.mediaType || block.source?.mediaType;
168
+ const { data, mimeType } = parseImageData(rawData, explicitMime);
169
+ return data ? [{ inlineData: { mimeType, data } }] : [];
170
+ }
171
+ return [];
172
+ });
173
+ }
174
+
175
+ function asImageParts(content: unknown): GeminiInlineDataPart[] {
176
+ if (!Array.isArray(content)) return [];
177
+ return content.flatMap((item): GeminiInlineDataPart[] => {
178
+ if (!isRecord(item)) return [];
179
+ const block = item as ContentBlock;
180
+ if (block.type === "image") {
181
+ const rawData = block.data || block.source?.data;
182
+ if (!rawData) return [];
183
+ const explicitMime = block.mimeType || block.mediaType || block.source?.mediaType;
184
+ const { data, mimeType } = parseImageData(rawData, explicitMime);
185
+ return data ? [{ inlineData: { mimeType, data } }] : [];
186
+ }
187
+ return [];
188
+ });
189
+ }
190
+
191
+ function appendTurn(contents: GeminiContent[], role: GeminiRole, parts: GeminiPart[]): void {
192
+ if (!parts.length) return;
193
+ const last = contents[contents.length - 1];
194
+ if (last && last.role === role) {
195
+ last.parts.push(...parts);
196
+ } else {
197
+ contents.push({ role, parts });
198
+ }
199
+ }
200
+
201
+ /** Exported for unit tests. */
202
+ export function convertMessages(
203
+ model: Model<Api>,
204
+ context: Context,
205
+ runtimeModel: string,
206
+ ): GeminiContent[] {
207
+ const contents: GeminiContent[] = [];
208
+ const requiresSig = geminiRequiresThoughtSignature(runtimeModel);
209
+ const droppedToolCallIds = new Map<string, string>();
210
+ for (const msg of context.messages) {
211
+ if (msg.role === "user") {
212
+ const parts = asTextParts(msg.content);
213
+ appendTurn(contents, GeminiRole.User, parts);
214
+ } else if (msg.role === "assistant") {
215
+ if (msg.stopReason === "error" || msg.stopReason === "aborted") {
216
+ continue;
217
+ }
218
+ const parts: GeminiPart[] = [];
219
+ const isSameModel = msg.provider === PROVIDER_ID && msg.model === model.id;
220
+ const toolCalls = msg.content.filter((b): b is ToolCall => b.type === "toolCall");
221
+ const firstCallHasSig =
222
+ toolCalls.length > 0 && isValidThoughtSignature(toolCalls[0]?.thoughtSignature);
223
+ const allSigsValid = toolCalls.every(
224
+ (tc) => !tc.thoughtSignature || isValidThoughtSignature(tc.thoughtSignature),
225
+ );
226
+ const groupIsSigned = isSameModel && firstCallHasSig && allSigsValid;
227
+
228
+ for (const block of msg.content) {
229
+ if (block.type === "text") {
230
+ const textSig =
231
+ isSameModel && isValidThoughtSignature(block.textSignature)
232
+ ? block.textSignature
233
+ : undefined;
234
+ if ((!block.text || block.text.trim() === "") && !textSig) {
235
+ continue;
236
+ }
237
+ parts.push({
238
+ text: sanitizeText(block.text),
239
+ ...(textSig ? { thoughtSignature: textSig } : {}),
240
+ });
241
+ } else if (block.type === "thinking" && String(block.thinking || "").trim()) {
242
+ if (!isSameModel) continue;
243
+ parts.push({
244
+ thought: true,
245
+ text: sanitizeText(block.thinking),
246
+ ...(block.thinkingSignature ? { thoughtSignature: block.thinkingSignature } : {}),
247
+ });
248
+ } else if (block.type === "toolCall") {
249
+ if (requiresSig && !groupIsSigned) {
250
+ const rawId = block.id || "";
251
+ const argsText = (() => {
252
+ try {
253
+ return JSON.stringify(block.arguments ?? {});
254
+ } catch {
255
+ return "{}";
256
+ }
257
+ })();
258
+ if (rawId) {
259
+ droppedToolCallIds.set(rawId, argsText);
260
+ droppedToolCallIds.set(sanitizeToolCallId(rawId, block.name), argsText);
261
+ } else {
262
+ droppedToolCallIds.set(`empty:${block.name}`, argsText);
263
+ }
264
+ } else {
265
+ parts.push({
266
+ functionCall: {
267
+ name: block.name,
268
+ args: block.arguments ?? {},
269
+ ...(toolCallIdNeeded(model.id, runtimeModel)
270
+ ? { id: sanitizeToolCallId(block.id || "", block.name) }
271
+ : {}),
272
+ },
273
+ ...(block.thoughtSignature ? { thoughtSignature: block.thoughtSignature } : {}),
274
+ });
275
+ }
276
+ }
277
+ }
278
+ appendTurn(contents, GeminiRole.Model, parts);
279
+ } else if (msg.role === "toolResult") {
280
+ const text = msg.content
281
+ .filter((c): c is TextContent => c.type === "text")
282
+ .map((c) => sanitizeText(c.text))
283
+ .join("\n");
284
+ const responseText = text || (msg.isError ? "Tool failed" : "");
285
+ const imageParts = asImageParts(msg.content);
286
+ const rawId = msg.toolCallId || "";
287
+ const sanitizedId = toolCallIdNeeded(model.id, runtimeModel)
288
+ ? sanitizeToolCallId(rawId, msg.toolName)
289
+ : rawId;
290
+ const droppedArgs = requiresSig
291
+ ? (droppedToolCallIds.get(rawId) ??
292
+ droppedToolCallIds.get(sanitizedId) ??
293
+ (rawId === "" ? droppedToolCallIds.get(`empty:${msg.toolName}`) : undefined))
294
+ : undefined;
295
+ if (droppedArgs !== undefined) {
296
+ const label =
297
+ droppedArgs === "{}" ? `\`${msg.toolName}\`` : `\`${msg.toolName}\` (${droppedArgs})`;
298
+ appendTurn(contents, GeminiRole.User, [
299
+ { text: sanitizeText(`[Observation from ${label}:\n${responseText}]`) },
300
+ ...imageParts,
301
+ ]);
302
+ } else {
303
+ const part: GeminiFunctionResponsePart = {
304
+ functionResponse: {
305
+ name: msg.toolName,
306
+ response: msg.isError ? { error: responseText } : { output: responseText },
307
+ ...(toolCallIdNeeded(model.id, runtimeModel)
308
+ ? { id: sanitizeToolCallId(msg.toolCallId || "", msg.toolName) }
309
+ : {}),
310
+ },
311
+ };
312
+ appendTurn(contents, GeminiRole.User, [part, ...imageParts]);
313
+ }
314
+ }
315
+ }
316
+
317
+ // Google Antigravity requires a natural-language user part in the request,
318
+ // including tool-only continuation turns. Keep injected Skills in the system
319
+ // instruction, then add this protocol bridge only when existing context gives
320
+ // the model something concrete to act on.
321
+ const hasUserText = contents.some(
322
+ (turn) =>
323
+ turn.role === GeminiRole.User &&
324
+ turn.parts.some((part) => "text" in part && Boolean(part.text.trim())),
325
+ );
326
+ if (!hasUserText && contents.length > 0) {
327
+ const bridge = {
328
+ text: "Continue the active task using the available instructions and context.",
329
+ };
330
+ const userTurn = contents.find((turn) => turn.role === GeminiRole.User);
331
+ if (userTurn) userTurn.parts.push(bridge);
332
+ else contents.unshift({ role: GeminiRole.User, parts: [bridge] });
333
+ }
334
+
335
+ return contents;
336
+ }
337
+
338
+ type SchemaReferenceIssue = {
339
+ path: string;
340
+ ref: string;
341
+ reason: string;
342
+ };
343
+
344
+ type DereferencedSchema = {
345
+ schema: unknown;
346
+ issues: SchemaReferenceIssue[];
347
+ };
348
+
349
+ type DereferenceState = {
350
+ nodes: number;
351
+ };
352
+
353
+ const MAX_SCHEMA_DEREFERENCE_DEPTH = 64;
354
+ const MAX_SCHEMA_DEREFERENCE_NODES = 10_000;
355
+ const SCHEMA_MAP_KEYWORDS = new Set([
356
+ "properties",
357
+ "patternProperties",
358
+ "dependentSchemas",
359
+ "dependencies",
360
+ ]);
361
+ const SCHEMA_VALUE_KEYWORDS = new Set([
362
+ "additionalItems",
363
+ "additionalProperties",
364
+ "contains",
365
+ "contentSchema",
366
+ "else",
367
+ "if",
368
+ "items",
369
+ "not",
370
+ "propertyNames",
371
+ "then",
372
+ "unevaluatedItems",
373
+ "unevaluatedProperties",
374
+ ]);
375
+ const SCHEMA_ARRAY_KEYWORDS = new Set(["allOf", "anyOf", "oneOf", "prefixItems"]);
376
+
377
+ /** Resolve a local RFC 6901 JSON Pointer against the original tool schema. */
378
+ function resolveLocalJsonPointer(ref: string, rootSchema: unknown): unknown {
379
+ if (ref === "#") return rootSchema;
380
+ if (!ref.startsWith("#/")) return undefined;
381
+
382
+ let current: unknown = rootSchema;
383
+ for (const token of ref.slice(2).split("/")) {
384
+ const key = token.replace(/~1/g, "/").replace(/~0/g, "~");
385
+ if (Array.isArray(current)) {
386
+ if (!/^(0|[1-9]\d*)$/.test(key)) return undefined;
387
+ const index = Number(key);
388
+ if (!Number.isSafeInteger(index) || index >= current.length) return undefined;
389
+ current = current[index];
390
+ continue;
391
+ }
392
+ if (!current || typeof current !== "object") return undefined;
393
+ if (!Object.prototype.hasOwnProperty.call(current, key)) return undefined;
394
+ current = (current as Record<string, unknown>)[key];
395
+ }
396
+ return current;
397
+ }
398
+
399
+ /** Dereference every schema in a keyword map without interpreting map keys as JSON Schema keywords. */
400
+ function dereferenceSchemaMap(
401
+ schemaMap: unknown,
402
+ rootSchema: unknown,
403
+ refStack: Set<string>,
404
+ objectStack: Set<object>,
405
+ state: DereferenceState,
406
+ path: string,
407
+ depth: number,
408
+ ): DereferencedSchema {
409
+ if (!isRecord(schemaMap)) {
410
+ return dereferenceSchema(schemaMap, rootSchema, refStack, objectStack, state, path, depth);
411
+ }
412
+
413
+ const out: Record<string, unknown> = {};
414
+ const issues: SchemaReferenceIssue[] = [];
415
+ for (const [key, value] of Object.entries(schemaMap)) {
416
+ const result = dereferenceSchema(
417
+ value,
418
+ rootSchema,
419
+ refStack,
420
+ objectStack,
421
+ state,
422
+ `${path}.${key}`,
423
+ depth,
424
+ );
425
+ out[key] = result.schema;
426
+ issues.push(...result.issues);
427
+ }
428
+ return { schema: out, issues };
429
+ }
430
+
431
+ /**
432
+ * Inline reachable local references while bounding expansion of untrusted MCP schemas.
433
+ * A reported issue causes only the affected tool declaration to be omitted.
434
+ */
435
+ function dereferenceSchema(
436
+ schema: unknown,
437
+ rootSchema: unknown = schema,
438
+ refStack = new Set<string>(),
439
+ objectStack = new Set<object>(),
440
+ state: DereferenceState = { nodes: 0 },
441
+ path = "$",
442
+ depth = 0,
443
+ ): DereferencedSchema {
444
+ if (depth > MAX_SCHEMA_DEREFERENCE_DEPTH) {
445
+ return {
446
+ schema: {},
447
+ issues: [
448
+ {
449
+ path,
450
+ ref: "(depth limit)",
451
+ reason: `schema expansion exceeded ${MAX_SCHEMA_DEREFERENCE_DEPTH} levels`,
452
+ },
453
+ ],
454
+ };
455
+ }
456
+ state.nodes += 1;
457
+ if (state.nodes > MAX_SCHEMA_DEREFERENCE_NODES) {
458
+ return {
459
+ schema: {},
460
+ issues: [
461
+ {
462
+ path,
463
+ ref: "(node limit)",
464
+ reason: `schema expansion exceeded ${MAX_SCHEMA_DEREFERENCE_NODES} nodes`,
465
+ },
466
+ ],
467
+ };
468
+ }
469
+ if (!schema || typeof schema !== "object") return { schema, issues: [] };
470
+
471
+ if (Array.isArray(schema)) {
472
+ const results = schema.map((item, index) =>
473
+ dereferenceSchema(
474
+ item,
475
+ rootSchema,
476
+ refStack,
477
+ objectStack,
478
+ state,
479
+ `${path}[${index}]`,
480
+ depth + 1,
481
+ ),
482
+ );
483
+ return {
484
+ schema: results.map((result) => result.schema),
485
+ issues: results.flatMap((result) => result.issues),
486
+ };
487
+ }
488
+
489
+ const s = schema as Record<string, unknown>;
490
+ if (objectStack.has(s)) {
491
+ return {
492
+ schema: {},
493
+ issues: [{ path, ref: "(object cycle)", reason: "circular schema object" }],
494
+ };
495
+ }
496
+
497
+ const nextObjectStack = new Set(objectStack);
498
+ nextObjectStack.add(s);
499
+
500
+ if (typeof s.$ref === "string") {
501
+ const ref = s.$ref;
502
+ if (refStack.has(ref)) {
503
+ return {
504
+ schema: {},
505
+ issues: [{ path, ref, reason: "circular local reference" }],
506
+ };
507
+ }
508
+
509
+ const target = resolveLocalJsonPointer(ref, rootSchema);
510
+ if (target === undefined) {
511
+ return {
512
+ schema: {},
513
+ issues: [{ path, ref, reason: "target is not present in the root schema" }],
514
+ };
515
+ }
516
+
517
+ const nextRefStack = new Set(refStack);
518
+ nextRefStack.add(ref);
519
+ const resolved = dereferenceSchema(
520
+ target,
521
+ rootSchema,
522
+ nextRefStack,
523
+ nextObjectStack,
524
+ state,
525
+ path,
526
+ depth + 1,
527
+ );
528
+ const { $ref: _, ...siblings } = s;
529
+ const siblingResult = dereferenceSchema(
530
+ siblings,
531
+ rootSchema,
532
+ refStack,
533
+ nextObjectStack,
534
+ state,
535
+ path,
536
+ depth + 1,
537
+ );
538
+
539
+ if (isRecord(resolved.schema) && isRecord(siblingResult.schema)) {
540
+ return {
541
+ schema: { ...resolved.schema, ...siblingResult.schema },
542
+ issues: [...resolved.issues, ...siblingResult.issues],
543
+ };
544
+ }
545
+ return {
546
+ schema: resolved.schema,
547
+ issues: [...resolved.issues, ...siblingResult.issues],
548
+ };
549
+ }
550
+
551
+ const out: Record<string, unknown> = {};
552
+ const issues: SchemaReferenceIssue[] = [];
553
+ for (const [key, value] of Object.entries(s)) {
554
+ // Definitions are available through rootSchema while resolving $ref, but must
555
+ // not be emitted because the Antigravity backend requires self-contained schemas.
556
+ if (key === "$defs" || key === "definitions") continue;
557
+
558
+ let result: DereferencedSchema | undefined;
559
+ if (SCHEMA_MAP_KEYWORDS.has(key)) {
560
+ result = dereferenceSchemaMap(
561
+ value,
562
+ rootSchema,
563
+ refStack,
564
+ nextObjectStack,
565
+ state,
566
+ `${path}.${key}`,
567
+ depth + 1,
568
+ );
569
+ } else if (SCHEMA_VALUE_KEYWORDS.has(key) || SCHEMA_ARRAY_KEYWORDS.has(key)) {
570
+ result = dereferenceSchema(
571
+ value,
572
+ rootSchema,
573
+ refStack,
574
+ nextObjectStack,
575
+ state,
576
+ `${path}.${key}`,
577
+ depth + 1,
578
+ );
579
+ }
580
+
581
+ if (result) {
582
+ out[key] = result.schema;
583
+ issues.push(...result.issues);
584
+ } else {
585
+ out[key] = value;
586
+ }
587
+ }
588
+ return { schema: out, issues };
589
+ }
590
+
591
+ function ensureRootObjectSchema(schema: unknown): Record<string, unknown> {
592
+ if (!isRecord(schema)) {
593
+ return { type: "object", properties: {} };
594
+ }
595
+ if (!schema.type) {
596
+ return { ...schema, type: "object", properties: schema.properties || {} };
597
+ }
598
+ return schema;
599
+ }
600
+
601
+ const META_SCHEMA_KEYWORDS = new Set([
602
+ "$schema",
603
+ "$id",
604
+ "$anchor",
605
+ "$dynamicAnchor",
606
+ "$vocabulary",
607
+ "$comment",
608
+ "$defs",
609
+ "definitions",
610
+ ]);
611
+
612
+ /** Remove schema metadata without treating user-defined property names as keywords. */
613
+ function stripMetaSchemaMap(schemaMap: unknown): unknown {
614
+ if (!isRecord(schemaMap)) return stripMetaSchema(schemaMap);
615
+ return Object.fromEntries(
616
+ Object.entries(schemaMap).map(([key, value]) => [key, stripMetaSchema(value)]),
617
+ );
618
+ }
619
+
620
+ /** Remove metadata that Cloud Code Assist rejects from JSON Schema keyword positions. */
621
+ function stripMetaSchema(schema: unknown): unknown {
622
+ if (!schema || typeof schema !== "object") return schema;
623
+ if (Array.isArray(schema)) return schema.map(stripMetaSchema);
624
+
625
+ const out: Record<string, unknown> = {};
626
+ for (const [key, value] of Object.entries(schema)) {
627
+ if (META_SCHEMA_KEYWORDS.has(key)) continue;
628
+ if (SCHEMA_MAP_KEYWORDS.has(key)) {
629
+ out[key] = stripMetaSchemaMap(value);
630
+ } else if (SCHEMA_VALUE_KEYWORDS.has(key) || SCHEMA_ARRAY_KEYWORDS.has(key)) {
631
+ out[key] = stripMetaSchema(value);
632
+ } else {
633
+ out[key] = value;
634
+ }
635
+ }
636
+ return out;
637
+ }
638
+
639
+ /**
640
+ * Protobuf `Schema` fields accepted by Cloud Code Assist's Claude/GPT custom-tool
641
+ * bridge (`parameters` field). Anything else — `nullable`, `anyOf`, `format`,
642
+ * `$ref`, etc. — returns `Unknown name "..."` / Invalid JSON payload (400).
643
+ * Allowlist rather than denylist so new JSON Schema keywords cannot 400 the request.
644
+ * Pi still validates tool args after the model calls them.
645
+ */
646
+ const CUSTOM_TOOL_SCHEMA_ALLOW = new Set([
647
+ "type",
648
+ "description",
649
+ "properties",
650
+ "required",
651
+ "items",
652
+ "enum",
653
+ ]);
654
+
655
+ function normalizeCustomToolType(value: unknown): unknown {
656
+ if (typeof value === "string") return value;
657
+ if (!Array.isArray(value)) return undefined;
658
+ // JSON Schema union types like ["string","null"] → first non-null scalar type.
659
+ const entries = value as unknown[];
660
+ const scalar = entries.find(
661
+ (entry): entry is string => typeof entry === "string" && entry !== "null",
662
+ );
663
+ return scalar;
664
+ }
665
+
666
+ function normalizeCustomToolSchema(schema: unknown): unknown {
667
+ if (!schema || typeof schema !== "object") return schema;
668
+ if (Array.isArray(schema)) return schema.map(normalizeCustomToolSchema);
669
+
670
+ const out: Record<string, unknown> = {};
671
+ for (const [key, value] of Object.entries(schema)) {
672
+ if (!CUSTOM_TOOL_SCHEMA_ALLOW.has(key)) continue;
673
+ if (key === "type") {
674
+ const normalizedType = normalizeCustomToolType(value);
675
+ if (normalizedType !== undefined) out.type = normalizedType;
676
+ continue;
677
+ }
678
+ if (key === "properties" && value && typeof value === "object" && !Array.isArray(value)) {
679
+ // Property names are user-defined, not Schema keywords — never allowlist-filter them.
680
+ const props: Record<string, unknown> = {};
681
+ for (const [propName, propSchema] of Object.entries(value as Record<string, unknown>)) {
682
+ props[propName] = normalizeCustomToolSchema(propSchema);
683
+ }
684
+ out.properties = props;
685
+ continue;
686
+ }
687
+ if (
688
+ key === "enum" &&
689
+ Array.isArray(value) &&
690
+ !value.every((entry) => typeof entry === "string")
691
+ ) {
692
+ continue;
693
+ }
694
+ out[key] = normalizeCustomToolSchema(value);
695
+ }
696
+ return out;
697
+ }
698
+
699
+ /**
700
+ * Gemini accepts JSON Schema through parametersJsonSchema. Claude and GPT-OSS
701
+ * use Cloud Code Assist's custom-tool bridge, which requires a compatible
702
+ * Draft 2020-12 subset in the legacy parameters field.
703
+ */
704
+ export function convertTools(
705
+ tools: Tool[] | undefined,
706
+ useLegacyParameters = false,
707
+ ): { functionDeclarations: GeminiFunctionDeclaration[] }[] | undefined {
708
+ if (!tools?.length) return undefined;
709
+
710
+ const warnings: string[] = [];
711
+ const functionDeclarations = tools.flatMap((tool) => {
712
+ const dereferenced = dereferenceSchema(tool.parameters);
713
+ if (dereferenced.issues.length > 0) {
714
+ const detail = dereferenced.issues
715
+ .map((issue) => `${issue.path} (${issue.ref}: ${issue.reason})`)
716
+ .join(", ");
717
+ warnings.push(`Skipped tool '${tool.name}' due to unresolved schema reference: ${detail}`);
718
+ return [];
719
+ }
720
+
721
+ const rootObject = ensureRootObjectSchema(dereferenced.schema);
722
+ const schema = stripMetaSchema(rootObject);
723
+ return [
724
+ {
725
+ name: tool.name,
726
+ description: tool.description,
727
+ ...(useLegacyParameters
728
+ ? { parameters: normalizeCustomToolSchema(schema) }
729
+ : { parametersJsonSchema: schema }),
730
+ },
731
+ ];
732
+ });
733
+
734
+ setLastToolSchemaWarnings(warnings.length ? warnings : undefined);
735
+ if (!functionDeclarations.length) return undefined;
736
+ return [{ functionDeclarations }];
737
+ }
738
+
739
+ function mapToolChoiceMode(
740
+ toolChoice: AntigravityStreamOptions["toolChoice"],
741
+ ): GeminiToolCallingMode {
742
+ if (toolChoice === ToolChoice.None) return GeminiToolCallingMode.None;
743
+ if (toolChoice === ToolChoice.Any || toolChoice === ToolChoice.Required)
744
+ return GeminiToolCallingMode.Any;
745
+ return GeminiToolCallingMode.Auto;
746
+ }
747
+
748
+ /** Exported for unit tests. */
749
+ export function buildRequest(
750
+ model: Model<Api>,
751
+ context: Context,
752
+ projectId: string,
753
+ options: AntigravityStreamOptions,
754
+ runtimeModel: string,
755
+ ): AntigravityGenerateRequest {
756
+ const injectedSkills = context.messages.flatMap((msg) =>
757
+ msg.role === "user" ? skillBlocks(msg.content) : [],
758
+ );
759
+ const systemParts = context.systemPrompt
760
+ ? [{ text: sanitizeText(context.systemPrompt) }]
761
+ : [{ text: ANTIGRAVITY_SYSTEM_INSTRUCTION }, { text: ANTIGRAVITY_NO_PREAMBLE_INSTRUCTION }];
762
+ systemParts.push(...injectedSkills.map((skill) => ({ text: sanitizeText(skill) })));
763
+
764
+ const contents = convertMessages(model, context, runtimeModel);
765
+ const hasUserText = contents.some(
766
+ (turn) =>
767
+ turn.role === GeminiRole.User &&
768
+ turn.parts.some((part) => "text" in part && Boolean(part.text.trim())),
769
+ );
770
+ if (!hasUserText && (injectedSkills.length > 0 || Boolean(context.systemPrompt))) {
771
+ contents.unshift({
772
+ role: GeminiRole.User,
773
+ parts: [{ text: "Apply the active system instructions." }],
774
+ });
775
+ }
776
+
777
+ const request: GeminiRequestBody = {
778
+ contents,
779
+ systemInstruction: {
780
+ role: GeminiRole.User,
781
+ parts: systemParts,
782
+ },
783
+ };
784
+
785
+ const generationConfig: GeminiGenerationConfig = {};
786
+ if (options.temperature !== undefined) generationConfig.temperature = options.temperature;
787
+ const thinking = getThinkingConfig(runtimeModel, options.reasoning ?? "off");
788
+ if (thinking) generationConfig.thinkingConfig = thinking;
789
+ const maxAllowed = getMaxOutputTokens(model.id, runtimeModel);
790
+ if (options.maxTokens !== undefined) {
791
+ generationConfig.maxOutputTokens = Math.min(options.maxTokens, maxAllowed);
792
+ } else {
793
+ generationConfig.maxOutputTokens = Math.min(maxAllowed, model.maxTokens || maxAllowed);
794
+ }
795
+ if (Object.keys(generationConfig).length) request.generationConfig = generationConfig;
796
+
797
+ const isClaude = model.id.startsWith("claude-") || runtimeModel.startsWith("claude-");
798
+ const tools = convertTools(context.tools, isClaude || model.id.startsWith("gpt-oss-"));
799
+ if (tools) {
800
+ request.tools = tools;
801
+ }
802
+ if (options.toolChoice && options.toolChoice !== ToolChoice.Auto) {
803
+ request.toolConfig = {
804
+ functionCallingConfig: {
805
+ mode: mapToolChoiceMode(options.toolChoice),
806
+ },
807
+ };
808
+ }
809
+
810
+ const isNonGemini =
811
+ isClaude ||
812
+ model.id.startsWith("gpt-oss-") ||
813
+ runtimeModel.startsWith("gpt-oss-") ||
814
+ (!model.id.startsWith("gemini-") && !runtimeModel.startsWith("gemini-"));
815
+
816
+ // Pure agy CLI wire alignment:
817
+ // - step in requestId (.../<step>) equals contents.length (total content blocks)
818
+ // - last_step_index is 0-based index of the last content block (contents.length - 1)
819
+ // - request_id is ${trajectoryId}-${requestIndex} (0-based HTTP request sequence counter)
820
+ // In multi-turn agent loops (with tools), every completed assistant response increments the request counter.
821
+ const step = Math.max(1, request.contents.length);
822
+ const lastStepIndex = String(Math.max(0, request.contents.length - 1));
823
+ const requestIndex =
824
+ context.messages?.filter(
825
+ (m) => m.role === "assistant" && m.stopReason !== "error" && m.stopReason !== "aborted",
826
+ ).length ?? 0;
827
+
828
+ const { conversationId, trajectoryId } = resolveSessionTrajectory(context);
829
+
830
+ const envelope = antigravityRequestEnvelope(runtimeModel, {
831
+ isClaude,
832
+ isNonGemini,
833
+ step,
834
+ lastStepIndex,
835
+ requestIndex,
836
+ conversationId,
837
+ trajectoryId,
838
+ });
839
+ request.sessionId = options.sessionId || envelope.sessionId;
840
+ request.labels = envelope.labels;
841
+
842
+ return {
843
+ project: projectId,
844
+ model: runtimeModel,
845
+ request,
846
+ requestType: AntigravityRequestType.Agent,
847
+ userAgent: AntigravityUserAgent.Antigravity,
848
+ requestId: envelope.requestId,
849
+ };
850
+ }
851
+
852
+ /** Exported for unit tests. */
853
+ export function mapStopReason(reason: string | undefined): StopReason {
854
+ if (reason === "STOP") return StopReason.Stop;
855
+ if (reason === "MAX_TOKENS") return StopReason.Length;
856
+ return reason ? StopReason.Error : StopReason.Stop;
857
+ }
858
+
859
+ /** Exported for unit tests. */
860
+ export function friendlyAntigravityError(status: number | undefined, text: string): string {
861
+ const msg = redactSecrets(jsonOrTextError(text)).slice(0, 500);
862
+ if (status === 400) {
863
+ if (/API key not valid|API_KEY_INVALID/i.test(msg)) {
864
+ return "Antigravity login expired or credentials are invalid. Next: run /login antigravity, then retry.";
865
+ }
866
+ if (/Invalid JSON payload|Unknown name/i.test(msg)) {
867
+ return `Antigravity request format was rejected by the backend (${msg}). Next: switch to a simpler model or retry after updating the extension.`;
868
+ }
869
+ if (/Request contains an invalid argument/i.test(msg)) {
870
+ return `Antigravity rejected this request (${msg}). Next: retry once; if it keeps failing, switch models or re-login.`;
871
+ }
872
+ return `Bad request from Antigravity. Next: retry once, then run /login antigravity if it keeps failing. Backend said: ${msg}`;
873
+ }
874
+ if (status === 401) {
875
+ return "Antigravity authentication failed. Next: run /login antigravity, then retry.";
876
+ }
877
+ if (status === 403) {
878
+ if (/permission|forbidden|access/i.test(msg)) {
879
+ return "Antigravity access was denied for this account or project. Next: try another model, re-login, or use an account with access.";
880
+ }
881
+ return `Antigravity denied this request. Next: re-login or try another model. Backend said: ${msg}`;
882
+ }
883
+ if (status === 404) {
884
+ if (/Requested entity was not found/i.test(msg)) {
885
+ return "This model is not available right now. Next: switch to gemini-3.8-flash, gemini-3.7-flash, gemini-3.6-flash, gemini-3.5-flash, gemini-3.1-pro, or another working model.";
886
+ }
887
+ return `Antigravity could not find the requested resource. Next: retry or switch models. Backend said: ${msg}`;
888
+ }
889
+ if (status === 408) return "Antigravity timed out. Next: retry the same request.";
890
+ if (status === 409) {
891
+ return "Antigravity reported a conflict for this request. Next: retry once or start a new chat session.";
892
+ }
893
+ if (status === 429) {
894
+ const wait = msg.match(/Resets? in ([^.\n]+)/i)?.[1]?.trim();
895
+ if (/Individual quota reached/i.test(msg)) {
896
+ return `Quota reached. Please wait ${wait || "for reset"}. Next: switch models or try again after reset.`;
897
+ }
898
+ if (/quota/i.test(msg)) {
899
+ return `Quota reached.${wait ? ` Please wait ${wait}.` : ""} Next: switch models or retry later.`;
900
+ }
901
+ return `Rate limited by Antigravity. Next: wait a bit and retry.${wait ? ` Reset: ${wait}.` : ""}`;
902
+ }
903
+ if (status === 500) {
904
+ return "Antigravity had an internal server error. Next: retry in a moment or switch models.";
905
+ }
906
+ if (status === 502) return "Antigravity returned a bad gateway error. Next: retry in a moment.";
907
+ if (status === 503) {
908
+ if (/No capacity available/i.test(msg)) {
909
+ return "This model has no capacity right now. Next: retry later or switch to another model.";
910
+ }
911
+ return "Antigravity is temporarily unavailable. Next: retry in a moment or switch models.";
912
+ }
913
+ if (status === 504) return "Antigravity timed out upstream. Next: retry in a moment.";
914
+ return msg;
915
+ }
916
+
917
+ function createOutput(model: Model<Api>): AssistantMessage {
918
+ return {
919
+ role: "assistant",
920
+ content: [],
921
+ api: ANTIGRAVITY_API,
922
+ provider: PROVIDER_ID,
923
+ model: model.id,
924
+ usage: {
925
+ input: 0,
926
+ output: 0,
927
+ cacheRead: 0,
928
+ cacheWrite: 0,
929
+ totalTokens: 0,
930
+ cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
931
+ },
932
+ stopReason: "stop",
933
+ timestamp: Date.now(),
934
+ };
935
+ }
936
+
937
+ function asToolCallArguments(args: Record<string, unknown> | undefined): ToolCall["arguments"] {
938
+ return (args ?? {}) as ToolCall["arguments"];
939
+ }
940
+
941
+ /** Default response-header deadline for streaming requests (see fetchWithHeaderDeadline). */
942
+ const STREAM_HEADER_TIMEOUT_DEFAULT_MS = 180_000;
943
+ /** Default mid-body stall deadline: abort when no SSE bytes arrive for this long. */
944
+ const STREAM_STALL_TIMEOUT_DEFAULT_MS = 120_000;
945
+
946
+ function envTimeoutMs(name: string, fallback: number): number {
947
+ const raw = antigravityEnv(name);
948
+ if (!raw || !/^\d+$/.test(raw)) return fallback;
949
+ const value = Number(raw);
950
+ return Number.isSafeInteger(value) ? value : fallback;
951
+ }
952
+
953
+ /**
954
+ * Streaming header deadline in milliseconds, from ANTIGRAVITY_STREAM_HEADER_TIMEOUT_MS
955
+ * (or the legacy NOAGY_ prefix). 0 disables the deadline; invalid values fall back
956
+ * to the default.
957
+ */
958
+ export function streamHeaderTimeoutMs(): number {
959
+ return envTimeoutMs("STREAM_HEADER_TIMEOUT_MS", STREAM_HEADER_TIMEOUT_DEFAULT_MS);
960
+ }
961
+
962
+ /**
963
+ * Mid-body stall deadline in milliseconds, from ANTIGRAVITY_STREAM_STALL_TIMEOUT_MS
964
+ * (legacy NOAGY_ prefix honored). 0 disables. A healthy SSE stream emits bytes
965
+ * continuously while generating, so a silent gap this long means the connection
966
+ * is dead even though headers arrived — abort with a named error rather than
967
+ * hanging until an external process timeout.
968
+ */
969
+ export function streamStallTimeoutMs(): number {
970
+ return envTimeoutMs("STREAM_STALL_TIMEOUT_MS", STREAM_STALL_TIMEOUT_DEFAULT_MS);
971
+ }
972
+
973
+ function stallError(stallMs: number): Error {
974
+ return new Error(`stream stalled: no data for ${stallMs}ms`);
975
+ }
976
+
977
+ /**
978
+ * Guard a response body against mid-stream stalls while retaining caller
979
+ * cancellation until the body finishes or is cancelled. The wrapper reads only
980
+ * when its consumer pulls, preserving backpressure; its timer is unref'd so an
981
+ * armed deadline cannot keep the process alive.
982
+ */
983
+ function guardResponseBody(
984
+ response: Response,
985
+ controller: AbortController,
986
+ stallMs: number,
987
+ cleanup: () => void,
988
+ ): Response {
989
+ if (!response.body) {
990
+ cleanup();
991
+ return response;
992
+ }
993
+ const reader = response.body.getReader();
994
+ let timer: ReturnType<typeof setTimeout> | undefined;
995
+ let finished = false;
996
+ const finish = () => {
997
+ if (finished) return;
998
+ finished = true;
999
+ if (timer) clearTimeout(timer);
1000
+ cleanup();
1001
+ };
1002
+ const reset = () => {
1003
+ if (stallMs <= 0) return;
1004
+ if (timer) clearTimeout(timer);
1005
+ timer = setTimeout(() => controller.abort(stallError(stallMs)), stallMs);
1006
+ timer.unref?.();
1007
+ };
1008
+ let streamController: ReadableStreamDefaultController<Uint8Array> | undefined;
1009
+ const abortBody = () => {
1010
+ streamController?.error(controller.signal.reason);
1011
+ void reader.cancel(controller.signal.reason).catch(() => undefined);
1012
+ finish();
1013
+ };
1014
+ controller.signal.addEventListener("abort", abortBody, { once: true });
1015
+ reset();
1016
+ const guarded = new ReadableStream<Uint8Array>({
1017
+ start(controller) {
1018
+ streamController = controller;
1019
+ },
1020
+ async pull(streamController) {
1021
+ try {
1022
+ const chunk = await reader.read();
1023
+ if (chunk.done) {
1024
+ finish();
1025
+ streamController.close();
1026
+ return;
1027
+ }
1028
+ if (!(chunk.value instanceof Uint8Array)) {
1029
+ throw new Error("Response body yielded an invalid chunk");
1030
+ }
1031
+ reset();
1032
+ streamController.enqueue(chunk.value);
1033
+ } catch (error) {
1034
+ finish();
1035
+ streamController.error(error);
1036
+ }
1037
+ },
1038
+ async cancel(reason) {
1039
+ finish();
1040
+ await reader.cancel(reason);
1041
+ },
1042
+ });
1043
+ return new Response(guarded, response);
1044
+ }
1045
+
1046
+ /**
1047
+ * Fetch with a response-header deadline and a mid-body stall watchdog. A server
1048
+ * can accept a request on a warm keep-alive socket and then never send response
1049
+ * headers (header phase), or send headers and then go silent mid-body (stall
1050
+ * phase); either way the request is aborted with a named error instead of
1051
+ * hanging until an external process timeout. Long healthy responses are never
1052
+ * cut: the header timer disarms once headers arrive, and the stall timer resets
1053
+ * on every chunk, so only genuine silence aborts.
1054
+ *
1055
+ * Exported with an injectable fetch for unit tests.
1056
+ */
1057
+ export async function fetchWithHeaderDeadline(
1058
+ url: string,
1059
+ init: RequestInit,
1060
+ callerSignal: AbortSignal | undefined,
1061
+ timeoutMs: number,
1062
+ stallMs: number = 0,
1063
+ fetchFn: (input: string, init: RequestInit) => Promise<Response> = antigravityFetch,
1064
+ ): Promise<Response> {
1065
+ if (timeoutMs <= 0 && stallMs <= 0) {
1066
+ return fetchFn(url, { ...init, signal: callerSignal ?? init.signal });
1067
+ }
1068
+ const controller = new AbortController();
1069
+ const forward = () => controller.abort(callerSignal?.reason);
1070
+ const cleanup = () => callerSignal?.removeEventListener("abort", forward);
1071
+ callerSignal?.addEventListener("abort", forward, { once: true });
1072
+ if (callerSignal?.aborted) forward();
1073
+ const timer =
1074
+ timeoutMs > 0
1075
+ ? setTimeout(
1076
+ () => controller.abort(new Error(`no response headers within ${timeoutMs}ms`)),
1077
+ timeoutMs,
1078
+ )
1079
+ : undefined;
1080
+ let responseBodyGuarded = false;
1081
+ try {
1082
+ const response = await fetchFn(url, { ...init, signal: controller.signal });
1083
+ responseBodyGuarded = Boolean(response.body);
1084
+ return guardResponseBody(response, controller, stallMs, cleanup);
1085
+ } finally {
1086
+ if (timer) clearTimeout(timer);
1087
+ if (!responseBodyGuarded) cleanup();
1088
+ }
1089
+ }
1090
+
1091
+ /** Exported for unit tests. */
1092
+
1093
+ export async function streamResponse(
1094
+ response: Response,
1095
+ stream: AssistantMessageEventStream,
1096
+ output: AssistantMessage,
1097
+ model?: Model<Api>,
1098
+ ): Promise<boolean> {
1099
+ if (!response.body) throw new Error("No response body");
1100
+ const reader = response.body.getReader();
1101
+ const decoder = new TextDecoder();
1102
+ let buffer = "";
1103
+ // Consumed-prefix offset: the buffer is compacted once per network chunk instead of
1104
+ // re-copying the whole remainder for every SSE line.
1105
+ let scanStart = 0;
1106
+ let started = false;
1107
+ let currentBlock: ActiveBlock | null = null;
1108
+ let hasContent = false;
1109
+ const blocks = output.content;
1110
+ const blockIndex = () => blocks.length - 1;
1111
+
1112
+ const ensureStarted = () => {
1113
+ if (!started) {
1114
+ stream.push({ type: "start", partial: output });
1115
+ started = true;
1116
+ }
1117
+ };
1118
+
1119
+ const finishCurrent = () => {
1120
+ if (!currentBlock) return;
1121
+ if (currentBlock.type === "text") {
1122
+ stream.push({
1123
+ type: "text_end",
1124
+ contentIndex: blockIndex(),
1125
+ content: currentBlock.text,
1126
+ partial: output,
1127
+ });
1128
+ } else {
1129
+ stream.push({
1130
+ type: "thinking_end",
1131
+ contentIndex: blockIndex(),
1132
+ content: currentBlock.thinking,
1133
+ partial: output,
1134
+ });
1135
+ }
1136
+ currentBlock = null;
1137
+ };
1138
+
1139
+ while (true) {
1140
+ const result = await reader.read();
1141
+ if (result.done) break;
1142
+ if (!(result.value instanceof Uint8Array)) continue;
1143
+ buffer += decoder.decode(result.value, { stream: true });
1144
+
1145
+ let newlineIdx: number;
1146
+ while ((newlineIdx = buffer.indexOf("\n", scanStart)) !== -1) {
1147
+ const line = buffer.slice(scanStart, newlineIdx);
1148
+ scanStart = newlineIdx + 1;
1149
+ if (!line.startsWith("data:")) continue;
1150
+ const json = line.slice(5).trim();
1151
+ if (!json || json === "[DONE]") continue;
1152
+
1153
+ let chunk: StreamChunk;
1154
+ try {
1155
+ chunk = JSON.parse(json) as StreamChunk;
1156
+ } catch {
1157
+ continue;
1158
+ }
1159
+
1160
+ if (chunk.error) {
1161
+ throw new Error(chunk.error.message || JSON.stringify(chunk.error));
1162
+ }
1163
+
1164
+ const responseData = chunk.response || chunk;
1165
+ const candidate = responseData.candidates?.[0];
1166
+
1167
+ for (const part of candidate?.content?.parts || []) {
1168
+ if (part.text !== undefined) {
1169
+ hasContent = true;
1170
+ const isThinking = part.thought === true;
1171
+ const type = isThinking ? "thinking" : "text";
1172
+ if (!currentBlock || currentBlock.type !== type) {
1173
+ finishCurrent();
1174
+ currentBlock = isThinking
1175
+ ? { type: "thinking", thinking: "", thinkingSignature: undefined }
1176
+ : { type: "text", text: "" };
1177
+ blocks.push(currentBlock);
1178
+ ensureStarted();
1179
+ stream.push({
1180
+ type: isThinking ? "thinking_start" : "text_start",
1181
+ contentIndex: blockIndex(),
1182
+ partial: output,
1183
+ });
1184
+ }
1185
+ if (isThinking && currentBlock.type === "thinking") {
1186
+ currentBlock.thinking += part.text;
1187
+ if (part.thoughtSignature) currentBlock.thinkingSignature = part.thoughtSignature;
1188
+ stream.push({
1189
+ type: "thinking_delta",
1190
+ contentIndex: blockIndex(),
1191
+ delta: part.text,
1192
+ partial: output,
1193
+ });
1194
+ } else if (!isThinking && currentBlock.type === "text") {
1195
+ currentBlock.text += part.text;
1196
+ if (part.thoughtSignature) currentBlock.textSignature = part.thoughtSignature;
1197
+ stream.push({
1198
+ type: "text_delta",
1199
+ contentIndex: blockIndex(),
1200
+ delta: part.text,
1201
+ partial: output,
1202
+ });
1203
+ }
1204
+ }
1205
+
1206
+ if (part.functionCall) {
1207
+ hasContent = true;
1208
+ finishCurrent();
1209
+ const rawId = part.functionCall.id || "";
1210
+ const toolCall: ToolCall = {
1211
+ type: "toolCall",
1212
+ id: sanitizeToolCallId(rawId, part.functionCall.name),
1213
+ name: part.functionCall.name || "",
1214
+ arguments: asToolCallArguments(part.functionCall.args),
1215
+ ...(part.thoughtSignature ? { thoughtSignature: part.thoughtSignature } : {}),
1216
+ };
1217
+ blocks.push(toolCall);
1218
+ ensureStarted();
1219
+ stream.push({ type: "toolcall_start", contentIndex: blockIndex(), partial: output });
1220
+ stream.push({
1221
+ type: "toolcall_delta",
1222
+ contentIndex: blockIndex(),
1223
+ delta: JSON.stringify(toolCall.arguments),
1224
+ partial: output,
1225
+ });
1226
+ stream.push({
1227
+ type: "toolcall_end",
1228
+ contentIndex: blockIndex(),
1229
+ toolCall,
1230
+ partial: output,
1231
+ });
1232
+ }
1233
+ }
1234
+
1235
+ if (candidate?.finishReason) {
1236
+ output.rawStopReason = candidate.finishReason;
1237
+ output.stopReason = blocks.some((b) => b.type === "toolCall")
1238
+ ? StopReason.ToolUse
1239
+ : mapStopReason(candidate.finishReason);
1240
+ }
1241
+
1242
+ if (responseData.usageMetadata) {
1243
+ const prompt = responseData.usageMetadata.promptTokenCount || 0;
1244
+ const cacheRead = responseData.usageMetadata.cachedContentTokenCount || 0;
1245
+ const thoughts = responseData.usageMetadata.thoughtsTokenCount || 0;
1246
+ output.usage.input = prompt - cacheRead;
1247
+ output.usage.output = (responseData.usageMetadata.candidatesTokenCount || 0) + thoughts;
1248
+ output.usage.reasoning = thoughts;
1249
+ output.usage.cacheRead = cacheRead;
1250
+ output.usage.totalTokens = responseData.usageMetadata.totalTokenCount || 0;
1251
+ if (model?.cost) {
1252
+ calculateCost(model, output.usage);
1253
+ } else {
1254
+ output.usage.cost = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 };
1255
+ }
1256
+ }
1257
+ }
1258
+
1259
+ if (scanStart > 0) {
1260
+ buffer = buffer.slice(scanStart);
1261
+ scanStart = 0;
1262
+ }
1263
+ }
1264
+
1265
+ finishCurrent();
1266
+ return hasContent;
1267
+ }
1268
+
1269
+ export function streamAntigravity(
1270
+ model: Model<Api>,
1271
+ context: Context,
1272
+ options?: AntigravityStreamOptions,
1273
+ ): AssistantMessageEventStream {
1274
+ const stream = createAssistantMessageEventStream();
1275
+ const opts = options ?? {};
1276
+
1277
+ void runWithDiagnostics(async () => {
1278
+ const startTime = Date.now();
1279
+ const output = createOutput(model);
1280
+ try {
1281
+ const creds = parseApiKey(opts.apiKey);
1282
+ // Skip loadCodeAssist roundtrip when credentials already carry a projectId.
1283
+ const warmedProject = creds.projectId ? null : await loadCodeAssist(creds.token);
1284
+ const projectId = resolveProjectId({
1285
+ token: creds.token,
1286
+ warmedProject,
1287
+ credentialProjectId: creds.projectId,
1288
+ });
1289
+ setLastProjectId(projectId);
1290
+
1291
+ const effort = opts.reasoning ?? "off";
1292
+ const isKnownModel = model.id in getCurrentAntigravityRouting();
1293
+ const baseRuntimeModel =
1294
+ antigravityEnv("RUNTIME_MODEL")?.trim() || getAntigravityRequestModelId(model.id, effort);
1295
+
1296
+ let initialRuntimeModel = baseRuntimeModel;
1297
+ // Skip pre-flight model discovery for known static models to optimize TTFT latency.
1298
+ // Dynamic lookup is only needed for unmapped custom models.
1299
+ if (!isKnownModel && !antigravityEnv("RUNTIME_MODEL")) {
1300
+ const dynamic = await fetchAvailableRuntimeModel(creds.token, projectId, baseRuntimeModel);
1301
+ if (dynamic?.id && /^(gemini-|claude-|gpt-oss-)/i.test(dynamic.id)) {
1302
+ initialRuntimeModel = dynamic.id;
1303
+ }
1304
+ }
1305
+
1306
+ const runtimeCandidates = [initialRuntimeModel];
1307
+ const fallback = getFallbackRuntimeModel(initialRuntimeModel, effort);
1308
+ if (fallback && fallback !== initialRuntimeModel) {
1309
+ runtimeCandidates.push(fallback);
1310
+ }
1311
+
1312
+ const requestHeaders = antigravityHeaders(creds.token);
1313
+
1314
+ let response: Response | undefined;
1315
+ let lastText = "";
1316
+ let received = false;
1317
+ let runtimeModel = initialRuntimeModel;
1318
+
1319
+ for (let emptyAttempt = 0; emptyAttempt <= 2; emptyAttempt++) {
1320
+ if (opts.signal?.aborted) throw new Error("Request was aborted");
1321
+ if (emptyAttempt > 0) {
1322
+ const delay = 500 * 2 ** (emptyAttempt - 1);
1323
+ await new Promise((res) => setTimeout(res, delay));
1324
+ }
1325
+
1326
+ for (let candIdx = 0; candIdx < runtimeCandidates.length; candIdx++) {
1327
+ runtimeModel = runtimeCandidates[candIdx]!;
1328
+ setLastResolvedRuntimeModel(runtimeModel);
1329
+ const body = JSON.stringify(buildRequest(model, context, projectId, opts, runtimeModel));
1330
+
1331
+ for (const endpoint of endpointCandidates()) {
1332
+ setLastEndpoint(endpoint);
1333
+ response = await fetchWithHeaderDeadline(
1334
+ `${endpoint}/v1internal:streamGenerateContent?alt=sse`,
1335
+ {
1336
+ method: "POST",
1337
+ headers: requestHeaders,
1338
+ body,
1339
+ },
1340
+ opts.signal,
1341
+ streamHeaderTimeoutMs(),
1342
+ streamStallTimeoutMs(),
1343
+ );
1344
+ setLastStatus(response.status);
1345
+ if (response.ok) break;
1346
+ lastText = await response.text();
1347
+ if (response.status === 429 && /Individual quota reached/i.test(lastText)) break;
1348
+ if (![403, 404, 429, 500, 502, 503, 504].includes(response.status)) break;
1349
+ }
1350
+
1351
+ if (response?.ok) break;
1352
+ if (response?.status === 404) {
1353
+ if (candIdx + 1 < runtimeCandidates.length) {
1354
+ continue;
1355
+ }
1356
+ if (isKnownModel && candIdx === runtimeCandidates.length - 1) {
1357
+ const dynamic = await fetchAvailableRuntimeModel(
1358
+ creds.token,
1359
+ projectId,
1360
+ baseRuntimeModel,
1361
+ );
1362
+ if (
1363
+ dynamic?.id &&
1364
+ !runtimeCandidates.includes(dynamic.id) &&
1365
+ /^(gemini-|claude-|gpt-oss-)/i.test(dynamic.id)
1366
+ ) {
1367
+ runtimeCandidates.push(dynamic.id);
1368
+ continue;
1369
+ }
1370
+ }
1371
+ }
1372
+ break;
1373
+ }
1374
+
1375
+ if (!response || !response.ok) {
1376
+ if (antigravityEnv("DEBUG_DUMP") === "1") {
1377
+ try {
1378
+ const body = JSON.stringify(
1379
+ buildRequest(model, context, projectId, opts, runtimeModel),
1380
+ );
1381
+ let parsedBody: unknown = body;
1382
+ try {
1383
+ parsedBody = JSON.parse(body) as unknown;
1384
+ } catch {
1385
+ parsedBody = body;
1386
+ }
1387
+ await (
1388
+ await import("node:fs/promises")
1389
+ ).writeFile(
1390
+ "/tmp/antigravity-last-request.json",
1391
+ JSON.stringify(
1392
+ {
1393
+ status: response?.status,
1394
+ runtimeModel,
1395
+ lastText: lastText.slice(0, 4000),
1396
+ body: parsedBody,
1397
+ },
1398
+ null,
1399
+ 2,
1400
+ ),
1401
+ );
1402
+ } catch {
1403
+ // ignore dump failures
1404
+ }
1405
+ }
1406
+ const friendly = friendlyAntigravityError(response?.status, lastText);
1407
+ if (response?.status === 429 && /Quota reached\./i.test(friendly)) {
1408
+ throw new Error(friendly);
1409
+ }
1410
+ throw new Error(
1411
+ `Antigravity API error (${response?.status ?? "no response"}, ${formatRequestDiagnostics({ projectId, runtimeModel })}): ${friendly}`,
1412
+ );
1413
+ }
1414
+
1415
+ output.content = [];
1416
+ output.usage = {
1417
+ input: 0,
1418
+ output: 0,
1419
+ cacheRead: 0,
1420
+ cacheWrite: 0,
1421
+ totalTokens: 0,
1422
+ cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
1423
+ };
1424
+ output.stopReason = "stop";
1425
+
1426
+ received = await streamResponse(response, stream, output, model);
1427
+ if (received) break;
1428
+ }
1429
+
1430
+ if (!received) throw new Error("Antigravity API returned an empty response");
1431
+ setLastLatencyMs(Date.now() - startTime);
1432
+ if (output.stopReason === "error" || output.stopReason === "aborted") {
1433
+ const errorDetail = output.rawStopReason
1434
+ ? `Provider stopped with: ${output.rawStopReason}`
1435
+ : "An unknown error occurred";
1436
+ output.errorMessage = output.errorMessage || errorDetail;
1437
+ setLastError(output.errorMessage);
1438
+ stream.push({ type: "error", reason: output.stopReason, error: output });
1439
+ } else if (output.stopReason === "pending") {
1440
+ throw new Error("Antigravity API returned no stop reason");
1441
+ } else {
1442
+ stream.push({ type: "done", reason: output.stopReason, message: output });
1443
+ }
1444
+ stream.end();
1445
+ } catch (error) {
1446
+ setLastLatencyMs(Date.now() - startTime);
1447
+ output.stopReason = opts.signal?.aborted ? "aborted" : "error";
1448
+ output.errorMessage = safeError(error);
1449
+ setLastError(output.errorMessage);
1450
+ // Ensure endpoint is recorded even if failure happened before setLastEndpoint.
1451
+ if (!getCurrentEndpoint() && endpointCandidates()[0]) {
1452
+ setLastEndpoint(endpointCandidates()[0]);
1453
+ }
1454
+ stream.push({ type: "error", reason: output.stopReason, error: output });
1455
+ stream.end();
1456
+ }
1457
+ });
1458
+
1459
+ return stream;
1460
+ }