@iislee/opencodex 2.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (476) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +225 -0
  3. package/assets/architecture.png +0 -0
  4. package/assets/banner.png +0 -0
  5. package/assets/claude-code-models.gif +0 -0
  6. package/assets/codex-app-picker.png +0 -0
  7. package/bin/ocx.mjs +451 -0
  8. package/bin/package-main.mjs +9 -0
  9. package/gui/dist/assets/index-DTpMHS4F.js +67 -0
  10. package/gui/dist/assets/index-ZNVDE3C7.css +1 -0
  11. package/gui/dist/favicon.png +0 -0
  12. package/gui/dist/icons.svg +24 -0
  13. package/gui/dist/index.html +25 -0
  14. package/gui/dist/logo.png +0 -0
  15. package/gui/dist/provider-icons/alibaba-color.svg +1 -0
  16. package/gui/dist/provider-icons/antigravity-color.svg +1 -0
  17. package/gui/dist/provider-icons/antigravity.svg +1 -0
  18. package/gui/dist/provider-icons/claude-color.svg +1 -0
  19. package/gui/dist/provider-icons/claude.svg +1 -0
  20. package/gui/dist/provider-icons/cloudflare-ai-gateway-color.svg +1 -0
  21. package/gui/dist/provider-icons/copilot-color.svg +1 -0
  22. package/gui/dist/provider-icons/copilot.svg +1 -0
  23. package/gui/dist/provider-icons/cursor-color.svg +2 -0
  24. package/gui/dist/provider-icons/cursor.svg +2 -0
  25. package/gui/dist/provider-icons/deepseek-color.svg +1 -0
  26. package/gui/dist/provider-icons/discord.svg +1 -0
  27. package/gui/dist/provider-icons/firepass-color.svg +1 -0
  28. package/gui/dist/provider-icons/fireworks-color.svg +1 -0
  29. package/gui/dist/provider-icons/gemini-color.svg +1 -0
  30. package/gui/dist/provider-icons/gemini.svg +1 -0
  31. package/gui/dist/provider-icons/github-copilot-color.svg +1 -0
  32. package/gui/dist/provider-icons/gitlab-duo-color.svg +1 -0
  33. package/gui/dist/provider-icons/grok-color.svg +1 -0
  34. package/gui/dist/provider-icons/grok.svg +1 -0
  35. package/gui/dist/provider-icons/groq-color.svg +1 -0
  36. package/gui/dist/provider-icons/huggingface-color.svg +1 -0
  37. package/gui/dist/provider-icons/kimi-color.svg +1 -0
  38. package/gui/dist/provider-icons/kiro-color.svg +15 -0
  39. package/gui/dist/provider-icons/kiro.svg +14 -0
  40. package/gui/dist/provider-icons/lm-studio-color.svg +1 -0
  41. package/gui/dist/provider-icons/mistral-color.svg +1 -0
  42. package/gui/dist/provider-icons/moonshot-color.svg +1 -0
  43. package/gui/dist/provider-icons/nvidia-color.svg +1 -0
  44. package/gui/dist/provider-icons/ollama-color.svg +1 -0
  45. package/gui/dist/provider-icons/openai.svg +1 -0
  46. package/gui/dist/provider-icons/opencode.svg +1 -0
  47. package/gui/dist/provider-icons/openrouter-color.svg +1 -0
  48. package/gui/dist/provider-icons/pi.svg +21 -0
  49. package/gui/dist/provider-icons/qianfan-color.svg +1 -0
  50. package/gui/dist/provider-icons/qwen-portal-color.svg +1 -0
  51. package/gui/dist/provider-icons/telegram.svg +1 -0
  52. package/gui/dist/provider-icons/vercel-ai-gateway-color.svg +1 -0
  53. package/gui/dist/provider-icons/vllm-color.svg +1 -0
  54. package/gui/dist/provider-icons/xiaomi-color.svg +1 -0
  55. package/package.json +102 -0
  56. package/src/AGENTS.md +28 -0
  57. package/src/adapters/anthropic-image-guard.ts +251 -0
  58. package/src/adapters/anthropic-image-normalize.ts +518 -0
  59. package/src/adapters/anthropic.ts +1003 -0
  60. package/src/adapters/azure.ts +36 -0
  61. package/src/adapters/base.ts +72 -0
  62. package/src/adapters/client-fingerprint.ts +59 -0
  63. package/src/adapters/cursor/arg-codec.ts +38 -0
  64. package/src/adapters/cursor/arg-normalize.ts +104 -0
  65. package/src/adapters/cursor/cursor-errors.ts +165 -0
  66. package/src/adapters/cursor/discovery.ts +276 -0
  67. package/src/adapters/cursor/effort-map.ts +127 -0
  68. package/src/adapters/cursor/exec-policy.ts +88 -0
  69. package/src/adapters/cursor/framing.ts +211 -0
  70. package/src/adapters/cursor/gen/agent_pb.ts +15274 -0
  71. package/src/adapters/cursor/kv-store.ts +52 -0
  72. package/src/adapters/cursor/live-models.ts +153 -0
  73. package/src/adapters/cursor/live-smoke-gate.ts +41 -0
  74. package/src/adapters/cursor/live-transport.ts +1214 -0
  75. package/src/adapters/cursor/mcp-config.ts +42 -0
  76. package/src/adapters/cursor/mcp-manager.ts +333 -0
  77. package/src/adapters/cursor/message-mapper.ts +49 -0
  78. package/src/adapters/cursor/native-exec-common.ts +55 -0
  79. package/src/adapters/cursor/native-exec-desktop.ts +184 -0
  80. package/src/adapters/cursor/native-exec-fs.ts +329 -0
  81. package/src/adapters/cursor/native-exec-mcp.ts +153 -0
  82. package/src/adapters/cursor/native-exec-network.ts +43 -0
  83. package/src/adapters/cursor/native-exec-shell.ts +548 -0
  84. package/src/adapters/cursor/native-exec-tools.ts +118 -0
  85. package/src/adapters/cursor/native-exec.ts +576 -0
  86. package/src/adapters/cursor/protobuf-events.ts +563 -0
  87. package/src/adapters/cursor/protobuf-request.ts +714 -0
  88. package/src/adapters/cursor/request-builder.ts +255 -0
  89. package/src/adapters/cursor/thread-continuity.ts +67 -0
  90. package/src/adapters/cursor/tool-definitions.ts +505 -0
  91. package/src/adapters/cursor/transport-retry.ts +132 -0
  92. package/src/adapters/cursor/transport.ts +57 -0
  93. package/src/adapters/cursor/types.ts +52 -0
  94. package/src/adapters/cursor.ts +196 -0
  95. package/src/adapters/google-antigravity-replay.ts +303 -0
  96. package/src/adapters/google-antigravity-wire.ts +108 -0
  97. package/src/adapters/google-errors.ts +85 -0
  98. package/src/adapters/google-http.ts +100 -0
  99. package/src/adapters/google-tool-schema.ts +173 -0
  100. package/src/adapters/google-truncation.ts +13 -0
  101. package/src/adapters/google-wire-compiler.ts +232 -0
  102. package/src/adapters/google.ts +758 -0
  103. package/src/adapters/identity.ts +44 -0
  104. package/src/adapters/image.ts +23 -0
  105. package/src/adapters/kiro-constants.ts +16 -0
  106. package/src/adapters/kiro-errors.ts +197 -0
  107. package/src/adapters/kiro-events.ts +179 -0
  108. package/src/adapters/kiro-images.ts +129 -0
  109. package/src/adapters/kiro-retry.ts +312 -0
  110. package/src/adapters/kiro-thinking.ts +96 -0
  111. package/src/adapters/kiro-tool-fallback.ts +36 -0
  112. package/src/adapters/kiro-tools.ts +215 -0
  113. package/src/adapters/kiro-truncation.ts +33 -0
  114. package/src/adapters/kiro-wire.ts +129 -0
  115. package/src/adapters/kiro.ts +1898 -0
  116. package/src/adapters/mimo-free.ts +263 -0
  117. package/src/adapters/openai-chat.ts +1005 -0
  118. package/src/adapters/openai-responses.ts +1137 -0
  119. package/src/adapters/run-turn-queue.ts +114 -0
  120. package/src/adapters/tool-catalog-nudge.ts +71 -0
  121. package/src/adapters/upstream-http-error.ts +48 -0
  122. package/src/bridge.ts +1619 -0
  123. package/src/chat/inbound.ts +295 -0
  124. package/src/chat/outbound.ts +765 -0
  125. package/src/claude/agents-inject.ts +243 -0
  126. package/src/claude/alias.ts +149 -0
  127. package/src/claude/auth-detect.ts +229 -0
  128. package/src/claude/auth-mode-migration.ts +32 -0
  129. package/src/claude/auth-mode.ts +62 -0
  130. package/src/claude/context-windows.ts +189 -0
  131. package/src/claude/desktop-3p-guard.ts +35 -0
  132. package/src/claude/desktop-3p-paths.ts +84 -0
  133. package/src/claude/desktop-3p.ts +381 -0
  134. package/src/claude/desktop-health.ts +26 -0
  135. package/src/claude/desktop-profile.ts +263 -0
  136. package/src/claude/gateway-cache.ts +70 -0
  137. package/src/claude/inbound-debug.ts +163 -0
  138. package/src/claude/inbound.ts +509 -0
  139. package/src/claude/model-info.ts +151 -0
  140. package/src/claude/outbound.ts +872 -0
  141. package/src/cli/access.ts +108 -0
  142. package/src/cli/account-api.ts +268 -0
  143. package/src/cli/account-auth.ts +223 -0
  144. package/src/cli/account-extended.ts +350 -0
  145. package/src/cli/account.ts +275 -0
  146. package/src/cli/agent-driven.ts +70 -0
  147. package/src/cli/agent.ts +184 -0
  148. package/src/cli/catalog-prewarm.ts +27 -0
  149. package/src/cli/claude-desktop.ts +188 -0
  150. package/src/cli/claude.ts +286 -0
  151. package/src/cli/codex-shim-autorestore.ts +45 -0
  152. package/src/cli/combo.ts +119 -0
  153. package/src/cli/config-command.ts +145 -0
  154. package/src/cli/debug.ts +228 -0
  155. package/src/cli/doctor.ts +930 -0
  156. package/src/cli/export-command.ts +187 -0
  157. package/src/cli/help.ts +354 -0
  158. package/src/cli/index.ts +1113 -0
  159. package/src/cli/init.ts +224 -0
  160. package/src/cli/integrations.ts +142 -0
  161. package/src/cli/interactive-confirm.ts +133 -0
  162. package/src/cli/internal-dispatch.ts +20 -0
  163. package/src/cli/models-runtime.ts +212 -0
  164. package/src/cli/models.ts +336 -0
  165. package/src/cli/observe.ts +117 -0
  166. package/src/cli/opencode.ts +586 -0
  167. package/src/cli/pi.ts +188 -0
  168. package/src/cli/provider-runtime.ts +162 -0
  169. package/src/cli/provider.ts +463 -0
  170. package/src/cli/runtime-api.ts +325 -0
  171. package/src/cli/star-prompt.ts +155 -0
  172. package/src/cli/status-oauth.ts +78 -0
  173. package/src/cli/status.ts +321 -0
  174. package/src/cli/sync-cloud.ts +283 -0
  175. package/src/cli/system-command.ts +112 -0
  176. package/src/cli/tray-proxy.ts +52 -0
  177. package/src/cli/v2.ts +173 -0
  178. package/src/cli.ts +10 -0
  179. package/src/clients/config-export.ts +377 -0
  180. package/src/clients/effective-status.ts +385 -0
  181. package/src/clients/probes/agy.ts +55 -0
  182. package/src/clients/probes/cc-switch.ts +110 -0
  183. package/src/clients/probes/claude.ts +90 -0
  184. package/src/clients/probes/codex.ts +125 -0
  185. package/src/clients/probes/grok.ts +29 -0
  186. package/src/clients/probes/opencode.ts +109 -0
  187. package/src/clients/probes/paseo.ts +55 -0
  188. package/src/clients/probes/pi.ts +55 -0
  189. package/src/cloud/onedrive-auth.ts +666 -0
  190. package/src/cloud/onedrive-graph.ts +108 -0
  191. package/src/cloud/settings.ts +75 -0
  192. package/src/cloud/sync.ts +212 -0
  193. package/src/cloud/types.ts +56 -0
  194. package/src/cloud/vault.ts +89 -0
  195. package/src/codex/account-id.ts +34 -0
  196. package/src/codex/account-label.ts +34 -0
  197. package/src/codex/account-lifecycle.ts +55 -0
  198. package/src/codex/account-namespace-match.ts +63 -0
  199. package/src/codex/account-namespaces.ts +149 -0
  200. package/src/codex/account-pause.ts +20 -0
  201. package/src/codex/account-runtime-state.ts +31 -0
  202. package/src/codex/account-store.ts +517 -0
  203. package/src/codex/account-usability.ts +20 -0
  204. package/src/codex/app-server-processes.ts +756 -0
  205. package/src/codex/auth-api.ts +1540 -0
  206. package/src/codex/auth-collision.ts +107 -0
  207. package/src/codex/auth-context.ts +352 -0
  208. package/src/codex/autostart-health.ts +149 -0
  209. package/src/codex/catalog/aggregation.ts +378 -0
  210. package/src/codex/catalog/bundled.ts +251 -0
  211. package/src/codex/catalog/effort.ts +355 -0
  212. package/src/codex/catalog/metadata.ts +180 -0
  213. package/src/codex/catalog/parsing.ts +456 -0
  214. package/src/codex/catalog/provider-fetch.ts +902 -0
  215. package/src/codex/catalog/sync.ts +620 -0
  216. package/src/codex/catalog.ts +12 -0
  217. package/src/codex/data/upstream-models.json +830 -0
  218. package/src/codex/exec-invocation.ts +22 -0
  219. package/src/codex/features.ts +969 -0
  220. package/src/codex/history-migration-guardian.ts +102 -0
  221. package/src/codex/history-provider.ts +776 -0
  222. package/src/codex/home.ts +206 -0
  223. package/src/codex/inject.ts +799 -0
  224. package/src/codex/injected-marker.ts +72 -0
  225. package/src/codex/journal.ts +163 -0
  226. package/src/codex/main-account-cache.ts +32 -0
  227. package/src/codex/main-account.ts +40 -0
  228. package/src/codex/model-cache.ts +227 -0
  229. package/src/codex/paths.ts +65 -0
  230. package/src/codex/plugins-doctor.ts +242 -0
  231. package/src/codex/pool-rotation.ts +225 -0
  232. package/src/codex/project-config-warnings.ts +411 -0
  233. package/src/codex/quota.ts +411 -0
  234. package/src/codex/refresh.ts +53 -0
  235. package/src/codex/routing.ts +1477 -0
  236. package/src/codex/runtime.ts +538 -0
  237. package/src/codex/shim.ts +1189 -0
  238. package/src/codex/subagent-defaults.ts +550 -0
  239. package/src/codex/subagent-model-fallback.ts +469 -0
  240. package/src/codex/sync.ts +130 -0
  241. package/src/codex/warmup.ts +192 -0
  242. package/src/codex/websocket-registry.ts +100 -0
  243. package/src/combos/failover.ts +140 -0
  244. package/src/combos/index.ts +41 -0
  245. package/src/combos/request.ts +62 -0
  246. package/src/combos/resolve.ts +232 -0
  247. package/src/combos/types.ts +326 -0
  248. package/src/config.ts +2356 -0
  249. package/src/generated/jawcode-model-metadata.ts +104 -0
  250. package/src/github/star-state.ts +203 -0
  251. package/src/grok/inject.ts +545 -0
  252. package/src/grok/status.ts +121 -0
  253. package/src/grok/sync.ts +103 -0
  254. package/src/grok/usage-hook/report.mjs +348 -0
  255. package/src/grok/usage-hook.ts +278 -0
  256. package/src/images/artifacts.ts +516 -0
  257. package/src/images/fulfill-video.ts +163 -0
  258. package/src/images/fulfill.ts +149 -0
  259. package/src/images/index.ts +4 -0
  260. package/src/images/loop.ts +829 -0
  261. package/src/images/plan.ts +133 -0
  262. package/src/images/synthetic-tool.ts +133 -0
  263. package/src/images/types.ts +41 -0
  264. package/src/images/xai-client.ts +141 -0
  265. package/src/images/xai-video-client.ts +163 -0
  266. package/src/index.ts +22 -0
  267. package/src/lib/abort.ts +146 -0
  268. package/src/lib/admin-secrets.ts +25 -0
  269. package/src/lib/admission.ts +83 -0
  270. package/src/lib/app-owned-memory-stores.ts +173 -0
  271. package/src/lib/app-owned-memory.ts +265 -0
  272. package/src/lib/bounded-body.ts +202 -0
  273. package/src/lib/bun-binary-validator.d.mts +3 -0
  274. package/src/lib/bun-binary-validator.mjs +18 -0
  275. package/src/lib/bun-runtime.ts +71 -0
  276. package/src/lib/bun-stream-caps.ts +126 -0
  277. package/src/lib/config-ownership.ts +360 -0
  278. package/src/lib/crash-guard.ts +344 -0
  279. package/src/lib/debug-log-buffer.ts +83 -0
  280. package/src/lib/debug-settings.ts +108 -0
  281. package/src/lib/debug.ts +31 -0
  282. package/src/lib/destination-policy.ts +316 -0
  283. package/src/lib/errors.ts +364 -0
  284. package/src/lib/eventstream-decoder.ts +253 -0
  285. package/src/lib/gcp-adc.ts +341 -0
  286. package/src/lib/injection-debug-log.ts +58 -0
  287. package/src/lib/open-url.ts +25 -0
  288. package/src/lib/pinned-http.ts +151 -0
  289. package/src/lib/privacy.ts +20 -0
  290. package/src/lib/process-control.ts +165 -0
  291. package/src/lib/provider-outbound.ts +170 -0
  292. package/src/lib/provider-url.ts +14 -0
  293. package/src/lib/proxy-env.ts +18 -0
  294. package/src/lib/redact.ts +105 -0
  295. package/src/lib/retry-after.ts +55 -0
  296. package/src/lib/service-secrets.ts +25 -0
  297. package/src/lib/shadow-call.ts +30 -0
  298. package/src/lib/sidecar-tracker.ts +52 -0
  299. package/src/lib/sse-decoder.ts +323 -0
  300. package/src/lib/state-store-registrations.ts +109 -0
  301. package/src/lib/state-store-sweeper.ts +184 -0
  302. package/src/lib/test-home-guard.ts +90 -0
  303. package/src/lib/token-estimate.ts +69 -0
  304. package/src/lib/translator-budget.ts +356 -0
  305. package/src/lib/upstream-retry.ts +239 -0
  306. package/src/lib/win-exec.ts +115 -0
  307. package/src/lib/win-paths.ts +68 -0
  308. package/src/lib/windows-elevation.ts +705 -0
  309. package/src/lib/windows-secret-acl.ts +514 -0
  310. package/src/lib/winsw.ts +375 -0
  311. package/src/oauth/anthropic-routing.ts +594 -0
  312. package/src/oauth/anthropic.ts +177 -0
  313. package/src/oauth/callback-server.ts +294 -0
  314. package/src/oauth/chatgpt.ts +150 -0
  315. package/src/oauth/cursor.ts +211 -0
  316. package/src/oauth/github-copilot.ts +428 -0
  317. package/src/oauth/google-antigravity.ts +230 -0
  318. package/src/oauth/health.ts +399 -0
  319. package/src/oauth/index.ts +1174 -0
  320. package/src/oauth/key-providers.ts +108 -0
  321. package/src/oauth/kimi.ts +213 -0
  322. package/src/oauth/kiro-credentials.ts +726 -0
  323. package/src/oauth/kiro.ts +577 -0
  324. package/src/oauth/local-token-detect.ts +121 -0
  325. package/src/oauth/log.ts +48 -0
  326. package/src/oauth/login-cli.ts +163 -0
  327. package/src/oauth/pkce.ts +15 -0
  328. package/src/oauth/store.ts +630 -0
  329. package/src/oauth/token-guardian.ts +303 -0
  330. package/src/oauth/types.ts +62 -0
  331. package/src/oauth/xai.ts +241 -0
  332. package/src/pi/extensions.ts +72 -0
  333. package/src/pi/home.ts +42 -0
  334. package/src/pi/index.ts +40 -0
  335. package/src/pi/models.ts +278 -0
  336. package/src/pi/packages.ts +219 -0
  337. package/src/pi/settings.ts +365 -0
  338. package/src/pi/status.ts +68 -0
  339. package/src/pi/sync.ts +75 -0
  340. package/src/providers/alibaba-region-backup.ts +75 -0
  341. package/src/providers/alibaba-region-migration.ts +156 -0
  342. package/src/providers/alibaba-region-startup.ts +36 -0
  343. package/src/providers/antigravity-models.ts +205 -0
  344. package/src/providers/api-keys.ts +140 -0
  345. package/src/providers/base-url-choices.ts +64 -0
  346. package/src/providers/context-cap.ts +65 -0
  347. package/src/providers/derive.ts +339 -0
  348. package/src/providers/free-directory.ts +184 -0
  349. package/src/providers/github-copilot-transport.ts +56 -0
  350. package/src/providers/key-failover.ts +203 -0
  351. package/src/providers/kiro-models.ts +67 -0
  352. package/src/providers/label.ts +19 -0
  353. package/src/providers/model-discovery.ts +356 -0
  354. package/src/providers/openai-sidecar.ts +175 -0
  355. package/src/providers/openai-tier-startup.ts +27 -0
  356. package/src/providers/openai-tiers.ts +301 -0
  357. package/src/providers/openai-virtual-models.ts +82 -0
  358. package/src/providers/openrouter-routing.ts +102 -0
  359. package/src/providers/provider-id-rewrite.ts +150 -0
  360. package/src/providers/quota.ts +1260 -0
  361. package/src/providers/registry.ts +1600 -0
  362. package/src/providers/slug-codec.ts +67 -0
  363. package/src/providers/xai-transport.ts +141 -0
  364. package/src/reasoning-effort.ts +135 -0
  365. package/src/responses/compaction.ts +117 -0
  366. package/src/responses/parser.ts +656 -0
  367. package/src/responses/reasoning-envelope.ts +52 -0
  368. package/src/responses/schema.ts +159 -0
  369. package/src/responses/spill-store.ts +394 -0
  370. package/src/responses/state.ts +895 -0
  371. package/src/responses/tool-groups.ts +19 -0
  372. package/src/router.ts +425 -0
  373. package/src/server/adapter-resolve.ts +80 -0
  374. package/src/server/auth-cors.ts +530 -0
  375. package/src/server/chat-completions.ts +368 -0
  376. package/src/server/claude-messages.ts +914 -0
  377. package/src/server/effort-policy.ts +172 -0
  378. package/src/server/gui-static.ts +123 -0
  379. package/src/server/image-retry.ts +42 -0
  380. package/src/server/images.ts +476 -0
  381. package/src/server/index.ts +1126 -0
  382. package/src/server/lifecycle.ts +227 -0
  383. package/src/server/live.ts +598 -0
  384. package/src/server/management/agent-settings-routes.ts +1169 -0
  385. package/src/server/management/api-access.ts +141 -0
  386. package/src/server/management/api-key-usage.ts +167 -0
  387. package/src/server/management/body.ts +35 -0
  388. package/src/server/management/clients-routes.ts +63 -0
  389. package/src/server/management/cloud-sync-routes.ts +266 -0
  390. package/src/server/management/combo-routes.ts +220 -0
  391. package/src/server/management/config-routes.ts +422 -0
  392. package/src/server/management/context.ts +31 -0
  393. package/src/server/management/logs-usage-routes.ts +707 -0
  394. package/src/server/management/model-routes.ts +525 -0
  395. package/src/server/management/oauth-account-routes.ts +563 -0
  396. package/src/server/management/provider-routes.ts +556 -0
  397. package/src/server/management/shared.ts +277 -0
  398. package/src/server/management/sidebar-routes.ts +90 -0
  399. package/src/server/management/system-restart.ts +179 -0
  400. package/src/server/management/system-routes.ts +117 -0
  401. package/src/server/management/usage-summary-cache.ts +86 -0
  402. package/src/server/management-api.ts +215 -0
  403. package/src/server/management-auth.ts +267 -0
  404. package/src/server/memory-watchdog.ts +156 -0
  405. package/src/server/port-reclaim.ts +307 -0
  406. package/src/server/ports.ts +116 -0
  407. package/src/server/proxy-liveness.ts +201 -0
  408. package/src/server/relay-eager.ts +313 -0
  409. package/src/server/relay.ts +1049 -0
  410. package/src/server/request-decompress.ts +132 -0
  411. package/src/server/request-log-conversation.ts +168 -0
  412. package/src/server/request-log.ts +1046 -0
  413. package/src/server/responses/collaboration.ts +354 -0
  414. package/src/server/responses/compact.ts +384 -0
  415. package/src/server/responses/core.ts +2758 -0
  416. package/src/server/responses/encrypted-payload.ts +308 -0
  417. package/src/server/responses/fetch-helpers.ts +157 -0
  418. package/src/server/responses/passthrough-error.ts +78 -0
  419. package/src/server/responses/terminal-guard.ts +230 -0
  420. package/src/server/responses/upstream-error.ts +48 -0
  421. package/src/server/responses-image-gen-repair.ts +132 -0
  422. package/src/server/responses-item-id-repair.ts +224 -0
  423. package/src/server/responses.ts +9 -0
  424. package/src/server/search.ts +136 -0
  425. package/src/server/sse-payload-rewrite.ts +175 -0
  426. package/src/server/startup-action-control.ts +308 -0
  427. package/src/server/startup-health-cache.ts +113 -0
  428. package/src/server/system-env.ts +413 -0
  429. package/src/server/windows-tcp-drop.ts +184 -0
  430. package/src/server/windows-tray-control.ts +41 -0
  431. package/src/server/ws-bridge.ts +471 -0
  432. package/src/service.ts +2554 -0
  433. package/src/stall-timeout.ts +20 -0
  434. package/src/storage/cleanup-job.ts +57 -0
  435. package/src/storage/cleanup.ts +3085 -0
  436. package/src/storage/policy-job.ts +457 -0
  437. package/src/storage/policy-scheduler.ts +40 -0
  438. package/src/storage/policy-worker.ts +59 -0
  439. package/src/storage/policy.ts +527 -0
  440. package/src/storage/restore-job.ts +299 -0
  441. package/src/storage/restore-worker.ts +58 -0
  442. package/src/storage/scanner.ts +238 -0
  443. package/src/storage/storage-mutation-coordinator.ts +139 -0
  444. package/src/storage/worker-lifecycle.ts +215 -0
  445. package/src/tray/assets/opencodex-tray-offline.ico +0 -0
  446. package/src/tray/assets/opencodex-tray-online.ico +0 -0
  447. package/src/tray/assets/opencodex-tray-warning.ico +0 -0
  448. package/src/tray/assets/opencodex-tray.png +0 -0
  449. package/src/tray/windows-tray.ps1 +290 -0
  450. package/src/tray/windows.ts +730 -0
  451. package/src/types.ts +1237 -0
  452. package/src/update/badge.ts +72 -0
  453. package/src/update/index.ts +407 -0
  454. package/src/update/job.ts +1520 -0
  455. package/src/update/notify.ts +257 -0
  456. package/src/update/npm-invocation.d.mts +23 -0
  457. package/src/update/npm-invocation.mjs +94 -0
  458. package/src/update/tray-update-plan.d.mts +18 -0
  459. package/src/update/tray-update-plan.mjs +38 -0
  460. package/src/usage/cost.ts +0 -0
  461. package/src/usage/debug.ts +97 -0
  462. package/src/usage/expected-prices.ts +164 -0
  463. package/src/usage/log.ts +658 -0
  464. package/src/usage/summary.ts +585 -0
  465. package/src/usage/totals.ts +14 -0
  466. package/src/vision/anthropic-describe.ts +185 -0
  467. package/src/vision/describe.ts +125 -0
  468. package/src/vision/index.ts +467 -0
  469. package/src/web-search/anthropic-executor.ts +189 -0
  470. package/src/web-search/executor.ts +105 -0
  471. package/src/web-search/format-result.ts +89 -0
  472. package/src/web-search/index.ts +196 -0
  473. package/src/web-search/loop.ts +664 -0
  474. package/src/web-search/parse.ts +220 -0
  475. package/src/web-search/progress-stream.ts +342 -0
  476. package/src/web-search/synthetic-tool.ts +47 -0
@@ -0,0 +1,1898 @@
1
+ import { decodeEventStream } from "../lib/eventstream-decoder";
2
+ import { estimateTokens } from "../lib/token-estimate";
3
+ import { debugProviderDiagnostic } from "../lib/debug";
4
+ import { resolveKiroApiRegion, resolveKiroProfileArn } from "../oauth/kiro";
5
+ import { KIRO_MODEL_CONTEXT_WINDOWS, normalizeKiroModelId } from "../providers/kiro-models";
6
+ import { modelRecordValue } from "../reasoning-effort";
7
+ import { parseKiroEvent } from "./kiro-events";
8
+ import {
9
+ classifyKiroEventError,
10
+ classifyKiroHttpError,
11
+ classifyKiroStreamError,
12
+ safeKiroErrorMessage,
13
+ safeKiroHttpErrorMessage,
14
+ type KiroErrorClassification,
15
+ } from "./kiro-errors";
16
+ import { KiroThinkingParser } from "./kiro-thinking";
17
+ import { isCompleteKiroToolInput, kiroTruncationErrorMessage } from "./kiro-truncation";
18
+ import { createKiroToolNameRegistry, fallbackToolUseId, fingerprint, invocationId, isValidKiroConversationId, mapModelId, normalizeToolId, osTag, stableConversationId } from "./kiro-wire";
19
+ import { namespacedToolName } from "../types";
20
+ import {
21
+ isTranslatorBudgetExceededError,
22
+ type TranslatorBudget,
23
+ } from "../lib/translator-budget";
24
+ import type {
25
+ AdapterEvent,
26
+ OcxAssistantMessage,
27
+ OcxContentPart,
28
+ OcxMessage,
29
+ OcxParsedRequest,
30
+ OcxProviderConfig,
31
+ OcxTextContent,
32
+ OcxToolCall,
33
+ OcxToolResultMessage,
34
+ OcxUsage,
35
+ } from "../types";
36
+ import type { ProviderAdapter } from "./base";
37
+ import type { AdapterFetchContext, AdapterRequest } from "./base";
38
+ import { extractKiroImages, normalizeKiroImages, type KiroImage } from "./kiro-images";
39
+ import { sniffImageDimensions } from "./anthropic-image-guard";
40
+ import { fetchKiroWithRetry, noteKiroTransientThrottle } from "./kiro-retry";
41
+ import { convertKiroToolContext } from "./kiro-tools";
42
+ import { neutralizeIdentity } from "./identity";
43
+ import { buildNonOpenAIToolCatalogNudgeFromNames } from "./tool-catalog-nudge";
44
+ import {
45
+ KIRO_COMPLETION_INSTRUCTIONS,
46
+ KIRO_COMPLETION_RETRY_MESSAGE,
47
+ KIRO_COMPLETION_TOOL_NAME,
48
+ KIRO_CONTINUATION_MESSAGE,
49
+ KIRO_EMPTY_TOOL_RESULT_MESSAGE,
50
+ KIRO_TOOL_RESULT_CARRIER_MESSAGE,
51
+ MAX_KIRO_INJECTED_INSTRUCTION_CHARS,
52
+ type KiroCompletionMode,
53
+ } from "./kiro-constants";
54
+
55
+ const AMZ_TARGET = "AmazonCodeWhispererStreamingService.GenerateAssistantResponse";
56
+ const SDK_VERSION = "1.0.27";
57
+ const NODE_VERSION = "22.21.1";
58
+ const KIRO_IDE_VERSION = "1.0.0";
59
+ const KIRO_FALLBACK_SERIALIZATION_ENVELOPE_BYTES = 64 * 1024;
60
+ type KiroWireClient = "ide" | "cli";
61
+
62
+ function kiroCliPlatform(): "linux" | "macos" | "windows" {
63
+ return process.platform === "win32" ? "windows" : process.platform === "darwin" ? "macos" : "linux";
64
+ }
65
+
66
+ function kiroCliUserAgent(includeAppVersion: boolean): string {
67
+ return [
68
+ "aws-sdk-rust/1.3.15",
69
+ "ua/2.1",
70
+ "api/codewhispererstreaming/0.1.17975",
71
+ `os/${kiroCliPlatform()}`,
72
+ "lang/rust/1.92.0",
73
+ ...(includeAppVersion ? ["md/appVersion-2.14.2"] : []),
74
+ "m/F",
75
+ "app/AmazonQ-For-CLI",
76
+ ].join(" ");
77
+ }
78
+
79
+ // Payload construction (conversationState)
80
+ interface KiroToolUse {
81
+ name: string;
82
+ input: Record<string, unknown>; // OBJECT, not stringified
83
+ toolUseId: string;
84
+ }
85
+ interface KiroToolResult {
86
+ content: Array<{ text: string }>;
87
+ status: string;
88
+ toolUseId: string;
89
+ }
90
+ interface KiroUserInputMessage {
91
+ content: string;
92
+ modelId?: string;
93
+ origin?: string;
94
+ userInputMessageContext?: {
95
+ tools?: unknown[];
96
+ toolResults?: KiroToolResult[];
97
+ };
98
+ images?: KiroImage[];
99
+ }
100
+ interface KiroHistoryEntry {
101
+ userInputMessage?: KiroUserInputMessage;
102
+ assistantResponseMessage?: { content: string; toolUses?: KiroToolUse[] };
103
+ }
104
+
105
+ function kiroToolWireNames(tools: readonly unknown[]): string[] {
106
+ return tools
107
+ .map(tool => {
108
+ const spec = (tool as { toolSpecification?: { name?: unknown } }).toolSpecification;
109
+ return typeof spec?.name === "string" ? spec.name : undefined;
110
+ })
111
+ .filter((name): name is string => typeof name === "string");
112
+ }
113
+
114
+ function userContentText(content: string | OcxContentPart[]): string {
115
+ if (typeof content === "string") return content;
116
+ return content.map(p => (p.type === "text" ? p.text : "")).filter(Boolean).join("\n");
117
+ }
118
+
119
+ function usageContentText(content: string | OcxContentPart[]): string {
120
+ if (typeof content === "string") return content;
121
+ return content
122
+ .map(p => {
123
+ if (p.type === "text") return p.text;
124
+ if (p.type === "image") return `[image:${p.detail ?? "auto"}]`;
125
+ return "";
126
+ })
127
+ .filter(Boolean)
128
+ .join("\n");
129
+ }
130
+ function serializeForUsage(value: unknown): string {
131
+ try { return JSON.stringify(value); } catch { return String(value); }
132
+ }
133
+ function currentTurnUsageMessages(messages: OcxMessage[]): OcxMessage[] {
134
+ return messages.slice(messages.map(m => m.role).lastIndexOf("assistant") + 1).filter(m => m.role !== "assistant");
135
+ }
136
+ function kiroPayloadMessages(parsed: OcxParsedRequest): OcxMessage[] {
137
+ return parsed.context.messages;
138
+ }
139
+
140
+ function messageUsageText(msg: OcxMessage): string {
141
+ switch (msg.role) {
142
+ case "user":
143
+ case "developer":
144
+ return usageContentText(msg.content);
145
+ case "toolResult":
146
+ return [
147
+ msg.toolName,
148
+ msg.toolCallId,
149
+ msg.isError ? "error" : "success",
150
+ usageContentText(msg.content),
151
+ ].filter(Boolean).join("\n");
152
+ case "assistant":
153
+ return "";
154
+ }
155
+ }
156
+
157
+ function messageLogText(msg: OcxMessage): string {
158
+ if (msg.role !== "assistant") return messageUsageText(msg);
159
+ return msg.content.map(part => {
160
+ if (part.type === "text") return part.text;
161
+ if (part.type === "toolCall") return [part.name, part.id, serializeForUsage(part.arguments)].join("\n");
162
+ return part.thinking;
163
+ }).filter(Boolean).join("\n");
164
+ }
165
+
166
+ function estimateKiroImageTokens(image: KiroImage): number {
167
+ const dimensions = sniffImageDimensions(image.source.bytes);
168
+ if (dimensions) {
169
+ return Math.max(256, Math.ceil(dimensions.width * dimensions.height / 750));
170
+ }
171
+ const decodedBytes = Math.floor(image.source.bytes.length * 3 / 4);
172
+ return Math.max(256, Math.ceil(decodedBytes / 512));
173
+ }
174
+
175
+ function estimateKiroTokens(text: string, modelId?: string): number {
176
+ return estimateTokens(text, modelId ? `kiro/${modelId}` : "kiro");
177
+ }
178
+
179
+ function estimateKiroPayloadInputTokens(payload: Record<string, unknown>, modelId: string): number {
180
+ const conversationState = (payload as {
181
+ conversationState?: {
182
+ history?: KiroHistoryEntry[];
183
+ currentMessage?: KiroHistoryEntry;
184
+ };
185
+ }).conversationState;
186
+ if (!conversationState) return 0;
187
+
188
+ const parts: string[] = [];
189
+ let imageTokens = 0;
190
+ const entries = [
191
+ ...(conversationState.history ?? []),
192
+ ...(conversationState.currentMessage ? [conversationState.currentMessage] : []),
193
+ ];
194
+ for (const entry of entries) {
195
+ const user = entry.userInputMessage;
196
+ if (user) {
197
+ if (user.content) parts.push(user.content);
198
+ for (const image of user.images ?? []) imageTokens += estimateKiroImageTokens(image);
199
+ const context = user.userInputMessageContext;
200
+ if (context?.tools?.length) parts.push(serializeForUsage(context.tools));
201
+ if (context?.toolResults?.length) parts.push(serializeForUsage(context.toolResults));
202
+ }
203
+ const assistant = entry.assistantResponseMessage;
204
+ if (assistant) {
205
+ if (assistant.content) parts.push(assistant.content);
206
+ if (assistant.toolUses?.length) parts.push(serializeForUsage(assistant.toolUses));
207
+ }
208
+ }
209
+ return estimateKiroTokens(parts.join("\n"), modelId) + imageTokens;
210
+ }
211
+
212
+ function shouldCountStablePromptOverhead(parsed: OcxParsedRequest): boolean {
213
+ return !parsed.previousResponseId && !parsed.context.messages.some(m => m.role === "assistant");
214
+ }
215
+
216
+ function estimateKiroInputTokens(parsed: OcxParsedRequest): number {
217
+ const parts = currentTurnUsageMessages(parsed.context.messages)
218
+ .map(messageUsageText)
219
+ .filter(Boolean);
220
+
221
+ if (shouldCountStablePromptOverhead(parsed)) {
222
+ if (parsed.context.systemPrompt?.length) parts.push(...parsed.context.systemPrompt);
223
+ if (parsed.context.tools?.length) parts.push(serializeForUsage(parsed.context.tools));
224
+ }
225
+
226
+ return estimateKiroTokens(parts.join("\n"), parsed.modelId);
227
+ }
228
+
229
+ function estimateKiroLogInputTokens(parsed: OcxParsedRequest): number {
230
+ const parts = parsed.context.messages.map(messageLogText).filter(Boolean);
231
+ if (parsed.context.systemPrompt?.length) parts.push(...parsed.context.systemPrompt);
232
+ if (parsed.context.tools?.length) parts.push(serializeForUsage(parsed.context.tools));
233
+ return Math.max(estimateKiroInputTokens(parsed), estimateKiroTokens(parts.join("\n"), parsed.modelId));
234
+ }
235
+
236
+ function kiroUpstreamContextWindow(modelId: string | undefined): number | undefined {
237
+ if (!modelId) return undefined;
238
+ const normalizedModelId = normalizeKiroModelId(modelId);
239
+ if (normalizedModelId === "auto") return undefined;
240
+ const window = modelRecordValue(KIRO_MODEL_CONTEXT_WINDOWS, modelId)
241
+ ?? modelRecordValue(KIRO_MODEL_CONTEXT_WINDOWS, normalizedModelId);
242
+ return typeof window === "number" && Number.isFinite(window) && window > 0 ? window : undefined;
243
+ }
244
+
245
+ function kiroRuntimeEndpoint(provider: OcxProviderConfig, region: string): string {
246
+ const configured = new URL(provider.baseUrl);
247
+ if (
248
+ /^runtime\.[a-z]{2}(?:-[a-z]+)+-\d\.kiro\.dev$/i.test(configured.hostname)
249
+ && configured.pathname === "/"
250
+ ) {
251
+ return `https://runtime.${region}.kiro.dev/`;
252
+ }
253
+ return configured.toString();
254
+ }
255
+
256
+ export type KiroReasoningMode = "native" | "emulated";
257
+
258
+ // Kiro takes a verified native effort field for these models, and each model family names it
259
+ // differently: the Sol-only `reasoning.effort` versus the Claude-specific `output_config.effort`.
260
+ // Models absent from this table fall back to emulated thinking instructions.
261
+ const KIRO_NATIVE_EFFORT_FIELDS: Record<string, "reasoning" | "output_config"> = {
262
+ "gpt-5.6-sol": "reasoning",
263
+ "claude-opus-5": "output_config",
264
+ };
265
+
266
+ const KIRO_NATIVE_EFFORTS = ["low", "medium", "high", "xhigh", "max"];
267
+
268
+ function kiroNativeEffortField(modelId: string): "reasoning" | "output_config" | undefined {
269
+ return KIRO_NATIVE_EFFORT_FIELDS[normalizeKiroModelId(modelId)];
270
+ }
271
+
272
+ export function kiroReasoningMode(modelId: string): KiroReasoningMode {
273
+ return kiroNativeEffortField(modelId) ? "native" : "emulated";
274
+ }
275
+
276
+ function kiroThinkingBudget(parsed: OcxParsedRequest): number | undefined {
277
+ const effort = parsed.options.reasoning;
278
+ if (!effort || effort === "none") return undefined;
279
+ const maxTokens = parsed.options.maxOutputTokens || 4096;
280
+ const percent: Record<string, number> = {
281
+ minimal: 0.10,
282
+ low: 0.20,
283
+ medium: 0.50,
284
+ high: 0.80,
285
+ xhigh: 0.90,
286
+ max: 0.95,
287
+ };
288
+ const ratio = percent[effort];
289
+ return ratio === undefined ? undefined : Math.max(1, Math.floor(maxTokens * ratio));
290
+ }
291
+
292
+ function injectKiroThinkingTags(content: string, parsed: OcxParsedRequest): string {
293
+ if (kiroReasoningMode(parsed.modelId) !== "emulated") return content;
294
+ const budget = kiroThinkingBudget(parsed);
295
+ if (!budget) return content;
296
+ const instruction = [
297
+ "Think in English for better reasoning quality.",
298
+ "Be thorough and systematic, consider edge cases, challenge assumptions, and verify reasoning before answering.",
299
+ "After thinking, respond in the user's language.",
300
+ ].join("\n");
301
+ return [
302
+ "<thinking_mode>enabled</thinking_mode>",
303
+ `<max_thinking_length>${budget}</max_thinking_length>`,
304
+ `<thinking_instruction>${instruction}</thinking_instruction>`,
305
+ "",
306
+ content,
307
+ ].join("\n");
308
+ }
309
+
310
+ function validateKiroCapabilities(parsed: OcxParsedRequest): void {
311
+ const choice = parsed.options.toolChoice;
312
+ if (choice !== undefined && choice !== "auto" && choice !== "none") {
313
+ throw new Error("Kiro supports only automatic tool choice or tool_choice:none");
314
+ }
315
+ if (parsed.options.parallelToolCalls === true) {
316
+ throw new Error("Kiro does not support parallel tool calls");
317
+ }
318
+ if (parsed.options.serviceTier !== undefined) {
319
+ throw new Error("Kiro does not support service tiers");
320
+ }
321
+ const raw = parsed._rawBody as Record<string, unknown> | undefined;
322
+ if (parsed._structuredOutput || raw?.text !== undefined) {
323
+ throw new Error("Kiro does not support Responses text controls or structured output");
324
+ }
325
+ }
326
+
327
+ type KiroTurn =
328
+ | { kind: "user"; content: string; images: KiroImage[]; toolResults: KiroToolResult[] }
329
+ | { kind: "assistant"; content: string; toolUses: KiroToolUse[] };
330
+
331
+ function appendTurnText(target: string, next: string): string {
332
+ if (!next) return target;
333
+ return target ? `${target}\n\n${next}` : next;
334
+ }
335
+
336
+ function validateKiroConversationState(history: KiroHistoryEntry[], currentMessage: KiroHistoryEntry): void {
337
+ const entries = [...history, currentMessage];
338
+ const pendingToolUses = new Set<string>();
339
+ let previousRole: "user" | "assistant" | undefined;
340
+
341
+ for (const entry of entries) {
342
+ const user = entry.userInputMessage;
343
+ const assistant = entry.assistantResponseMessage;
344
+ if (Boolean(user) === Boolean(assistant)) {
345
+ throw new Error("Kiro conversation entries must contain exactly one message role");
346
+ }
347
+ const role = user ? "user" : "assistant";
348
+ if (role === previousRole) throw new Error("Kiro conversation roles must alternate");
349
+ previousRole = role;
350
+
351
+ if (user) {
352
+ const hasPayload = Boolean(user.content.trim())
353
+ || Boolean(user.images?.length)
354
+ || Boolean(user.userInputMessageContext?.toolResults?.length);
355
+ if (!hasPayload) throw new Error("Kiro user messages must not be empty");
356
+ for (const result of user.userInputMessageContext?.toolResults ?? []) {
357
+ if (!pendingToolUses.delete(result.toolUseId)) {
358
+ throw new Error(`Kiro tool result has no matching tool use ${JSON.stringify(result.toolUseId)}`);
359
+ }
360
+ if (!result.content.some(part => part.text.trim())) {
361
+ throw new Error(`Kiro tool result must not be empty ${JSON.stringify(result.toolUseId)}`);
362
+ }
363
+ }
364
+ continue;
365
+ }
366
+
367
+ const toolUses = assistant?.toolUses ?? [];
368
+ if (!assistant?.content.trim() && toolUses.length === 0) {
369
+ throw new Error("Kiro assistant messages must not be empty");
370
+ }
371
+ for (const toolUse of toolUses) {
372
+ if (pendingToolUses.has(toolUse.toolUseId)) {
373
+ throw new Error(`Kiro conversation contains duplicate tool use ${JSON.stringify(toolUse.toolUseId)}`);
374
+ }
375
+ pendingToolUses.add(toolUse.toolUseId);
376
+ }
377
+ }
378
+ if (pendingToolUses.size > 0) throw new Error("Kiro conversation contains an unanswered tool use");
379
+ }
380
+
381
+ function boundedInjectedInstruction(text: string, used: { value: number }): string | undefined {
382
+ const remaining = MAX_KIRO_INJECTED_INSTRUCTION_CHARS - used.value;
383
+ if (remaining <= 0 || !text) return undefined;
384
+ const result = text.length <= remaining ? text : text.slice(0, remaining);
385
+ used.value += result.length;
386
+ return result;
387
+ }
388
+
389
+ function kiroCompletionTool(): Record<string, unknown> {
390
+ return {
391
+ toolSpecification: {
392
+ name: KIRO_COMPLETION_TOOL_NAME,
393
+ description: "Finish the task and return the complete user-facing final answer. Call only when no more work or tool calls are needed.",
394
+ inputSchema: {
395
+ json: {
396
+ type: "object",
397
+ properties: {
398
+ answer: {
399
+ type: "string",
400
+ description: "The complete final answer to show the user.",
401
+ },
402
+ },
403
+ required: ["answer"],
404
+ },
405
+ },
406
+ },
407
+ };
408
+ }
409
+
410
+ export function buildKiroPayload(
411
+ parsed: OcxParsedRequest,
412
+ profileArn: string | undefined,
413
+ forcedCompletionMode?: KiroCompletionMode,
414
+ wireClient: KiroWireClient = "ide",
415
+ ): {
416
+ payload: Record<string, unknown>;
417
+ nameMap: Map<string, string>;
418
+ conversationId: string;
419
+ completionMode: KiroCompletionMode;
420
+ } {
421
+ validateKiroCapabilities(parsed);
422
+ const modelId = mapModelId(parsed.modelId);
423
+ const registry = createKiroToolNameRegistry();
424
+ const toolContext = convertKiroToolContext(parsed, registry);
425
+ const ordinaryTools = toolContext.tools;
426
+ const completionMode: KiroCompletionMode = forcedCompletionMode
427
+ ?? (ordinaryTools.length > 0 ? "required" : "disabled");
428
+ const kiroTools = completionMode === "disabled"
429
+ ? ordinaryTools
430
+ : [...ordinaryTools, kiroCompletionTool()];
431
+ const nameMap = toolContext.nameMap;
432
+ const systemParts: string[] = [];
433
+ const injectedChars = { value: 0 };
434
+ // Neutralize Codex's GPT-5 identity line so a routed Kiro model never misreports as GPT-5/OpenAI
435
+ // and the proxy identity never leaks upstream.
436
+ if (parsed.context.systemPrompt?.length) systemParts.push(neutralizeIdentity(parsed.context.systemPrompt.join("\n\n")));
437
+ for (const addition of toolContext.systemAdditions) {
438
+ const boundedAddition = boundedInjectedInstruction(addition, injectedChars);
439
+ if (boundedAddition) systemParts.push(boundedAddition);
440
+ }
441
+ const toolCatalogNudge = buildNonOpenAIToolCatalogNudgeFromNames(kiroToolWireNames(kiroTools));
442
+ const boundedNudge = toolCatalogNudge ? boundedInjectedInstruction(toolCatalogNudge, injectedChars) : undefined;
443
+ if (boundedNudge) systemParts.push(boundedNudge);
444
+ if (completionMode !== "disabled") {
445
+ const boundedCompletion = boundedInjectedInstruction(KIRO_COMPLETION_INSTRUCTIONS, injectedChars);
446
+ if (boundedCompletion) systemParts.push(boundedCompletion);
447
+ }
448
+ const systemPrefix = systemParts.length > 0 ? `${systemParts.join("\n\n")}\n\n` : "";
449
+ const turns: KiroTurn[] = [];
450
+ const priorCalls = new Map<string, { wireName: string }>();
451
+ const pushUser = (content: string, images: KiroImage[] = [], toolResults: KiroToolResult[] = []): void => {
452
+ const last = turns.at(-1);
453
+ if (last?.kind === "user") {
454
+ last.content = appendTurnText(last.content, content);
455
+ last.images.push(...images);
456
+ last.toolResults.push(...toolResults);
457
+ } else {
458
+ turns.push({ kind: "user", content, images: [...images], toolResults: [...toolResults] });
459
+ }
460
+ };
461
+ const pushAssistant = (content: string, toolUses: KiroToolUse[]): void => {
462
+ const last = turns.at(-1);
463
+ if (last?.kind === "assistant") {
464
+ last.content = appendTurnText(last.content, content);
465
+ last.toolUses.push(...toolUses);
466
+ } else {
467
+ turns.push({ kind: "assistant", content, toolUses: [...toolUses] });
468
+ }
469
+ };
470
+
471
+ for (const msg of kiroPayloadMessages(parsed)) {
472
+ if (msg.role === "user" || msg.role === "developer") {
473
+ const text = userContentText((msg as { content: string | OcxContentPart[] }).content);
474
+ const images = extractKiroImages((msg as { content: string | OcxContentPart[] }).content);
475
+ pushUser(text, images);
476
+ } else if (msg.role === "assistant") {
477
+ const aMsg = msg as OcxAssistantMessage;
478
+ const text = (aMsg.content || [])
479
+ .filter((b): b is OcxTextContent => b.type === "text")
480
+ .map(b => b.text)
481
+ .join("");
482
+ const toolCalls = (aMsg.content || [])
483
+ .filter((b): b is OcxToolCall => b.type === "toolCall");
484
+ const toolUses: KiroToolUse[] = toolCalls.map(tc => {
485
+ const toolUseId = normalizeToolId(tc.id);
486
+ if (!toolUseId) throw new Error("Kiro history contains a tool call with an empty id");
487
+ if (priorCalls.has(toolUseId)) throw new Error(`Kiro history contains duplicate tool call id ${JSON.stringify(tc.id)}`);
488
+ const wireName = namespacedToolName(tc.namespace, tc.name);
489
+ const name = registry.alias(wireName);
490
+ priorCalls.set(toolUseId, { wireName });
491
+ return { name, input: (tc.arguments ?? {}) as Record<string, unknown>, toolUseId };
492
+ });
493
+ if (!text && toolUses.length === 0) {
494
+ const hasReasoning = aMsg.content.some(part => part.type === "thinking" && part.thinking.trim());
495
+ if (hasReasoning) continue;
496
+ }
497
+ pushAssistant(text, toolUses);
498
+ } else if (msg.role === "toolResult") {
499
+ const tr = msg as OcxToolResultMessage;
500
+ if (tr.containsEncryptedContent) {
501
+ throw new Error(`Kiro cannot translate encrypted output for tool call ${JSON.stringify(tr.toolCallId)}`);
502
+ }
503
+ const text = userContentText(tr.content);
504
+ const resultText = text.trim() ? text : KIRO_EMPTY_TOOL_RESULT_MESSAGE;
505
+ const images = extractKiroImages(tr.content);
506
+ const toolUseId = normalizeToolId(tr.toolCallId);
507
+ if (!priorCalls.has(toolUseId)) {
508
+ throw new Error(`Kiro history contains an orphaned tool result for call ${JSON.stringify(tr.toolCallId)}`);
509
+ }
510
+ // Carrier text is a placeholder for an OTHERWISE EMPTY tool-result turn, not a prefix.
511
+ // Passing it here would push proxy filler AHEAD of a human instruction that Claude Code
512
+ // sends in the same turn (mid-turn steering / queued_command, issue #543), burying the
513
+ // newest user intent behind boilerplate. Backfill below only when nothing else speaks.
514
+ pushUser("", images, [{
515
+ content: [{ text: resultText }],
516
+ status: tr.isError ? "error" : "success",
517
+ toolUseId,
518
+ }]);
519
+ }
520
+ }
521
+
522
+ if (turns.length === 0 || turns[0].kind === "assistant") {
523
+ turns.unshift({ kind: "user", content: KIRO_CONTINUATION_MESSAGE, images: [], toolResults: [] });
524
+ }
525
+ if (turns.at(-1)?.kind === "assistant") {
526
+ turns.push({
527
+ kind: "user",
528
+ content: completionMode === "text_fallback" ? KIRO_COMPLETION_RETRY_MESSAGE : KIRO_CONTINUATION_MESSAGE,
529
+ images: [],
530
+ toolResults: [],
531
+ });
532
+ }
533
+
534
+ // Give tool-result turns a carrier sentence ONLY when they carry no other text. This runs
535
+ // before the pop below so the current turn is covered too: skipping it there would ship an
536
+ // empty current content, which validateKiroConversationState accepts (tool results count as
537
+ // payload) and would therefore fail silently.
538
+ for (const turn of turns) {
539
+ if (turn.kind === "user" && !turn.content.trim() && turn.toolResults.length > 0) {
540
+ turn.content = KIRO_TOOL_RESULT_CARRIER_MESSAGE;
541
+ }
542
+ }
543
+
544
+ const currentTurn = turns.pop();
545
+ if (!currentTurn || currentTurn.kind !== "user") throw new Error("Kiro request must end with a user turn");
546
+ const toEntry = (turn: KiroTurn): KiroHistoryEntry => turn.kind === "assistant"
547
+ ? {
548
+ assistantResponseMessage: {
549
+ content: turn.content,
550
+ ...(turn.toolUses.length > 0 ? { toolUses: turn.toolUses } : {}),
551
+ },
552
+ }
553
+ : {
554
+ userInputMessage: {
555
+ content: turn.content,
556
+ modelId,
557
+ origin: wireClient === "cli" ? "KIRO_CLI" : "AI_EDITOR",
558
+ ...(turn.images.length > 0 ? { images: turn.images } : {}),
559
+ ...(turn.toolResults.length > 0 ? { userInputMessageContext: { toolResults: turn.toolResults } } : {}),
560
+ },
561
+ };
562
+ const history = turns.map(toEntry);
563
+ const currentEntry = toEntry(currentTurn);
564
+ const currentUim = currentEntry.userInputMessage!;
565
+
566
+ if (systemPrefix) {
567
+ const firstUser = history.find(e => e.userInputMessage)?.userInputMessage;
568
+ if (firstUser) firstUser.content = systemPrefix + firstUser.content;
569
+ else currentUim.content = systemPrefix + currentUim.content;
570
+ }
571
+ if (kiroTools.length > 0) {
572
+ currentUim.userInputMessageContext = { ...(currentUim.userInputMessageContext ?? {}), tools: kiroTools };
573
+ }
574
+ if (completionMode === "text_fallback") {
575
+ if (currentUim.content !== KIRO_COMPLETION_RETRY_MESSAGE) {
576
+ currentUim.content = appendTurnText(currentUim.content, KIRO_COMPLETION_RETRY_MESSAGE);
577
+ }
578
+ } else if (!currentUim.userInputMessageContext?.toolResults && currentUim.content !== KIRO_CONTINUATION_MESSAGE) {
579
+ currentUim.content = injectKiroThinkingTags(currentUim.content, parsed);
580
+ }
581
+
582
+ validateKiroConversationState(history, currentEntry);
583
+ const conversationId = stableConversationId(parsed);
584
+ const payload: Record<string, unknown> = {
585
+ conversationState: {
586
+ chatTriggerType: "MANUAL",
587
+ ...(wireClient === "cli" ? {
588
+ agentContinuationId: crypto.randomUUID(),
589
+ agentTaskType: "vibe",
590
+ } : {}),
591
+ conversationId,
592
+ currentMessage: { userInputMessage: currentUim },
593
+ ...(history.length > 0 ? { history } : {}),
594
+ },
595
+ };
596
+ const effort = parsed.options.reasoning;
597
+ const effortField = kiroNativeEffortField(parsed.modelId);
598
+ if (effortField && effort && effort !== "none") {
599
+ if (!KIRO_NATIVE_EFFORTS.includes(effort)) {
600
+ throw new Error(`Kiro ${normalizeKiroModelId(parsed.modelId)} does not support reasoning effort ${JSON.stringify(effort)}`);
601
+ }
602
+ payload.additionalModelRequestFields = { [effortField]: { effort } };
603
+ }
604
+ if (profileArn) payload.profileArn = profileArn;
605
+ return { payload, nameMap, conversationId, completionMode };
606
+ }
607
+
608
+ // Stream parsing (shared by parseStream + parseResponse)
609
+ // CodeWhisperer GenerateAssistantResponse ALWAYS returns an AWS eventstream body (there is no
610
+ // non-streaming mode), so both the streaming bridge and the non-streaming web-search sidecar loop
611
+ // decode the same way — parseResponse just collects what parseStream yields.
612
+ interface KiroAttemptParseResult {
613
+ terminal?: AdapterEvent;
614
+ needsFallback?: boolean;
615
+ usage?: OcxUsage;
616
+ providerState?: { kiro: { conversationId: string } };
617
+ assistantText: string;
618
+ sawReasoning: boolean;
619
+ }
620
+
621
+ interface KiroAttemptResult extends KiroAttemptParseResult {
622
+ releaseRetained(): void;
623
+ }
624
+
625
+ interface KiroAttemptRetention {
626
+ trackReplacement(previousBytes: number, nextBytes: number): void;
627
+ retainEvent(event: AdapterEvent, bytes: number): void;
628
+ releaseEvent(event: AdapterEvent): void;
629
+ releaseAll(): void;
630
+ }
631
+
632
+ function createKiroAttemptRetention(budget: TranslatorBudget): KiroAttemptRetention {
633
+ let retainedBytes = 0;
634
+ const eventBytes = new Map<AdapterEvent, number>();
635
+ return {
636
+ trackReplacement(previousBytes, nextBytes) {
637
+ retainedBytes = Math.max(0, retainedBytes - previousBytes) + nextBytes;
638
+ },
639
+ retainEvent(event, bytes) {
640
+ retainedBytes += bytes;
641
+ eventBytes.set(event, bytes);
642
+ },
643
+ releaseEvent(event) {
644
+ const bytes = eventBytes.get(event);
645
+ if (bytes === undefined) return;
646
+ eventBytes.delete(event);
647
+ retainedBytes = Math.max(0, retainedBytes - bytes);
648
+ budget.releaseRetained(bytes, { kind: "retained_collectors" });
649
+ },
650
+ releaseAll() {
651
+ if (retainedBytes > 0) budget.releaseRetained(retainedBytes, { kind: "retained_collectors" });
652
+ retainedBytes = 0;
653
+ eventBytes.clear();
654
+ },
655
+ };
656
+ }
657
+
658
+ interface KiroFallbackAttempt {
659
+ response: Response;
660
+ inputTokens: number;
661
+ contextInputEstimate: number;
662
+ nameMap: Map<string, string>;
663
+ conversationId: string;
664
+ releaseRequestBody?: () => void;
665
+ }
666
+
667
+ function appendedUtf8Bytes(previous: string, previousBytes: number, fragment: string): number {
668
+ let nextBytes = previousBytes + Buffer.byteLength(fragment);
669
+ const previousLast = previous.charCodeAt(previous.length - 1);
670
+ const fragmentFirst = fragment.charCodeAt(0);
671
+ if (previousLast >= 0xd800 && previousLast <= 0xdbff
672
+ && fragmentFirst >= 0xdc00 && fragmentFirst <= 0xdfff) {
673
+ nextBytes -= 2;
674
+ }
675
+ return nextBytes;
676
+ }
677
+
678
+ /** Exact UTF-8 size JSON.stringify() will use for a string, without materializing that copy. */
679
+ function jsonStringSerializedUtf8Bytes(value: string): number {
680
+ let bytes = 2; // Opening and closing quotes.
681
+ for (let index = 0; index < value.length; index++) {
682
+ const code = value.charCodeAt(index);
683
+ if (code === 0x22 || code === 0x5c) {
684
+ bytes += 2;
685
+ } else if (code === 0x08 || code === 0x09 || code === 0x0a || code === 0x0c || code === 0x0d) {
686
+ bytes += 2;
687
+ } else if (code < 0x20) {
688
+ bytes += 6;
689
+ } else if (code <= 0x7f) {
690
+ bytes += 1;
691
+ } else if (code <= 0x7ff) {
692
+ bytes += 2;
693
+ } else if (code >= 0xd800 && code <= 0xdbff) {
694
+ const next = value.charCodeAt(index + 1);
695
+ if (next >= 0xdc00 && next <= 0xdfff) {
696
+ bytes += 4;
697
+ index++;
698
+ } else {
699
+ bytes += 6;
700
+ }
701
+ } else if (code >= 0xdc00 && code <= 0xdfff) {
702
+ bytes += 6;
703
+ } else {
704
+ bytes += 3;
705
+ }
706
+ }
707
+ return bytes;
708
+ }
709
+
710
+ interface KiroContextWindowState {
711
+ value?: number;
712
+ }
713
+
714
+ type KiroFallbackFactory = (
715
+ conversationId: string | undefined,
716
+ assistantText: string,
717
+ sawReasoning: boolean,
718
+ budget: TranslatorBudget,
719
+ ) => Promise<KiroFallbackAttempt>;
720
+
721
+ function mergeKiroUsage(
722
+ first: OcxUsage | undefined,
723
+ second: OcxUsage | undefined,
724
+ preserveFirstContextGrowth = false,
725
+ ): OcxUsage | undefined {
726
+ if (!first) return second;
727
+ if (!second) return first;
728
+ const sumOptional = (key: keyof OcxUsage): number | undefined => {
729
+ const a = first[key];
730
+ const b = second[key];
731
+ return typeof a === "number" || typeof b === "number"
732
+ ? (typeof a === "number" ? a : 0) + (typeof b === "number" ? b : 0)
733
+ : undefined;
734
+ };
735
+ const totalTokens = typeof first.totalTokens === "number" && typeof second.totalTokens === "number"
736
+ ? first.totalTokens + second.totalTokens
737
+ : undefined;
738
+ const carriedContextTotal = preserveFirstContextGrowth && typeof first.contextTotalTokens === "number"
739
+ ? first.contextTotalTokens + second.outputTokens
740
+ : undefined;
741
+ const combinedOutputTokens = first.outputTokens + second.outputTokens;
742
+ return {
743
+ inputTokens: first.inputTokens + second.inputTokens,
744
+ outputTokens: combinedOutputTokens,
745
+ ...(typeof first.contextTotalTokens === "number" || typeof second.contextTotalTokens === "number"
746
+ ? {
747
+ contextTotalTokens: Math.max(
748
+ first.contextTotalTokens ?? 0,
749
+ second.contextTotalTokens ?? 0,
750
+ carriedContextTotal ?? 0,
751
+ combinedOutputTokens,
752
+ ),
753
+ }
754
+ : {}),
755
+ ...(totalTokens !== undefined ? { totalTokens } : {}),
756
+ ...(sumOptional("cachedInputTokens") !== undefined ? { cachedInputTokens: sumOptional("cachedInputTokens") } : {}),
757
+ ...(sumOptional("cacheReadInputTokens") !== undefined ? { cacheReadInputTokens: sumOptional("cacheReadInputTokens") } : {}),
758
+ ...(sumOptional("cacheCreationInputTokens") !== undefined ? { cacheCreationInputTokens: sumOptional("cacheCreationInputTokens") } : {}),
759
+ ...(sumOptional("reasoningOutputTokens") !== undefined ? { reasoningOutputTokens: sumOptional("reasoningOutputTokens") } : {}),
760
+ ...(first.estimated || second.estimated ? { estimated: true } : {}),
761
+ };
762
+ }
763
+
764
+ function retryableKiroIncomplete(
765
+ reason: string,
766
+ message: string,
767
+ usage: OcxUsage,
768
+ providerState: { kiro: { conversationId: string } } | undefined,
769
+ retryable = true,
770
+ ): AdapterEvent {
771
+ return {
772
+ type: "incomplete",
773
+ reason,
774
+ message,
775
+ usage,
776
+ retryable,
777
+ endTurn: false,
778
+ ...(providerState ? { providerState } : {}),
779
+ };
780
+ }
781
+
782
+ /**
783
+ * Catch-path retryability for #519: only transport/socket failures with no emitted output
784
+ * are replay-safe. Malformed event payloads (`invalid Kiro …`) and any post-output failure
785
+ * stay terminal — same spirit as cursor's emittedOutput gate.
786
+ */
787
+ export function isRetryableKiroStreamCatchError(err: unknown, emittedOutput: boolean): boolean {
788
+ if (emittedOutput) return false;
789
+ const message = err instanceof Error ? err.message : String(err);
790
+ if (/^invalid Kiro\b/i.test(message)) return false;
791
+ // Include Smithy/eventstream truncation (`eventstream: truncated message at end of stream`):
792
+ // partial frame + clean EOF with zero output is the same replay-safe class as a socket close.
793
+ return /socket connection was closed|connection(?: was)? closed unexpectedly|ECONNRESET|EPIPE|UND_ERR_|fetch failed|decoder failed|premature close|other side closed|unexpected EOF|network connection lost|terminated|truncated message at end of stream|eventstream:\s*truncated/i
794
+ .test(message);
795
+ }
796
+
797
+ /** Native clean-stop reason eligible for bounded private-completion validation. */
798
+ const KIRO_END_TURN_STOP_REASON = "END_TURN";
799
+
800
+ async function* parseKiroAttempt(
801
+ response: Response,
802
+ budget: TranslatorBudget,
803
+ mode: KiroCompletionMode,
804
+ modelId: string | undefined,
805
+ inputTokens: number,
806
+ contextWindowState: KiroContextWindowState,
807
+ nameMap: Map<string, string> | undefined,
808
+ conversationId: string | undefined,
809
+ contextInputEstimate?: number,
810
+ /** True when an earlier attempt already flushed visible content to the client (#520). */
811
+ priorEmittedOutput = false,
812
+ ): AsyncGenerator<AdapterEvent, KiroAttemptResult> {
813
+ // `required` mode holds staged commentary until a real tool call or terminal metadata identifies
814
+ // the attempt boundary. Anything the inner parser leaves behind is flushed before the terminal.
815
+ const deferred: AdapterEvent[] = [];
816
+ const retention = createKiroAttemptRetention(budget);
817
+ const attempt = parseKiroAttemptEvents(
818
+ response,
819
+ budget,
820
+ mode,
821
+ modelId,
822
+ inputTokens,
823
+ contextWindowState,
824
+ nameMap,
825
+ conversationId,
826
+ deferred,
827
+ retention,
828
+ contextInputEstimate,
829
+ priorEmittedOutput,
830
+ );
831
+ let handedOff = false;
832
+ try {
833
+ let next = await attempt.next();
834
+ while (!next.done) {
835
+ yield next.value;
836
+ next = await attempt.next();
837
+ }
838
+ for (const event of deferred.splice(0)) {
839
+ try { yield event; } finally { retention.releaseEvent(event); }
840
+ }
841
+ handedOff = true;
842
+ return { ...next.value, releaseRetained: () => retention.releaseAll() };
843
+ } finally {
844
+ if (!handedOff) retention.releaseAll();
845
+ }
846
+ }
847
+
848
+ async function* parseKiroAttemptEvents(
849
+ response: Response,
850
+ budget: TranslatorBudget,
851
+ mode: KiroCompletionMode,
852
+ modelId: string | undefined,
853
+ inputTokens: number,
854
+ contextWindowState: KiroContextWindowState,
855
+ nameMap: Map<string, string> | undefined,
856
+ conversationId: string | undefined,
857
+ deferred: AdapterEvent[],
858
+ retention: KiroAttemptRetention,
859
+ contextInputEstimate?: number,
860
+ priorEmittedOutput = false,
861
+ ): AsyncGenerator<AdapterEvent, KiroAttemptParseResult> {
862
+ const emptyResult = (): KiroAttemptParseResult => ({ assistantText: "", sawReasoning: false });
863
+ if (!response.body) {
864
+ return {
865
+ ...emptyResult(),
866
+ terminal: { type: "error", message: "Kiro response has no body", status: 502, errorType: "upstream_error" },
867
+ };
868
+ }
869
+
870
+ let open: { id: string; name: string; chunks: string[]; completion: boolean } | null = null;
871
+ let outputChars = "";
872
+ let outputCharsBytes = 0;
873
+ let contextUsagePercentage: number | undefined;
874
+ let returnedConversationId = conversationId;
875
+ let assistantText = "";
876
+ let assistantTextBytes = 0;
877
+ let sawText = false;
878
+ let sawReasoning = false;
879
+ let sawRealTool = false;
880
+ let completionAnswer: string | undefined;
881
+ let completionCalls = 0;
882
+ let authoritativeUsage: OcxUsage | undefined;
883
+ let stopReason: string | undefined;
884
+ const fallbackEvents: AdapterEvent[] = [];
885
+ const thinking = new KiroThinkingParser(budget);
886
+
887
+ const retainedEventBytes = (event: AdapterEvent): number => Buffer.byteLength(JSON.stringify(event));
888
+ const retainEvent = (event: AdapterEvent): void => {
889
+ const bytes = retainedEventBytes(event);
890
+ budget.chargeRetained(bytes, { kind: "retained_collectors" });
891
+ retention.retainEvent(event, bytes);
892
+ };
893
+ const emitRetained = async function* (events: Iterable<AdapterEvent>): AsyncGenerator<AdapterEvent> {
894
+ for (const event of events) {
895
+ try { yield event; } finally { retention.releaseEvent(event); }
896
+ }
897
+ };
898
+
899
+ const providerState = (): { kiro: { conversationId: string } } | undefined =>
900
+ returnedConversationId ? { kiro: { conversationId: returnedConversationId } } : undefined;
901
+
902
+ const contextUsageTotalFloor = (): number | undefined => {
903
+ if (contextUsagePercentage === undefined || !contextWindowState.value) return undefined;
904
+ const floor = Math.ceil(contextWindowState.value * Math.min(contextUsagePercentage, 100) / 100);
905
+ return Number.isFinite(floor) && floor > 0 ? floor : undefined;
906
+ };
907
+ const usage = (): OcxUsage => {
908
+ const base = authoritativeUsage ?? {
909
+ inputTokens,
910
+ outputTokens: estimateKiroTokens(outputChars, modelId),
911
+ estimated: true,
912
+ };
913
+ const estimatedContextTotal = contextInputEstimate !== undefined
914
+ ? contextInputEstimate + base.outputTokens
915
+ : undefined;
916
+ const authoritativeTurnTotal = base.inputTokens + base.outputTokens;
917
+ const contextTotal = Math.max(
918
+ estimatedContextTotal ?? 0,
919
+ contextUsageTotalFloor() ?? 0,
920
+ authoritativeTurnTotal,
921
+ );
922
+ return contextTotal > 0 ? { ...base, contextTotalTokens: contextTotal } : base;
923
+ };
924
+
925
+ const classifiedTerminal = (failure: KiroErrorClassification): AdapterEvent => {
926
+ // Upstream exception/error frames can arrive after commentary was already staged (and will be
927
+ // flushed before this terminal is yielded). Replaying after that content would duplicate it.
928
+ const emittedOutput = priorEmittedOutput
929
+ || sawText
930
+ || sawReasoning
931
+ || sawRealTool
932
+ || assistantText.length > 0
933
+ || deferred.length > 0
934
+ || completionAnswer !== undefined
935
+ || completionCalls > 0
936
+ || open !== null
937
+ || fallbackEvents.length > 0;
938
+ if (failure.status === 429 && failure.retryable) noteKiroTransientThrottle();
939
+ return {
940
+ type: "error",
941
+ message: failure.message,
942
+ status: failure.status,
943
+ errorType: failure.errorType,
944
+ code: failure.code,
945
+ retryable: emittedOutput ? false : failure.retryable,
946
+ usage: usage(),
947
+ };
948
+ };
949
+
950
+ const protocolTerminal = (message: string, malformedCompletion = false): AdapterEvent => {
951
+ if (mode === "text_fallback" && malformedCompletion) {
952
+ return retryableKiroIncomplete(
953
+ "malformed_kiro_completion",
954
+ message,
955
+ usage(),
956
+ providerState(),
957
+ // First-attempt progress was already flushed before this bounded fallback (#520).
958
+ !priorEmittedOutput,
959
+ );
960
+ }
961
+ return {
962
+ type: "error",
963
+ message,
964
+ status: 502,
965
+ errorType: "upstream_error",
966
+ code: malformedCompletion ? "invalid_kiro_completion" : "kiro_stream_protocol_error",
967
+ retryable: false,
968
+ usage: usage(),
969
+ };
970
+ };
971
+
972
+ const classifyTool = (
973
+ tool: { id: string; name: string; chunks: string[]; completion: boolean },
974
+ ): AdapterEvent | undefined => {
975
+ if (tool.name !== KIRO_COMPLETION_TOOL_NAME) {
976
+ tool.completion = false;
977
+ return completionAnswer !== undefined || completionCalls > 0
978
+ ? protocolTerminal("Kiro returned a real tool call alongside a private final answer")
979
+ : undefined;
980
+ }
981
+ if (mode === "disabled") {
982
+ return protocolTerminal("Kiro returned the reserved private final-answer tool while explicit completion was disabled");
983
+ }
984
+ tool.completion = true;
985
+ if (completionAnswer !== undefined || completionCalls > 0) {
986
+ return protocolTerminal("Kiro returned more than one private final-answer tool call", true);
987
+ }
988
+ if (sawRealTool) {
989
+ return protocolTerminal("Kiro returned a private final answer alongside a real tool call");
990
+ }
991
+ return undefined;
992
+ };
993
+
994
+ const beginTool = (
995
+ id: string,
996
+ name: string,
997
+ ): { tool?: { id: string; name: string; chunks: string[]; completion: boolean }; terminal?: AdapterEvent } => {
998
+ const next = { id, name, chunks: [], completion: false };
999
+ const terminal = classifyTool(next);
1000
+ return terminal ? { terminal } : { tool: next };
1001
+ };
1002
+
1003
+ // In `required` mode Kiro's stop reason only arrives on the terminal metadata event, so staged
1004
+ // commentary is held until either a real tool call proves the turn continues (flush as
1005
+ // commentary) or the stream ends (relabel as the final answer when END_TURN says so). A heartbeat
1006
+ // stands in for each held event so the bridge's stall watchdog stays armed.
1007
+ const defer = (event: AdapterEvent): AdapterEvent[] => {
1008
+ if (sawRealTool) return [...deferred.splice(0), event];
1009
+ if (event.type !== "text_delta" && deferred.length === 0) return [event];
1010
+ deferred.push(event);
1011
+ retainEvent(event);
1012
+ return [{ type: "heartbeat" }];
1013
+ };
1014
+
1015
+ const stage = (event: AdapterEvent): AdapterEvent[] => {
1016
+ if (event.type === "text_delta") {
1017
+ const nextAssistantTextBytes = appendedUtf8Bytes(assistantText, assistantTextBytes, event.text);
1018
+ const assistantReservation = budget.reserveTransient(nextAssistantTextBytes, { kind: "retained_collectors" });
1019
+ assistantText += event.text;
1020
+ assistantReservation.commitRetained();
1021
+ budget.releaseRetained(assistantTextBytes, { kind: "retained_collectors" });
1022
+ retention.trackReplacement(assistantTextBytes, nextAssistantTextBytes);
1023
+ assistantTextBytes = nextAssistantTextBytes;
1024
+ if (event.text.trim()) sawText = true;
1025
+ const nextOutputCharsBytes = appendedUtf8Bytes(outputChars, outputCharsBytes, event.text);
1026
+ const outputReservation = budget.reserveTransient(nextOutputCharsBytes, { kind: "retained_collectors" });
1027
+ outputChars += event.text;
1028
+ outputReservation.commitRetained();
1029
+ budget.releaseRetained(outputCharsBytes, { kind: "retained_collectors" });
1030
+ retention.trackReplacement(outputCharsBytes, nextOutputCharsBytes);
1031
+ outputCharsBytes = nextOutputCharsBytes;
1032
+ const phased = mode === "disabled"
1033
+ ? event
1034
+ : { ...event, phase: "commentary" as const };
1035
+ if (mode === "text_fallback") {
1036
+ fallbackEvents.push(phased);
1037
+ retainEvent(phased);
1038
+ return [];
1039
+ }
1040
+ return mode === "required" ? defer(phased) : [phased];
1041
+ }
1042
+ if (event.type === "reasoning_raw_delta" || event.type === "thinking_delta") {
1043
+ const text = event.type === "reasoning_raw_delta" ? event.text : event.thinking;
1044
+ if (text.trim()) sawReasoning = true;
1045
+ const nextOutputCharsBytes = appendedUtf8Bytes(outputChars, outputCharsBytes, text);
1046
+ const reasoningReservation = budget.reserveTransient(nextOutputCharsBytes, { kind: "retained_collectors" });
1047
+ outputChars += text;
1048
+ reasoningReservation.commitRetained();
1049
+ budget.releaseRetained(outputCharsBytes, { kind: "retained_collectors" });
1050
+ retention.trackReplacement(outputCharsBytes, nextOutputCharsBytes);
1051
+ outputCharsBytes = nextOutputCharsBytes;
1052
+ }
1053
+ if (mode === "text_fallback" && event.type !== "heartbeat") {
1054
+ fallbackEvents.push(event);
1055
+ retainEvent(event);
1056
+ return [];
1057
+ }
1058
+ return mode === "required" ? defer(event) : [event];
1059
+ };
1060
+
1061
+ const parseCompletion = (chunks: string[]): string | Error => {
1062
+ const raw = chunks.join("").trim();
1063
+ let value: unknown;
1064
+ try {
1065
+ value = JSON.parse(raw || "{}");
1066
+ } catch {
1067
+ return new Error("Kiro returned invalid JSON for the private final-answer tool");
1068
+ }
1069
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
1070
+ return new Error("Kiro returned a non-object value for the private final-answer tool");
1071
+ }
1072
+ const answer = (value as { answer?: unknown }).answer;
1073
+ if (typeof answer !== "string" || !answer.trim()) {
1074
+ return new Error("Kiro returned an empty final answer");
1075
+ }
1076
+ return answer;
1077
+ };
1078
+
1079
+ const flushOpen = (): { events: AdapterEvent[]; terminal?: AdapterEvent } => {
1080
+ if (!open) return { events: [] };
1081
+ const tool = open;
1082
+ open = null;
1083
+ budget.closeCall(tool.id);
1084
+ const input = tool.chunks.join("");
1085
+ if (!isCompleteKiroToolInput(input)) {
1086
+ return { events: [], terminal: protocolTerminal(kiroTruncationErrorMessage("incomplete tool input JSON"), tool.completion) };
1087
+ }
1088
+ if (tool.completion) {
1089
+ completionCalls++;
1090
+ if (completionCalls > 1) {
1091
+ return { events: [], terminal: protocolTerminal("Kiro returned more than one private final-answer tool call", true) };
1092
+ }
1093
+ if (sawRealTool) {
1094
+ return { events: [], terminal: protocolTerminal("Kiro returned a private final answer alongside a real tool call") };
1095
+ }
1096
+ const answer = parseCompletion(tool.chunks);
1097
+ if (answer instanceof Error) return { events: [], terminal: protocolTerminal(answer.message, true) };
1098
+ completionAnswer = answer;
1099
+ return { events: [] };
1100
+ }
1101
+ if (completionAnswer !== undefined || completionCalls > 0) {
1102
+ return { events: [], terminal: protocolTerminal("Kiro returned a real tool call alongside a private final answer") };
1103
+ }
1104
+ sawRealTool = true;
1105
+ const restored = nameMap?.get(tool.name) ?? tool.name;
1106
+ return {
1107
+ events: [
1108
+ { type: "tool_call_start", id: tool.id, name: restored },
1109
+ ...tool.chunks.filter(Boolean).map(argumentsChunk => ({ type: "tool_call_delta", arguments: argumentsChunk }) as AdapterEvent),
1110
+ { type: "tool_call_end" },
1111
+ ],
1112
+ };
1113
+ };
1114
+
1115
+ try {
1116
+ for await (const msg of decodeEventStream(response.body)) {
1117
+ const mt = msg.headers[":message-type"];
1118
+ if (mt === "exception" || mt === "error") {
1119
+ open = null;
1120
+ return {
1121
+ assistantText,
1122
+ sawReasoning,
1123
+ terminal: classifiedTerminal(classifyKiroStreamError(msg.headers, new TextDecoder().decode(msg.payload))),
1124
+ };
1125
+ }
1126
+ if (mt !== "event") {
1127
+ open = null;
1128
+ return {
1129
+ assistantText,
1130
+ sawReasoning,
1131
+ terminal: protocolTerminal(`Kiro response protocol error: unsupported Smithy message type ${JSON.stringify(mt ?? "missing")}`),
1132
+ };
1133
+ }
1134
+ const eventType = msg.headers[":event-type"];
1135
+ if (!eventType) {
1136
+ open = null;
1137
+ return { assistantText, sawReasoning, terminal: protocolTerminal("Kiro response protocol error: event is missing :event-type") };
1138
+ }
1139
+ const ev = parseKiroEvent(eventType, msg.payload);
1140
+ if (!ev) continue;
1141
+ switch (ev.type) {
1142
+ case "metadata":
1143
+ if (ev.usage) authoritativeUsage = ev.usage;
1144
+ if (ev.contextUsagePercentage !== undefined && ev.contextUsagePercentage > 0) {
1145
+ contextUsagePercentage = ev.contextUsagePercentage;
1146
+ }
1147
+ if (ev.stopReason !== undefined) stopReason = ev.stopReason;
1148
+ break;
1149
+ case "message_metadata":
1150
+ if (isValidKiroConversationId(ev.conversationId)) returnedConversationId = ev.conversationId;
1151
+ break;
1152
+ case "content":
1153
+ if (ev.modelId) {
1154
+ contextWindowState.value = kiroUpstreamContextWindow(ev.modelId) ?? contextWindowState.value;
1155
+ }
1156
+ if (open) {
1157
+ open = null;
1158
+ return { assistantText, sawReasoning, terminal: protocolTerminal(kiroTruncationErrorMessage("content arrived before tool stop")) };
1159
+ }
1160
+ if (ev.data) {
1161
+ for (const contentEvent of thinking.feed(ev.data)) {
1162
+ yield* emitRetained(stage(contentEvent));
1163
+ }
1164
+ }
1165
+ break;
1166
+ case "reasoning":
1167
+ for (const contentEvent of thinking.flush()) {
1168
+ yield* emitRetained(stage(contentEvent));
1169
+ }
1170
+ if (ev.data) {
1171
+ yield* emitRetained(stage({ type: "reasoning_raw_delta", text: ev.data }));
1172
+ }
1173
+ break;
1174
+ case "tool": {
1175
+ for (const contentEvent of thinking.flush()) {
1176
+ yield* emitRetained(stage(contentEvent));
1177
+ }
1178
+ if (!open) {
1179
+ if (ev.stop === true) {
1180
+ return { assistantText, sawReasoning, terminal: protocolTerminal("Kiro response protocol error: tool stop received without an open tool call") };
1181
+ }
1182
+ if (!ev.toolUseId || !ev.name) {
1183
+ return { assistantText, sawReasoning, terminal: protocolTerminal("Kiro response protocol error: new tool event is missing toolUseId or name") };
1184
+ }
1185
+ const started = beginTool(ev.toolUseId, ev.name);
1186
+ if (started.terminal) return { assistantText, sawReasoning, terminal: started.terminal };
1187
+ open = started.tool!;
1188
+ budget.openCall(open.id);
1189
+ } else if (
1190
+ (ev.toolUseId && ev.toolUseId !== open.id)
1191
+ || (ev.name && open.name !== "unknown" && ev.name !== open.name)
1192
+ ) {
1193
+ budget.closeCall(open.id);
1194
+ open = null;
1195
+ return { assistantText, sawReasoning, terminal: protocolTerminal(kiroTruncationErrorMessage("tool input changed identity before stop")) };
1196
+ }
1197
+ if (open && open.name === "unknown" && ev.name) {
1198
+ open.name = ev.name;
1199
+ const terminal = classifyTool(open);
1200
+ if (terminal) {
1201
+ open = null;
1202
+ return { assistantText, sawReasoning, terminal };
1203
+ }
1204
+ }
1205
+ if (open && ev.input !== undefined) {
1206
+ const previousCallBytes = open.chunks.reduce((total, chunk) => total + Buffer.byteLength(chunk), 0);
1207
+ const nextCallBytes = previousCallBytes + Buffer.byteLength(ev.input);
1208
+ const callReservation = budget.reserveTransient(nextCallBytes, { kind: "tool_args", callId: open.id });
1209
+ open.chunks.push(ev.input);
1210
+ callReservation.commitRetained();
1211
+ budget.releaseRetained(previousCallBytes, { kind: "tool_args", callId: open.id });
1212
+ const nextOutputCharsBytes = appendedUtf8Bytes(outputChars, outputCharsBytes, ev.input);
1213
+ const toolOutputReservation = budget.reserveTransient(nextOutputCharsBytes, { kind: "retained_collectors" });
1214
+ outputChars += ev.input;
1215
+ toolOutputReservation.commitRetained();
1216
+ budget.releaseRetained(outputCharsBytes, { kind: "retained_collectors" });
1217
+ retention.trackReplacement(outputCharsBytes, nextOutputCharsBytes);
1218
+ outputCharsBytes = nextOutputCharsBytes;
1219
+ }
1220
+ if (ev.stop === true) {
1221
+ const flushed = flushOpen();
1222
+ if (flushed.terminal) return { assistantText, sawReasoning, terminal: flushed.terminal };
1223
+ for (const event of flushed.events) {
1224
+ yield* emitRetained(stage(event));
1225
+ }
1226
+ } else {
1227
+ yield { type: "heartbeat" };
1228
+ }
1229
+ break;
1230
+ }
1231
+ case "invalid_state":
1232
+ open = null;
1233
+ return { assistantText, sawReasoning, terminal: classifiedTerminal(classifyKiroEventError(undefined, ev.message ?? "Kiro entered an invalid state")) };
1234
+ case "error":
1235
+ open = null;
1236
+ return { assistantText, sawReasoning, terminal: classifiedTerminal(classifyKiroEventError(ev.reason, ev.message)) };
1237
+ case "truncation":
1238
+ open = null;
1239
+ return { assistantText, sawReasoning, terminal: protocolTerminal(kiroTruncationErrorMessage(ev.data)) };
1240
+ }
1241
+ }
1242
+
1243
+ for (const contentEvent of thinking.flush()) {
1244
+ yield* emitRetained(stage(contentEvent));
1245
+ }
1246
+ if (open) {
1247
+ const input = open.chunks.join("");
1248
+ if (!isCompleteKiroToolInput(input)) {
1249
+ const privateTool = open.completion;
1250
+ open = null;
1251
+ return {
1252
+ assistantText,
1253
+ sawReasoning,
1254
+ terminal: protocolTerminal(kiroTruncationErrorMessage("stream ended before tool stop"), privateTool),
1255
+ };
1256
+ }
1257
+ const flushed = flushOpen();
1258
+ if (flushed.terminal) return { assistantText, sawReasoning, terminal: flushed.terminal };
1259
+ for (const event of flushed.events) {
1260
+ yield* emitRetained(stage(event));
1261
+ }
1262
+ }
1263
+
1264
+ const finalUsage = usage();
1265
+ const finalProviderState = providerState();
1266
+ if (contextUsagePercentage !== undefined) {
1267
+ debugProviderDiagnostic("kiro", "context_usage", {
1268
+ contextUsagePercentage,
1269
+ ...(contextWindowState.value ? { upstreamContextWindow: contextWindowState.value } : {}),
1270
+ });
1271
+ }
1272
+ // Native stop metadata proves that this inference ended, but it does not prove that ordinary
1273
+ // text is a final answer. Kiro has emitted END_TURN for progress prose, so tool-enabled turns
1274
+ // still require the private completion call to distinguish commentary from completion (#531).
1275
+ const normalizedStopReason = stopReason?.trim().toUpperCase();
1276
+ const nativeCompletionStop = (normalizedStopReason === KIRO_END_TURN_STOP_REASON
1277
+ || normalizedStopReason === "STOP_SEQUENCE")
1278
+ && sawText
1279
+ && !sawRealTool
1280
+ && completionAnswer === undefined
1281
+ && completionCalls === 0;
1282
+
1283
+ debugProviderDiagnostic("kiro", "attempt_complete", {
1284
+ mode,
1285
+ sawText,
1286
+ sawReasoning,
1287
+ sawRealTool,
1288
+ completionCalls,
1289
+ nativeCompletionStop,
1290
+ ...(stopReason !== undefined ? { stopReason } : {}),
1291
+ assistantChars: assistantText.length,
1292
+ });
1293
+
1294
+ if (mode === "required") {
1295
+ yield* emitRetained(deferred.splice(0));
1296
+ }
1297
+
1298
+ if (mode === "text_fallback") {
1299
+ if (completionAnswer !== undefined) {
1300
+ yield* emitRetained(fallbackEvents);
1301
+ yield { type: "text_delta", text: completionAnswer, phase: "final_answer" };
1302
+ return {
1303
+ assistantText,
1304
+ sawReasoning,
1305
+ terminal: { type: "done", usage: finalUsage, endTurn: true, ...(finalProviderState ? { providerState: finalProviderState } : {}) },
1306
+ };
1307
+ }
1308
+ if (sawRealTool) {
1309
+ yield* emitRetained(fallbackEvents);
1310
+ return {
1311
+ assistantText,
1312
+ sawReasoning,
1313
+ terminal: { type: "done", usage: finalUsage, endTurn: false, ...(finalProviderState ? { providerState: finalProviderState } : {}) },
1314
+ };
1315
+ }
1316
+ if (sawText) {
1317
+ for (const event of fallbackEvents) {
1318
+ try {
1319
+ if (event.type !== "text_delta") yield event;
1320
+ else yield { ...event, phase: "final_answer" };
1321
+ } finally {
1322
+ retention.releaseEvent(event);
1323
+ }
1324
+ }
1325
+ return {
1326
+ assistantText,
1327
+ sawReasoning,
1328
+ terminal: { type: "done", usage: finalUsage, endTurn: true, ...(finalProviderState ? { providerState: finalProviderState } : {}) },
1329
+ };
1330
+ }
1331
+ yield* emitRetained(fallbackEvents);
1332
+ return {
1333
+ assistantText,
1334
+ sawReasoning,
1335
+ terminal: retryableKiroIncomplete(
1336
+ sawReasoning ? "reasoning_only_kiro_fallback" : "empty_kiro_fallback",
1337
+ sawReasoning
1338
+ ? "Kiro produced reasoning but no final answer on its bounded completion retry"
1339
+ : "Kiro produced no final answer on its bounded completion retry",
1340
+ finalUsage,
1341
+ finalProviderState,
1342
+ // First-attempt progress was already flushed before this bounded fallback (#520).
1343
+ !priorEmittedOutput,
1344
+ ),
1345
+ };
1346
+ }
1347
+
1348
+ if (completionAnswer !== undefined) {
1349
+ yield { type: "text_delta", text: completionAnswer, phase: "final_answer" };
1350
+ return {
1351
+ assistantText,
1352
+ sawReasoning,
1353
+ terminal: { type: "done", usage: finalUsage, endTurn: true, ...(finalProviderState ? { providerState: finalProviderState } : {}) },
1354
+ };
1355
+ }
1356
+ if (sawRealTool) {
1357
+ return {
1358
+ assistantText,
1359
+ sawReasoning,
1360
+ terminal: { type: "done", usage: finalUsage, endTurn: false, ...(finalProviderState ? { providerState: finalProviderState } : {}) },
1361
+ };
1362
+ }
1363
+ if (mode === "required" && nativeCompletionStop) {
1364
+ return {
1365
+ assistantText,
1366
+ sawReasoning,
1367
+ needsFallback: true,
1368
+ usage: finalUsage,
1369
+ providerState: finalProviderState,
1370
+ };
1371
+ }
1372
+
1373
+ // An explicit non-completion stop reason has already terminated this inference. Converting it into
1374
+ // another model request would hide truncation behind a second paid call, and for context
1375
+ // exhaustion it would resubmit a request that cannot fit. Only a MISSING stop reason falls
1376
+ // through to the bounded compatibility fallback below.
1377
+ //
1378
+ // END_TURN and STOP_SEQUENCE with text take the bounded validation path above; reaching here
1379
+ // with either means the turn produced no replayable text.
1380
+ if (mode === "required" && normalizedStopReason !== undefined) {
1381
+ const providerStateField = finalProviderState ? { providerState: finalProviderState } : {};
1382
+ const incomplete = (reason: string, retryable: boolean) => ({
1383
+ assistantText,
1384
+ sawReasoning,
1385
+ terminal: {
1386
+ type: "incomplete" as const,
1387
+ reason,
1388
+ message: `Kiro stopped with ${normalizedStopReason} before an explicit final answer`,
1389
+ usage: finalUsage,
1390
+ retryable,
1391
+ endTurn: false,
1392
+ ...providerStateField,
1393
+ },
1394
+ });
1395
+
1396
+ if (normalizedStopReason === "MODEL_CONTEXT_WINDOW_EXCEEDED") {
1397
+ // Reuse the existing context-length contract (kiro-errors.ts) instead of inventing an
1398
+ // incomplete reason: an unrecognized incomplete becomes a retryable 529 in Claude
1399
+ // outbound, and `max_output_tokens` would make responses/state.ts cache this partial
1400
+ // for continuation replay. Both invite a retry that cannot succeed.
1401
+ return {
1402
+ assistantText,
1403
+ sawReasoning,
1404
+ terminal: {
1405
+ type: "error" as const,
1406
+ message: "Kiro stopped because the model context window was exhausted",
1407
+ status: 400,
1408
+ errorType: "invalid_request_error",
1409
+ code: "context_length_exceeded",
1410
+ retryable: false,
1411
+ usage: finalUsage,
1412
+ },
1413
+ };
1414
+ }
1415
+ if (normalizedStopReason === "MAX_TOKENS") return incomplete("max_output_tokens", true);
1416
+ if (normalizedStopReason === "CONTENT_FILTERED" || normalizedStopReason === "GUARDRAIL_INTERVENED") {
1417
+ return incomplete("content_filter", false);
1418
+ }
1419
+ if (normalizedStopReason === "MALFORMED_TOOL_USE") return incomplete("kiro_malformed_tool_use", false);
1420
+ if (normalizedStopReason === "MALFORMED_MODEL_OUTPUT") return incomplete("kiro_malformed_model_output", false);
1421
+ // TOOL_USE here means Kiro claimed a tool call it never emitted.
1422
+ if (normalizedStopReason === "TOOL_USE") return incomplete("kiro_tool_use_without_call", false);
1423
+ if (normalizedStopReason === KIRO_END_TURN_STOP_REASON || normalizedStopReason === "STOP_SEQUENCE") {
1424
+ return incomplete(`kiro_${normalizedStopReason.toLowerCase()}_without_text`, false);
1425
+ }
1426
+ return incomplete(`kiro_${normalizedStopReason.toLowerCase() || "unknown_stop"}`, false);
1427
+ }
1428
+ // Kiro text has no trustworthy final/progress marker. When completion is required, ordinary
1429
+ // text and reasoning remain unfinished until the one bounded fallback validates the turn.
1430
+ if (mode === "required" && (sawText || sawReasoning)) {
1431
+ return { assistantText, sawReasoning, needsFallback: true, usage: finalUsage, providerState: finalProviderState };
1432
+ }
1433
+ if (!sawText && !sawReasoning) {
1434
+ return {
1435
+ assistantText,
1436
+ sawReasoning,
1437
+ terminal: retryableKiroIncomplete(
1438
+ "empty_kiro_stream",
1439
+ "Kiro returned a successful but empty response stream",
1440
+ finalUsage,
1441
+ finalProviderState,
1442
+ ),
1443
+ };
1444
+ }
1445
+ return {
1446
+ assistantText,
1447
+ sawReasoning,
1448
+ terminal: {
1449
+ type: "done",
1450
+ usage: finalUsage,
1451
+ endTurn: mode === "disabled" ? sawText : false,
1452
+ ...(finalProviderState ? { providerState: finalProviderState } : {}),
1453
+ },
1454
+ };
1455
+ } catch (err) {
1456
+ if (isTranslatorBudgetExceededError(err)) {
1457
+ if (open) budget.closeCall(open.id);
1458
+ return {
1459
+ assistantText,
1460
+ sawReasoning,
1461
+ terminal: {
1462
+ type: "error",
1463
+ status: 502,
1464
+ errorType: "upstream_error",
1465
+ code: "translation_buffer_limit",
1466
+ message: "upstream translation buffer exceeded the safe limit",
1467
+ },
1468
+ };
1469
+ }
1470
+ // Mid-stream socket closes after response.created / heartbeats only must stay retryable:
1471
+ // nothing was relayed to the client, so a string-body replay is safe (see #519 / cursor's
1472
+ // emittedOutput gate). Once any assistant text, reasoning, tool, or deferred content exists
1473
+ // — including content flushed by a prior attempt before a bounded fallback — fail closed;
1474
+ // the client may already have partial output. Protocol parse throws stay non-retryable even
1475
+ // with zero output.
1476
+ const emittedOutput = priorEmittedOutput
1477
+ || sawText
1478
+ || sawReasoning
1479
+ || sawRealTool
1480
+ || assistantText.length > 0
1481
+ || deferred.length > 0
1482
+ || completionAnswer !== undefined
1483
+ || completionCalls > 0
1484
+ || open !== null
1485
+ || fallbackEvents.length > 0;
1486
+ return {
1487
+ assistantText,
1488
+ sawReasoning,
1489
+ terminal: {
1490
+ type: "error",
1491
+ message: safeKiroErrorMessage({}, err instanceof Error ? err.message : String(err)),
1492
+ status: 502,
1493
+ errorType: "server_error",
1494
+ code: "kiro_stream_protocol_error",
1495
+ retryable: isRetryableKiroStreamCatchError(err, emittedOutput),
1496
+ usage: usage(),
1497
+ },
1498
+ };
1499
+ }
1500
+ }
1501
+
1502
+ export async function* parseKiroStream(
1503
+ response: Response,
1504
+ budget: TranslatorBudget,
1505
+ modelId?: string,
1506
+ inputTokens = 0,
1507
+ contextWindow?: number,
1508
+ nameMap?: Map<string, string>,
1509
+ conversationId?: string,
1510
+ completionMode: KiroCompletionMode = "disabled",
1511
+ fallbackFactory?: KiroFallbackFactory,
1512
+ contextInputEstimate?: number,
1513
+ ): AsyncGenerator<AdapterEvent> {
1514
+ const contextWindowState: KiroContextWindowState = { value: contextWindow };
1515
+ const first = parseKiroAttempt(
1516
+ response,
1517
+ budget,
1518
+ completionMode,
1519
+ modelId,
1520
+ inputTokens,
1521
+ contextWindowState,
1522
+ nameMap,
1523
+ conversationId,
1524
+ contextInputEstimate,
1525
+ false,
1526
+ );
1527
+ let firstNext = await first.next();
1528
+ while (!firstNext.done) {
1529
+ yield firstNext.value;
1530
+ firstNext = await first.next();
1531
+ }
1532
+ const firstResult = firstNext.value;
1533
+ try {
1534
+ if (!firstResult.needsFallback) {
1535
+ if (firstResult.terminal) yield firstResult.terminal;
1536
+ return;
1537
+ }
1538
+ if (!fallbackFactory) {
1539
+ yield retryableKiroIncomplete(
1540
+ "uncompleted_kiro_response",
1541
+ "Kiro produced progress without an explicit final answer and no bounded retry transport was available",
1542
+ firstResult.usage ?? { inputTokens, outputTokens: 0, estimated: true },
1543
+ firstResult.providerState,
1544
+ );
1545
+ return;
1546
+ }
1547
+
1548
+ yield { type: "heartbeat" };
1549
+ // First attempt already flushed deferred progress before this point. Gate fallback
1550
+ // setup/HTTP failures the same way as the second-stream catch so a replay cannot
1551
+ // duplicate visible commentary (#520).
1552
+ const priorEmittedOutput = Boolean(firstResult.assistantText.trim()) || firstResult.sawReasoning;
1553
+ let firstAssistantText = firstResult.assistantText;
1554
+ const firstHadAssistantText = firstAssistantText.length > 0;
1555
+ let fallback: KiroFallbackAttempt;
1556
+ try {
1557
+ fallback = await fallbackFactory(
1558
+ firstResult.providerState?.kiro.conversationId ?? conversationId,
1559
+ firstAssistantText,
1560
+ firstResult.sawReasoning,
1561
+ budget,
1562
+ );
1563
+ } catch (err) {
1564
+ firstAssistantText = "";
1565
+ firstResult.assistantText = "";
1566
+ firstResult.releaseRetained();
1567
+ if (isTranslatorBudgetExceededError(err)) {
1568
+ yield {
1569
+ type: "error",
1570
+ message: "upstream translation buffer exceeded the safe limit",
1571
+ status: 502,
1572
+ errorType: "upstream_error",
1573
+ code: "translation_buffer_limit",
1574
+ usage: firstResult.usage,
1575
+ };
1576
+ return;
1577
+ }
1578
+ yield {
1579
+ type: "error",
1580
+ message: safeKiroErrorMessage({}, err instanceof Error ? err.message : String(err)),
1581
+ status: err instanceof Error && err.name === "TimeoutError" ? 504 : 502,
1582
+ errorType: "upstream_error",
1583
+ retryable: !priorEmittedOutput,
1584
+ usage: firstResult.usage,
1585
+ };
1586
+ return;
1587
+ }
1588
+ // The factory has finished using the live first-attempt alias and has retained its own retry
1589
+ // serialization through the fetch boundary. The discarded parser collectors can now release
1590
+ // before the second attempt begins on the same turn budget.
1591
+ firstAssistantText = "";
1592
+ firstResult.assistantText = "";
1593
+ firstResult.releaseRetained();
1594
+ fallback.releaseRequestBody?.();
1595
+ if (!fallback.response.ok) {
1596
+ const payload = await fallback.response.text().catch(() => "");
1597
+ const failure = classifyKiroHttpError(fallback.response.status, fallback.response.headers, payload);
1598
+ yield {
1599
+ type: "error",
1600
+ message: failure.message,
1601
+ status: failure.status,
1602
+ errorType: failure.errorType,
1603
+ code: failure.code,
1604
+ retryable: priorEmittedOutput ? false : failure.retryable,
1605
+ usage: firstResult.usage,
1606
+ };
1607
+ return;
1608
+ }
1609
+
1610
+ const second = parseKiroAttempt(
1611
+ fallback.response,
1612
+ budget,
1613
+ "text_fallback",
1614
+ modelId,
1615
+ fallback.inputTokens,
1616
+ contextWindowState,
1617
+ fallback.nameMap,
1618
+ fallback.conversationId,
1619
+ fallback.contextInputEstimate,
1620
+ // First attempt already flushed deferred progress to the client before this fallback.
1621
+ // A zero-output transport failure here must stay non-retryable to avoid duplicating that text.
1622
+ priorEmittedOutput,
1623
+ );
1624
+ let secondNext = await second.next();
1625
+ while (!secondNext.done) {
1626
+ yield secondNext.value;
1627
+ secondNext = await second.next();
1628
+ }
1629
+ const secondResult = secondNext.value;
1630
+ try {
1631
+ if (!secondResult.terminal) {
1632
+ yield retryableKiroIncomplete(
1633
+ "empty_kiro_fallback",
1634
+ "Kiro's bounded completion retry ended without a terminal result",
1635
+ mergeKiroUsage(firstResult.usage, secondResult.usage, firstHadAssistantText)
1636
+ ?? { inputTokens, outputTokens: 0, estimated: true },
1637
+ secondResult.providerState ?? firstResult.providerState,
1638
+ !priorEmittedOutput,
1639
+ );
1640
+ return;
1641
+ }
1642
+ if (secondResult.terminal.type === "done" || secondResult.terminal.type === "incomplete") {
1643
+ yield {
1644
+ ...secondResult.terminal,
1645
+ // Belt-and-suspenders: never advertise a replay-safe incomplete after flushed progress.
1646
+ ...(secondResult.terminal.type === "incomplete" && priorEmittedOutput
1647
+ ? { retryable: false as const }
1648
+ : {}),
1649
+ usage: mergeKiroUsage(firstResult.usage, secondResult.terminal.usage, firstHadAssistantText),
1650
+ providerState: secondResult.terminal.providerState ?? firstResult.providerState,
1651
+ };
1652
+ return;
1653
+ }
1654
+ yield {
1655
+ ...secondResult.terminal,
1656
+ ...(secondResult.terminal.type === "error"
1657
+ ? { usage: mergeKiroUsage(firstResult.usage, secondResult.terminal.usage, firstHadAssistantText) }
1658
+ : {}),
1659
+ };
1660
+ } finally {
1661
+ secondResult.releaseRetained();
1662
+ }
1663
+ } finally {
1664
+ firstResult.releaseRetained();
1665
+ }
1666
+ }
1667
+
1668
+ // Adapter
1669
+ export function createKiroAdapter(provider: OcxProviderConfig): ProviderAdapter {
1670
+ // Per-request closure (resolveAdapter builds a fresh adapter per request — server.ts:440 — so this
1671
+ // is race-free) carrying the heuristic input-token estimate from buildRequest into the stream.
1672
+ let inputTokens = 0;
1673
+ let contextInputEstimate = 0;
1674
+ let modelId: string | undefined;
1675
+ let contextWindow: number | undefined;
1676
+ let toolNameMap: Map<string, string> | undefined;
1677
+ let conversationId: string | undefined;
1678
+ let completionMode: KiroCompletionMode = "disabled";
1679
+ let requestSnapshot: OcxParsedRequest | undefined;
1680
+ let firstRequestBodyBytes = 0;
1681
+ let requestAbortSignal: AbortSignal | undefined;
1682
+
1683
+ const build = async (
1684
+ parsed: OcxParsedRequest,
1685
+ forcedCompletionMode?: KiroCompletionMode,
1686
+ ): Promise<{
1687
+ request: AdapterRequest;
1688
+ nameMap: Map<string, string>;
1689
+ conversationId: string;
1690
+ completionMode: KiroCompletionMode;
1691
+ inputTokens: number;
1692
+ contextInputEstimate: number;
1693
+ }> => {
1694
+ if (typeof provider.apiKey !== "string" || provider.apiKey.trim() === "") {
1695
+ throw new Error("kiro token missing — run ocx login kiro");
1696
+ }
1697
+ const region = resolveKiroApiRegion(parsed._kiroAuthContext);
1698
+ const resolvedProfileArn = resolveKiroProfileArn(parsed._kiroAuthContext);
1699
+ const isApiKey = provider.apiKey.trim().startsWith("ksk_");
1700
+ const profileArn = isApiKey ? undefined : resolvedProfileArn;
1701
+ // Builder ID and Kiro API keys have no profile ARN and are accepted only on Kiro's CLI
1702
+ // request path. Enterprise profiles retain the existing IDE-shaped request.
1703
+ const wireClient: KiroWireClient = isApiKey || !profileArn ? "cli" : "ide";
1704
+ const fp = fingerprint().slice(0, 64);
1705
+ const headers: Record<string, string> = wireClient === "cli" ? {
1706
+ authorization: `Bearer ${provider.apiKey}`,
1707
+ "content-type": "application/x-amz-json-1.0",
1708
+ accept: "*/*",
1709
+ "x-amz-target": AMZ_TARGET,
1710
+ "user-agent": kiroCliUserAgent(true),
1711
+ "x-amz-user-agent": kiroCliUserAgent(false),
1712
+ "x-amzn-codewhisperer-optout": "true",
1713
+ "amz-sdk-request": "attempt=1; max=3",
1714
+ "amz-sdk-invocation-id": invocationId(),
1715
+ ...(isApiKey ? { tokentype: "API_KEY" } : {}),
1716
+ } : {
1717
+ authorization: `Bearer ${provider.apiKey}`,
1718
+ "content-type": "application/x-amz-json-1.0",
1719
+ accept: "application/vnd.amazon.eventstream",
1720
+ "x-amz-target": AMZ_TARGET,
1721
+ "user-agent": `aws-sdk-js/${SDK_VERSION} ua/2.1 os/${osTag()} lang/js md/nodejs#${NODE_VERSION} api/codewhispererstreaming#${SDK_VERSION} m/E KiroIDE-${KIRO_IDE_VERSION}-${fp}`,
1722
+ "x-amz-user-agent": `aws-sdk-js/${SDK_VERSION} KiroIDE-${KIRO_IDE_VERSION}-${fp}`,
1723
+ "x-amzn-codewhisperer-optout": "true",
1724
+ "x-amzn-kiro-agent-mode": "vibe",
1725
+ "amz-sdk-invocation-id": invocationId(),
1726
+ };
1727
+ if (profileArn) headers["x-amzn-kiro-profile-arn"] = profileArn;
1728
+ const built = buildKiroPayload(parsed, profileArn, forcedCompletionMode, wireClient);
1729
+ await normalizeKiroImages(built.payload);
1730
+ const contextInputEstimate = estimateKiroPayloadInputTokens(built.payload, parsed.modelId);
1731
+ const body = JSON.stringify(built.payload);
1732
+ debugProviderDiagnostic("kiro", "request", {
1733
+ region,
1734
+ requestedModel: parsed.modelId,
1735
+ completionMode: built.completionMode,
1736
+ bodyBytes: new TextEncoder().encode(body).length,
1737
+ messageCount: kiroPayloadMessages(parsed).length,
1738
+ toolCount: parsed.context.tools?.length ?? 0,
1739
+ hasProfileArn: Boolean(profileArn),
1740
+ wireClient,
1741
+ hasPreviousResponseId: Boolean(parsed.previousResponseId),
1742
+ });
1743
+ return {
1744
+ request: {
1745
+ url: kiroRuntimeEndpoint(provider, region),
1746
+ method: "POST",
1747
+ headers,
1748
+ body,
1749
+ usageLog: { inputTokens: estimateKiroLogInputTokens(parsed), estimated: true },
1750
+ },
1751
+ nameMap: built.nameMap,
1752
+ conversationId: built.conversationId,
1753
+ completionMode: built.completionMode,
1754
+ inputTokens: estimateKiroInputTokens(parsed),
1755
+ contextInputEstimate,
1756
+ };
1757
+ };
1758
+
1759
+ const fallbackFactory: KiroFallbackFactory = async (
1760
+ returnedConversationId,
1761
+ assistantText,
1762
+ _sawReasoning,
1763
+ budget,
1764
+ ) => {
1765
+ if (!requestSnapshot) throw new Error("Kiro completion retry lost its request state");
1766
+ if (requestAbortSignal?.aborted) {
1767
+ throw requestAbortSignal.reason instanceof Error
1768
+ ? requestAbortSignal.reason
1769
+ : new DOMException("Kiro request was cancelled", "AbortError");
1770
+ }
1771
+ const retryParsed = structuredClone(requestSnapshot);
1772
+ retryParsed._providerContinuation = {
1773
+ ...(retryParsed._providerContinuation ?? {}),
1774
+ ...(returnedConversationId ? { kiro: { conversationId: returnedConversationId } } : {}),
1775
+ };
1776
+ // Reasoning is not replayable on the Kiro wire. Adding an empty assistant turn merely to mark
1777
+ // that reasoning existed creates REQUEST_BODY_INVALID; only visible text earns a replay turn.
1778
+ if (assistantText.trim()) {
1779
+ retryParsed.context.messages.push({
1780
+ role: "assistant",
1781
+ content: [{ type: "text" as const, text: assistantText }],
1782
+ phase: "commentary",
1783
+ model: retryParsed.modelId,
1784
+ timestamp: Date.now(),
1785
+ });
1786
+ }
1787
+ // The retry starts from the already measured first wire body, adds one JSON-escaped replay
1788
+ // string, and only changes bounded Kiro-owned fields (completion prompt/tool, history wrapper,
1789
+ // and <=256-byte conversation id). 64 KiB is a conservative envelope for those fixed fields.
1790
+ // Reserve that complete upper bound while the first-attempt collectors are still charged so a
1791
+ // near-cap turn fails before build() can materialize the retry payload or serialized body.
1792
+ const retryBodyUpperBound = firstRequestBodyBytes
1793
+ + jsonStringSerializedUtf8Bytes(assistantText)
1794
+ + KIRO_FALLBACK_SERIALIZATION_ENVELOPE_BYTES;
1795
+ const retryBodyReservation = budget.reserveTransient(retryBodyUpperBound, { kind: "request_copies" });
1796
+ let retryBodyBytes = 0;
1797
+ let retryBodyRetained = false;
1798
+ let requestBodyReleased = false;
1799
+ const releaseRequestBody = () => {
1800
+ if (requestBodyReleased) return;
1801
+ requestBodyReleased = true;
1802
+ if (retryBodyRetained) budget.releaseRetained(retryBodyBytes, { kind: "request_copies" });
1803
+ else retryBodyReservation.release();
1804
+ };
1805
+ try {
1806
+ const retry = await build(retryParsed, "text_fallback");
1807
+ retryBodyBytes = Buffer.byteLength(retry.request.body);
1808
+ if (retryBodyBytes > retryBodyUpperBound) {
1809
+ throw new Error("Kiro retry serialization exceeded its pre-admitted upper bound");
1810
+ }
1811
+ retryBodyReservation.commitRetained();
1812
+ retryBodyRetained = true;
1813
+ budget.releaseRetained(retryBodyUpperBound - retryBodyBytes, { kind: "request_copies" });
1814
+ const response = await fetchKiroWithRetry(retry.request, {
1815
+ abortSignal: requestAbortSignal,
1816
+ returnRawErrors: true,
1817
+ stream: true,
1818
+ });
1819
+ return {
1820
+ response,
1821
+ inputTokens: retry.inputTokens,
1822
+ contextInputEstimate: retry.contextInputEstimate,
1823
+ nameMap: retry.nameMap,
1824
+ conversationId: retry.conversationId,
1825
+ releaseRequestBody,
1826
+ };
1827
+ } catch (error) {
1828
+ releaseRequestBody();
1829
+ throw error;
1830
+ }
1831
+ };
1832
+
1833
+ return {
1834
+ name: "kiro",
1835
+ async buildRequest(parsed: OcxParsedRequest, incoming) {
1836
+ const built = await build(parsed);
1837
+ modelId = parsed.modelId;
1838
+ contextWindow = kiroUpstreamContextWindow(parsed.modelId);
1839
+ inputTokens = built.inputTokens;
1840
+ contextInputEstimate = built.contextInputEstimate;
1841
+ toolNameMap = built.nameMap;
1842
+ conversationId = built.conversationId;
1843
+ completionMode = built.completionMode;
1844
+ requestSnapshot = structuredClone(parsed);
1845
+ firstRequestBodyBytes = Buffer.byteLength(built.request.body);
1846
+ requestAbortSignal = incoming?.abortSignal;
1847
+ return built.request;
1848
+ },
1849
+
1850
+ parseStream(response: Response, budget: TranslatorBudget): AsyncGenerator<AdapterEvent> {
1851
+ return parseKiroStream(
1852
+ response,
1853
+ budget,
1854
+ modelId,
1855
+ inputTokens,
1856
+ contextWindow,
1857
+ toolNameMap,
1858
+ conversationId,
1859
+ completionMode,
1860
+ completionMode === "required" ? fallbackFactory : undefined,
1861
+ contextInputEstimate,
1862
+ );
1863
+ },
1864
+
1865
+ fetchResponse(request: AdapterRequest, ctx?: AdapterFetchContext): Promise<Response> {
1866
+ // The normal Responses path supplies cancellation at fetch time rather than build time.
1867
+ // Keep it for the adapter-owned bounded continuation so cancelling the client turn aborts
1868
+ // both the first Kiro request and its one allowed completion retry.
1869
+ if (ctx?.abortSignal) requestAbortSignal = ctx.abortSignal;
1870
+ return fetchKiroWithRetry(request, ctx);
1871
+ },
1872
+
1873
+ formatErrorBody(status: number, headers: Headers, payloadText: string): string {
1874
+ return safeKiroHttpErrorMessage(status, headers, payloadText);
1875
+ },
1876
+
1877
+ // Non-streaming path used by the web-search sidecar loop (loop.ts runs each iteration
1878
+ // non-streamed so it can inspect tool calls). CW only ever event-streams, so we drain the
1879
+ // same decoder into an array. Without this, any Codex request that includes the web_search
1880
+ // tool failed with "web-search sidecar requires a non-streaming adapter" (kiro-only).
1881
+ async parseResponse(response: Response, budget: TranslatorBudget): Promise<AdapterEvent[]> {
1882
+ const events: AdapterEvent[] = [];
1883
+ for await (const e of parseKiroStream(
1884
+ response,
1885
+ budget,
1886
+ modelId,
1887
+ inputTokens,
1888
+ contextWindow,
1889
+ toolNameMap,
1890
+ conversationId,
1891
+ completionMode,
1892
+ completionMode === "required" ? fallbackFactory : undefined,
1893
+ contextInputEstimate,
1894
+ )) events.push(e);
1895
+ return events;
1896
+ },
1897
+ };
1898
+ }