@aryee337/aery-ai 0.2.27 → 0.2.29

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 (417) hide show
  1. package/CHANGELOG.md +2914 -0
  2. package/README.md +614 -813
  3. package/package.json +140 -105
  4. package/src/api-registry.ts +96 -0
  5. package/src/auth-broker/client.ts +358 -0
  6. package/src/auth-broker/index.ts +5 -0
  7. package/src/auth-broker/refresher.ts +117 -0
  8. package/src/auth-broker/remote-store.ts +623 -0
  9. package/src/auth-broker/server.ts +644 -0
  10. package/src/auth-broker/types.ts +127 -0
  11. package/src/auth-broker/wire-schemas.ts +200 -0
  12. package/src/auth-gateway/http.ts +194 -0
  13. package/src/auth-gateway/index.ts +3 -0
  14. package/src/auth-gateway/server.ts +818 -0
  15. package/src/auth-gateway/types.ts +143 -0
  16. package/src/auth-storage.ts +4422 -0
  17. package/src/index.ts +54 -0
  18. package/src/model-cache.ts +129 -0
  19. package/src/model-manager.ts +469 -0
  20. package/src/model-thinking.ts +782 -0
  21. package/src/models.json +83530 -0
  22. package/src/models.json.d.ts +9 -0
  23. package/src/models.ts +56 -0
  24. package/src/prompts/turn-aborted-guidance.md +4 -0
  25. package/src/provider-details.ts +90 -0
  26. package/src/provider-models/bundled-references.ts +38 -0
  27. package/src/provider-models/descriptors.ts +355 -0
  28. package/src/provider-models/google.ts +88 -0
  29. package/src/provider-models/index.ts +5 -0
  30. package/src/provider-models/ollama.ts +153 -0
  31. package/src/provider-models/openai-compat.ts +2817 -0
  32. package/src/provider-models/special.ts +67 -0
  33. package/src/providers/aery-native-client.ts +228 -0
  34. package/src/providers/aery-native-server.ts +212 -0
  35. package/src/providers/amazon-bedrock.ts +873 -0
  36. package/src/providers/anthropic-client.ts +318 -0
  37. package/src/providers/anthropic-messages-server-schema.ts +243 -0
  38. package/src/providers/anthropic-messages-server.ts +683 -0
  39. package/src/providers/anthropic-wire.ts +268 -0
  40. package/src/providers/anthropic.ts +3094 -0
  41. package/src/providers/aws-credentials.ts +501 -0
  42. package/src/providers/aws-eventstream.ts +185 -0
  43. package/src/providers/aws-sigv4.ts +218 -0
  44. package/src/providers/azure-openai-responses.ts +361 -0
  45. package/src/providers/cursor/gen/agent_pb.ts +15274 -0
  46. package/src/providers/cursor/proto/agent.proto +3526 -0
  47. package/src/providers/cursor/proto/buf.gen.yaml +6 -0
  48. package/src/providers/cursor/proto/buf.yaml +17 -0
  49. package/src/providers/cursor.ts +2621 -0
  50. package/src/providers/error-message.ts +21 -0
  51. package/src/providers/github-copilot-headers.ts +140 -0
  52. package/src/providers/gitlab-duo.ts +372 -0
  53. package/src/providers/google-auth.ts +252 -0
  54. package/src/providers/google-gemini-cli.ts +809 -0
  55. package/src/providers/google-gemini-headers.ts +41 -0
  56. package/src/providers/google-shared.ts +917 -0
  57. package/src/providers/google-types.ts +167 -0
  58. package/src/providers/google-vertex.ts +91 -0
  59. package/src/providers/google.ts +41 -0
  60. package/src/providers/grammar.ts +70 -0
  61. package/src/providers/kimi.ts +52 -0
  62. package/src/providers/mock.ts +496 -0
  63. package/src/providers/ollama.ts +644 -0
  64. package/src/providers/openai-anthropic-shim.ts +138 -0
  65. package/src/providers/openai-chat-server-schema.ts +252 -0
  66. package/src/providers/openai-chat-server.ts +647 -0
  67. package/src/providers/openai-codex/constants.ts +43 -0
  68. package/src/providers/openai-codex/request-transformer.ts +161 -0
  69. package/src/providers/openai-codex/response-handler.ts +81 -0
  70. package/src/providers/openai-codex-responses.ts +3018 -0
  71. package/src/providers/openai-completions-compat.ts +300 -0
  72. package/src/providers/openai-completions.ts +1979 -0
  73. package/src/providers/openai-responses-server-schema.ts +290 -0
  74. package/src/providers/openai-responses-server.ts +1183 -0
  75. package/src/providers/openai-responses-shared.ts +873 -0
  76. package/src/providers/openai-responses.ts +679 -0
  77. package/src/providers/register-builtins.ts +436 -0
  78. package/src/providers/synthetic.ts +50 -0
  79. package/src/providers/transform-messages.ts +382 -0
  80. package/src/providers/vision-guard.ts +31 -0
  81. package/src/providers/xai-responses.ts +82 -0
  82. package/src/rate-limit-utils.ts +84 -0
  83. package/src/stream.ts +1065 -0
  84. package/src/types.ts +944 -0
  85. package/src/usage/claude.ts +482 -0
  86. package/src/usage/gemini.ts +250 -0
  87. package/src/usage/github-copilot.ts +421 -0
  88. package/src/usage/google-antigravity.ts +201 -0
  89. package/src/usage/kimi.ts +271 -0
  90. package/src/usage/minimax-code.ts +31 -0
  91. package/src/usage/openai-codex.ts +503 -0
  92. package/src/usage/shared.ts +10 -0
  93. package/src/usage/zai.ts +247 -0
  94. package/src/usage.ts +185 -0
  95. package/src/utils/abort.ts +51 -0
  96. package/src/utils/abortable-iterator.ts +69 -0
  97. package/src/utils/anthropic-auth.ts +93 -0
  98. package/src/utils/discovery/antigravity.ts +261 -0
  99. package/src/utils/discovery/codex.ts +371 -0
  100. package/src/utils/discovery/cursor.ts +306 -0
  101. package/src/utils/discovery/gemini.ts +248 -0
  102. package/src/utils/discovery/index.ts +4 -0
  103. package/src/utils/discovery/openai-compatible.ts +224 -0
  104. package/src/utils/event-stream.ts +142 -0
  105. package/src/utils/fireworks-model-id.ts +30 -0
  106. package/src/utils/foundry.ts +8 -0
  107. package/src/utils/http-inspector.ts +176 -0
  108. package/src/utils/idle-iterator.ts +267 -0
  109. package/src/utils/json-parse.ts +182 -0
  110. package/src/utils/oauth/__tests__/xai-oauth.test.ts +107 -0
  111. package/src/utils/oauth/alibaba-coding-plan.ts +59 -0
  112. package/src/utils/oauth/anthropic.ts +273 -0
  113. package/src/utils/oauth/api-key-login.ts +87 -0
  114. package/src/utils/oauth/api-key-validation.ts +92 -0
  115. package/src/utils/oauth/callback-server.ts +276 -0
  116. package/src/utils/oauth/cerebras.ts +16 -0
  117. package/src/utils/oauth/cloudflare-ai-gateway.ts +48 -0
  118. package/src/utils/oauth/cursor.ts +157 -0
  119. package/src/utils/oauth/deepseek.ts +53 -0
  120. package/src/utils/oauth/firepass.ts +24 -0
  121. package/src/utils/oauth/fireworks.ts +15 -0
  122. package/src/utils/oauth/github-copilot.ts +362 -0
  123. package/src/utils/oauth/gitlab-duo.ts +123 -0
  124. package/src/utils/oauth/google-antigravity.ts +200 -0
  125. package/src/utils/oauth/google-gemini-cli.ts +256 -0
  126. package/src/utils/oauth/google-oauth-shared.ts +110 -0
  127. package/src/utils/oauth/huggingface.ts +62 -0
  128. package/src/utils/oauth/index.ts +484 -0
  129. package/src/utils/oauth/kagi.ts +47 -0
  130. package/src/utils/oauth/kilo.ts +87 -0
  131. package/src/utils/oauth/kimi.ts +254 -0
  132. package/src/utils/oauth/litellm.ts +47 -0
  133. package/src/utils/oauth/lm-studio.ts +38 -0
  134. package/src/utils/oauth/minimax-code.ts +78 -0
  135. package/src/utils/oauth/moonshot.ts +23 -0
  136. package/src/utils/oauth/nanogpt.ts +15 -0
  137. package/src/utils/oauth/nvidia.ts +70 -0
  138. package/src/utils/oauth/oauth.html +203 -0
  139. package/src/utils/oauth/ollama-cloud.ts +28 -0
  140. package/src/utils/oauth/ollama.ts +47 -0
  141. package/src/utils/oauth/openai-codex.ts +299 -0
  142. package/src/utils/oauth/opencode.ts +49 -0
  143. package/src/utils/oauth/openrouter.ts +20 -0
  144. package/src/utils/oauth/parallel.ts +46 -0
  145. package/src/utils/oauth/perplexity.ts +206 -0
  146. package/src/utils/oauth/pkce.ts +18 -0
  147. package/src/utils/oauth/qianfan.ts +58 -0
  148. package/src/utils/oauth/qwen-portal.ts +60 -0
  149. package/src/utils/oauth/synthetic.ts +15 -0
  150. package/src/utils/oauth/tavily.ts +46 -0
  151. package/src/utils/oauth/together.ts +16 -0
  152. package/src/utils/oauth/types.ts +99 -0
  153. package/src/utils/oauth/venice.ts +59 -0
  154. package/src/utils/oauth/vercel-ai-gateway.ts +47 -0
  155. package/src/utils/oauth/vllm.ts +40 -0
  156. package/src/utils/oauth/wafer.ts +50 -0
  157. package/src/utils/oauth/xai-oauth.ts +342 -0
  158. package/src/utils/oauth/xiaomi.ts +139 -0
  159. package/src/utils/oauth/zai.ts +60 -0
  160. package/src/utils/oauth/zenmux.ts +15 -0
  161. package/src/utils/oauth/zhipu.ts +60 -0
  162. package/src/utils/overflow.ts +137 -0
  163. package/src/utils/parse-bind.ts +54 -0
  164. package/src/utils/provider-response.ts +30 -0
  165. package/src/utils/request-debug.ts +336 -0
  166. package/src/utils/retry-after.ts +110 -0
  167. package/src/utils/retry.ts +54 -0
  168. package/src/utils/schema/CONSTRAINTS.md +164 -0
  169. package/src/utils/schema/adapt.ts +36 -0
  170. package/src/utils/schema/compatibility.ts +435 -0
  171. package/src/utils/schema/dereference.ts +98 -0
  172. package/src/utils/schema/draft.ts +341 -0
  173. package/src/utils/schema/equality.ts +97 -0
  174. package/src/utils/schema/fields.ts +191 -0
  175. package/src/utils/schema/index.ts +13 -0
  176. package/src/utils/schema/json-schema-validator.ts +577 -0
  177. package/src/utils/schema/meta-validator.ts +167 -0
  178. package/src/utils/schema/normalize.ts +1588 -0
  179. package/src/utils/schema/spill.ts +43 -0
  180. package/src/utils/schema/stamps.ts +97 -0
  181. package/src/utils/schema/types.ts +10 -0
  182. package/src/utils/schema/wire.ts +293 -0
  183. package/src/utils/schema/zod-decontaminate.ts +331 -0
  184. package/src/utils/sdk-stream-timeout.ts +43 -0
  185. package/src/utils/sse-debug.ts +289 -0
  186. package/src/utils/stream-markup-healing.ts +612 -0
  187. package/src/utils/tool-choice.ts +99 -0
  188. package/src/utils/validation.ts +1024 -0
  189. package/src/utils.ts +166 -0
  190. package/dist/api-registry.d.ts +0 -20
  191. package/dist/api-registry.d.ts.map +0 -1
  192. package/dist/api-registry.js +0 -44
  193. package/dist/api-registry.js.map +0 -1
  194. package/dist/bedrock-provider.d.ts +0 -5
  195. package/dist/bedrock-provider.d.ts.map +0 -1
  196. package/dist/bedrock-provider.js +0 -6
  197. package/dist/bedrock-provider.js.map +0 -1
  198. package/dist/cli.d.ts +0 -3
  199. package/dist/cli.d.ts.map +0 -1
  200. package/dist/cli.js +0 -130
  201. package/dist/cli.js.map +0 -1
  202. package/dist/env-api-keys.d.ts +0 -18
  203. package/dist/env-api-keys.d.ts.map +0 -1
  204. package/dist/env-api-keys.js +0 -178
  205. package/dist/env-api-keys.js.map +0 -1
  206. package/dist/image-models.d.ts +0 -10
  207. package/dist/image-models.d.ts.map +0 -1
  208. package/dist/image-models.generated.d.ts +0 -440
  209. package/dist/image-models.generated.d.ts.map +0 -1
  210. package/dist/image-models.generated.js +0 -442
  211. package/dist/image-models.generated.js.map +0 -1
  212. package/dist/image-models.js +0 -23
  213. package/dist/image-models.js.map +0 -1
  214. package/dist/images-api-registry.d.ts +0 -14
  215. package/dist/images-api-registry.d.ts.map +0 -1
  216. package/dist/images-api-registry.js +0 -22
  217. package/dist/images-api-registry.js.map +0 -1
  218. package/dist/images.d.ts +0 -4
  219. package/dist/images.d.ts.map +0 -1
  220. package/dist/images.js +0 -14
  221. package/dist/images.js.map +0 -1
  222. package/dist/index.d.ts +0 -32
  223. package/dist/index.d.ts.map +0 -1
  224. package/dist/index.js +0 -20
  225. package/dist/index.js.map +0 -1
  226. package/dist/models.d.ts +0 -18
  227. package/dist/models.d.ts.map +0 -1
  228. package/dist/models.generated.d.ts +0 -17707
  229. package/dist/models.generated.d.ts.map +0 -1
  230. package/dist/models.generated.js +0 -16561
  231. package/dist/models.generated.js.map +0 -1
  232. package/dist/models.js +0 -71
  233. package/dist/models.js.map +0 -1
  234. package/dist/oauth.d.ts +0 -2
  235. package/dist/oauth.d.ts.map +0 -1
  236. package/dist/oauth.js +0 -2
  237. package/dist/oauth.js.map +0 -1
  238. package/dist/providers/aery-error-formatting.d.ts +0 -13
  239. package/dist/providers/aery-error-formatting.d.ts.map +0 -1
  240. package/dist/providers/aery-error-formatting.js +0 -112
  241. package/dist/providers/aery-error-formatting.js.map +0 -1
  242. package/dist/providers/amazon-bedrock.d.ts +0 -38
  243. package/dist/providers/amazon-bedrock.d.ts.map +0 -1
  244. package/dist/providers/amazon-bedrock.js +0 -763
  245. package/dist/providers/amazon-bedrock.js.map +0 -1
  246. package/dist/providers/anthropic.d.ts +0 -71
  247. package/dist/providers/anthropic.d.ts.map +0 -1
  248. package/dist/providers/anthropic.js +0 -949
  249. package/dist/providers/anthropic.js.map +0 -1
  250. package/dist/providers/azure-openai-responses.d.ts +0 -15
  251. package/dist/providers/azure-openai-responses.d.ts.map +0 -1
  252. package/dist/providers/azure-openai-responses.js +0 -225
  253. package/dist/providers/azure-openai-responses.js.map +0 -1
  254. package/dist/providers/cloudflare.d.ts +0 -13
  255. package/dist/providers/cloudflare.d.ts.map +0 -1
  256. package/dist/providers/cloudflare.js +0 -26
  257. package/dist/providers/cloudflare.js.map +0 -1
  258. package/dist/providers/faux.d.ts +0 -56
  259. package/dist/providers/faux.d.ts.map +0 -1
  260. package/dist/providers/faux.js +0 -368
  261. package/dist/providers/faux.js.map +0 -1
  262. package/dist/providers/github-copilot-headers.d.ts +0 -8
  263. package/dist/providers/github-copilot-headers.d.ts.map +0 -1
  264. package/dist/providers/github-copilot-headers.js +0 -29
  265. package/dist/providers/github-copilot-headers.js.map +0 -1
  266. package/dist/providers/google-gemini-cli.d.ts +0 -74
  267. package/dist/providers/google-gemini-cli.d.ts.map +0 -1
  268. package/dist/providers/google-gemini-cli.js +0 -779
  269. package/dist/providers/google-gemini-cli.js.map +0 -1
  270. package/dist/providers/google-shared.d.ts +0 -70
  271. package/dist/providers/google-shared.d.ts.map +0 -1
  272. package/dist/providers/google-shared.js +0 -329
  273. package/dist/providers/google-shared.js.map +0 -1
  274. package/dist/providers/google-vertex.d.ts +0 -15
  275. package/dist/providers/google-vertex.d.ts.map +0 -1
  276. package/dist/providers/google-vertex.js +0 -442
  277. package/dist/providers/google-vertex.js.map +0 -1
  278. package/dist/providers/google.d.ts +0 -13
  279. package/dist/providers/google.d.ts.map +0 -1
  280. package/dist/providers/google.js +0 -400
  281. package/dist/providers/google.js.map +0 -1
  282. package/dist/providers/images/openrouter.d.ts +0 -3
  283. package/dist/providers/images/openrouter.d.ts.map +0 -1
  284. package/dist/providers/images/openrouter.js +0 -129
  285. package/dist/providers/images/openrouter.js.map +0 -1
  286. package/dist/providers/images/register-builtins.d.ts +0 -4
  287. package/dist/providers/images/register-builtins.d.ts.map +0 -1
  288. package/dist/providers/images/register-builtins.js +0 -34
  289. package/dist/providers/images/register-builtins.js.map +0 -1
  290. package/dist/providers/mistral.d.ts +0 -25
  291. package/dist/providers/mistral.d.ts.map +0 -1
  292. package/dist/providers/mistral.js +0 -535
  293. package/dist/providers/mistral.js.map +0 -1
  294. package/dist/providers/openai-codex-responses.d.ts +0 -30
  295. package/dist/providers/openai-codex-responses.d.ts.map +0 -1
  296. package/dist/providers/openai-codex-responses.js +0 -1090
  297. package/dist/providers/openai-codex-responses.js.map +0 -1
  298. package/dist/providers/openai-completions.d.ts +0 -19
  299. package/dist/providers/openai-completions.d.ts.map +0 -1
  300. package/dist/providers/openai-completions.js +0 -950
  301. package/dist/providers/openai-completions.js.map +0 -1
  302. package/dist/providers/openai-prompt-cache.d.ts +0 -3
  303. package/dist/providers/openai-prompt-cache.d.ts.map +0 -1
  304. package/dist/providers/openai-prompt-cache.js +0 -10
  305. package/dist/providers/openai-prompt-cache.js.map +0 -1
  306. package/dist/providers/openai-responses-shared.d.ts +0 -18
  307. package/dist/providers/openai-responses-shared.d.ts.map +0 -1
  308. package/dist/providers/openai-responses-shared.js +0 -492
  309. package/dist/providers/openai-responses-shared.js.map +0 -1
  310. package/dist/providers/openai-responses.d.ts +0 -13
  311. package/dist/providers/openai-responses.d.ts.map +0 -1
  312. package/dist/providers/openai-responses.js +0 -237
  313. package/dist/providers/openai-responses.js.map +0 -1
  314. package/dist/providers/register-builtins.d.ts +0 -38
  315. package/dist/providers/register-builtins.d.ts.map +0 -1
  316. package/dist/providers/register-builtins.js +0 -278
  317. package/dist/providers/register-builtins.js.map +0 -1
  318. package/dist/providers/simple-options.d.ts +0 -8
  319. package/dist/providers/simple-options.d.ts.map +0 -1
  320. package/dist/providers/simple-options.js +0 -41
  321. package/dist/providers/simple-options.js.map +0 -1
  322. package/dist/providers/transform-messages.d.ts +0 -8
  323. package/dist/providers/transform-messages.d.ts.map +0 -1
  324. package/dist/providers/transform-messages.js +0 -184
  325. package/dist/providers/transform-messages.js.map +0 -1
  326. package/dist/session-resources.d.ts +0 -4
  327. package/dist/session-resources.d.ts.map +0 -1
  328. package/dist/session-resources.js +0 -22
  329. package/dist/session-resources.js.map +0 -1
  330. package/dist/stream.d.ts +0 -8
  331. package/dist/stream.d.ts.map +0 -1
  332. package/dist/stream.js +0 -27
  333. package/dist/stream.js.map +0 -1
  334. package/dist/types.d.ts +0 -498
  335. package/dist/types.d.ts.map +0 -1
  336. package/dist/types.js +0 -2
  337. package/dist/types.js.map +0 -1
  338. package/dist/utils/diagnostics.d.ts +0 -19
  339. package/dist/utils/diagnostics.d.ts.map +0 -1
  340. package/dist/utils/diagnostics.js +0 -25
  341. package/dist/utils/diagnostics.js.map +0 -1
  342. package/dist/utils/event-stream.d.ts +0 -21
  343. package/dist/utils/event-stream.d.ts.map +0 -1
  344. package/dist/utils/event-stream.js +0 -81
  345. package/dist/utils/event-stream.js.map +0 -1
  346. package/dist/utils/hash.d.ts +0 -3
  347. package/dist/utils/hash.d.ts.map +0 -1
  348. package/dist/utils/hash.js +0 -14
  349. package/dist/utils/hash.js.map +0 -1
  350. package/dist/utils/headers.d.ts +0 -2
  351. package/dist/utils/headers.d.ts.map +0 -1
  352. package/dist/utils/headers.js +0 -8
  353. package/dist/utils/headers.js.map +0 -1
  354. package/dist/utils/json-parse.d.ts +0 -16
  355. package/dist/utils/json-parse.d.ts.map +0 -1
  356. package/dist/utils/json-parse.js +0 -113
  357. package/dist/utils/json-parse.js.map +0 -1
  358. package/dist/utils/node-http-proxy.d.ts +0 -10
  359. package/dist/utils/node-http-proxy.d.ts.map +0 -1
  360. package/dist/utils/node-http-proxy.js +0 -97
  361. package/dist/utils/node-http-proxy.js.map +0 -1
  362. package/dist/utils/oauth/anthropic.d.ts +0 -25
  363. package/dist/utils/oauth/anthropic.d.ts.map +0 -1
  364. package/dist/utils/oauth/anthropic.js +0 -335
  365. package/dist/utils/oauth/anthropic.js.map +0 -1
  366. package/dist/utils/oauth/device-code.d.ts +0 -19
  367. package/dist/utils/oauth/device-code.d.ts.map +0 -1
  368. package/dist/utils/oauth/device-code.js +0 -55
  369. package/dist/utils/oauth/device-code.js.map +0 -1
  370. package/dist/utils/oauth/github-copilot.d.ts +0 -30
  371. package/dist/utils/oauth/github-copilot.d.ts.map +0 -1
  372. package/dist/utils/oauth/github-copilot.js +0 -268
  373. package/dist/utils/oauth/github-copilot.js.map +0 -1
  374. package/dist/utils/oauth/google-antigravity.d.ts +0 -26
  375. package/dist/utils/oauth/google-antigravity.d.ts.map +0 -1
  376. package/dist/utils/oauth/google-antigravity.js +0 -377
  377. package/dist/utils/oauth/google-antigravity.js.map +0 -1
  378. package/dist/utils/oauth/google-gemini-cli.d.ts +0 -26
  379. package/dist/utils/oauth/google-gemini-cli.d.ts.map +0 -1
  380. package/dist/utils/oauth/google-gemini-cli.js +0 -482
  381. package/dist/utils/oauth/google-gemini-cli.js.map +0 -1
  382. package/dist/utils/oauth/index.d.ts +0 -63
  383. package/dist/utils/oauth/index.d.ts.map +0 -1
  384. package/dist/utils/oauth/index.js +0 -131
  385. package/dist/utils/oauth/index.js.map +0 -1
  386. package/dist/utils/oauth/oauth-page.d.ts +0 -3
  387. package/dist/utils/oauth/oauth-page.d.ts.map +0 -1
  388. package/dist/utils/oauth/oauth-page.js +0 -105
  389. package/dist/utils/oauth/oauth-page.js.map +0 -1
  390. package/dist/utils/oauth/openai-codex.d.ts +0 -34
  391. package/dist/utils/oauth/openai-codex.d.ts.map +0 -1
  392. package/dist/utils/oauth/openai-codex.js +0 -385
  393. package/dist/utils/oauth/openai-codex.js.map +0 -1
  394. package/dist/utils/oauth/pkce.d.ts +0 -13
  395. package/dist/utils/oauth/pkce.d.ts.map +0 -1
  396. package/dist/utils/oauth/pkce.js +0 -31
  397. package/dist/utils/oauth/pkce.js.map +0 -1
  398. package/dist/utils/oauth/types.d.ts +0 -64
  399. package/dist/utils/oauth/types.d.ts.map +0 -1
  400. package/dist/utils/oauth/types.js +0 -2
  401. package/dist/utils/oauth/types.js.map +0 -1
  402. package/dist/utils/overflow.d.ts +0 -56
  403. package/dist/utils/overflow.d.ts.map +0 -1
  404. package/dist/utils/overflow.js +0 -151
  405. package/dist/utils/overflow.js.map +0 -1
  406. package/dist/utils/sanitize-unicode.d.ts +0 -22
  407. package/dist/utils/sanitize-unicode.d.ts.map +0 -1
  408. package/dist/utils/sanitize-unicode.js +0 -26
  409. package/dist/utils/sanitize-unicode.js.map +0 -1
  410. package/dist/utils/typebox-helpers.d.ts +0 -17
  411. package/dist/utils/typebox-helpers.d.ts.map +0 -1
  412. package/dist/utils/typebox-helpers.js +0 -21
  413. package/dist/utils/typebox-helpers.js.map +0 -1
  414. package/dist/utils/validation.d.ts +0 -18
  415. package/dist/utils/validation.d.ts.map +0 -1
  416. package/dist/utils/validation.js +0 -281
  417. package/dist/utils/validation.js.map +0 -1
@@ -0,0 +1,2621 @@
1
+ import { createHash } from "node:crypto";
2
+ import * as fs from "node:fs/promises";
3
+ import http2 from "node:http2";
4
+ import { $env, extractHttpStatusFromError, sanitizeText } from "@aryee337/aery-utils";
5
+ import { create, fromBinary, fromJson, type JsonValue, toBinary, toJson } from "@bufbuild/protobuf";
6
+ import { ValueSchema } from "@bufbuild/protobuf/wkt";
7
+ import { calculateCost } from "../models";
8
+ import type {
9
+ Api,
10
+ AssistantMessage,
11
+ Context,
12
+ CursorExecHandlerResult,
13
+ CursorExecHandlers,
14
+ CursorMcpCall,
15
+ CursorShellStreamCallbacks,
16
+ CursorToolResultHandler,
17
+ ImageContent,
18
+ Message,
19
+ Model,
20
+ StreamFunction,
21
+ StreamOptions,
22
+ TextContent,
23
+ ThinkingContent,
24
+ Tool,
25
+ ToolCall,
26
+ ToolResultMessage,
27
+ } from "../types";
28
+ import { normalizeSystemPrompts } from "../utils";
29
+ import { AssistantMessageEventStream } from "../utils/event-stream";
30
+ import { parseStreamingJson } from "../utils/json-parse";
31
+ import { createRequestDebugSession, isRequestDebugEnabled, type RequestDebugResponseLog } from "../utils/request-debug";
32
+ import { formatErrorMessageWithRetryAfter } from "../utils/retry-after";
33
+ import { toolWireSchema } from "../utils/schema/wire";
34
+ import type { McpToolDefinition } from "./cursor/gen/agent_pb";
35
+ import {
36
+ AgentClientMessageSchema,
37
+ AgentConversationTurnStructureSchema,
38
+ AgentRunRequestSchema,
39
+ type AgentServerMessage,
40
+ AgentServerMessageSchema,
41
+ AssistantMessageSchema,
42
+ BackgroundShellSpawnResultSchema,
43
+ ClientHeartbeatSchema,
44
+ ComputerUseResultSchema,
45
+ ConversationActionSchema,
46
+ type ConversationStateStructure,
47
+ ConversationStateStructureSchema,
48
+ ConversationStepSchema,
49
+ ConversationTurnStructureSchema,
50
+ DeleteErrorSchema,
51
+ DeleteRejectedSchema,
52
+ DeleteResultSchema,
53
+ DeleteSuccessSchema,
54
+ DiagnosticsErrorSchema,
55
+ DiagnosticsRejectedSchema,
56
+ DiagnosticsResultSchema,
57
+ DiagnosticsSuccessSchema,
58
+ ExecClientControlMessageSchema,
59
+ type ExecClientMessage,
60
+ ExecClientMessageSchema,
61
+ ExecClientStreamCloseSchema,
62
+ type ExecServerMessage,
63
+ FetchErrorSchema,
64
+ FetchResultSchema,
65
+ GetBlobResultSchema,
66
+ GrepContentMatchSchema,
67
+ GrepContentResultSchema,
68
+ GrepCountResultSchema,
69
+ GrepErrorSchema,
70
+ type GrepFileCount,
71
+ GrepFileCountSchema,
72
+ GrepFileMatchSchema,
73
+ GrepFilesResultSchema,
74
+ GrepResultSchema,
75
+ GrepSuccessSchema,
76
+ type GrepUnionResult,
77
+ GrepUnionResultSchema,
78
+ KvClientMessageSchema,
79
+ type KvServerMessage,
80
+ ListMcpResourcesExecResultSchema,
81
+ type LsDirectoryTreeNode,
82
+ type LsDirectoryTreeNode_File,
83
+ LsDirectoryTreeNode_FileSchema,
84
+ LsDirectoryTreeNodeSchema,
85
+ LsErrorSchema,
86
+ LsRejectedSchema,
87
+ LsResultSchema,
88
+ LsSuccessSchema,
89
+ McpErrorSchema,
90
+ McpImageContentSchema,
91
+ McpResultSchema,
92
+ McpSuccessSchema,
93
+ McpTextContentSchema,
94
+ McpToolDefinitionSchema,
95
+ McpToolNotFoundSchema,
96
+ McpToolResultContentItemSchema,
97
+ ModelDetailsSchema,
98
+ ReadErrorSchema,
99
+ ReadMcpResourceExecResultSchema,
100
+ ReadRejectedSchema,
101
+ ReadResultSchema,
102
+ ReadSuccessSchema,
103
+ RecordScreenResultSchema,
104
+ RequestContextResultSchema,
105
+ RequestContextSchema,
106
+ RequestContextSuccessSchema,
107
+ ResumeActionSchema,
108
+ SelectedContextSchema,
109
+ SelectedImageSchema,
110
+ SetBlobResultSchema,
111
+ type ShellArgs,
112
+ ShellFailureSchema,
113
+ ShellRejectedSchema,
114
+ type ShellResult,
115
+ ShellResultSchema,
116
+ type ShellStream,
117
+ ShellStreamExitSchema,
118
+ ShellStreamSchema,
119
+ ShellStreamStartSchema,
120
+ ShellStreamStderrSchema,
121
+ ShellStreamStdoutSchema,
122
+ ShellSuccessSchema,
123
+ UserMessageActionSchema,
124
+ UserMessageSchema,
125
+ WriteErrorSchema,
126
+ WriteRejectedSchema,
127
+ WriteResultSchema,
128
+ WriteShellStdinErrorSchema,
129
+ WriteShellStdinResultSchema,
130
+ WriteSuccessSchema,
131
+ } from "./cursor/gen/agent_pb";
132
+
133
+ export const CURSOR_API_URL = "https://api2.cursor.sh";
134
+ export const CURSOR_CLIENT_VERSION = "cli-2026.01.09-231024f";
135
+
136
+ const conversationStateCache = new Map<string, ConversationStateStructure>();
137
+ const conversationBlobStores = new Map<string, Map<string, Uint8Array>>();
138
+
139
+ export interface CursorOptions extends StreamOptions {
140
+ customSystemPrompt?: string;
141
+ conversationId?: string;
142
+ execHandlers?: CursorExecHandlers;
143
+ onToolResult?: CursorToolResultHandler;
144
+ }
145
+
146
+ const CONNECT_END_STREAM_FLAG = 0b00000010;
147
+
148
+ interface CursorLogEntry {
149
+ ts: number;
150
+ type: string;
151
+ subtype?: string;
152
+ data?: unknown;
153
+ }
154
+
155
+ async function appendCursorDebugLog(entry: CursorLogEntry): Promise<void> {
156
+ const logPath = $env.DEBUG_CURSOR_LOG;
157
+ if (!logPath) return;
158
+ try {
159
+ await fs.appendFile(logPath, `${JSON.stringify(entry, debugReplacer)}\n`);
160
+ } catch {
161
+ // Ignore debug log failures
162
+ }
163
+ }
164
+
165
+ function log(type: string, subtype?: string, data?: unknown): void {
166
+ if (!$env.DEBUG_CURSOR) return;
167
+ const normalizedData = data ? decodeLogData(data) : data;
168
+ const entry: CursorLogEntry = { ts: Date.now(), type, subtype, data: normalizedData };
169
+ const verbose = $env.DEBUG_CURSOR === "2" || $env.DEBUG_CURSOR === "verbose";
170
+ const dataStr = verbose && normalizedData ? ` ${JSON.stringify(normalizedData, debugReplacer)?.slice(0, 500)}` : "";
171
+ console.error(`[CURSOR] ${type}${subtype ? `: ${subtype}` : ""}${dataStr}`);
172
+ void appendCursorDebugLog(entry);
173
+ }
174
+
175
+ function frameConnectMessage(data: Uint8Array, flags = 0): Buffer {
176
+ const frame = Buffer.alloc(5 + data.length);
177
+ frame[0] = flags;
178
+ frame.writeUInt32BE(data.length, 1);
179
+ frame.set(data, 5);
180
+ return frame;
181
+ }
182
+
183
+ function parseConnectEndStream(data: Uint8Array): Error | null {
184
+ try {
185
+ const payload = JSON.parse(new TextDecoder().decode(data));
186
+ const error = payload?.error;
187
+ if (error) {
188
+ const code = typeof error.code === "string" ? error.code : "unknown";
189
+ const message = typeof error.message === "string" ? error.message : "Unknown error";
190
+ return new Error(`Connect error ${code}: ${message}`);
191
+ }
192
+ return null;
193
+ } catch {
194
+ return new Error("Failed to parse Connect end stream");
195
+ }
196
+ }
197
+
198
+ function debugBytes(bytes: Uint8Array, asHex: boolean): string {
199
+ if (asHex) {
200
+ return Buffer.from(bytes).toString("hex");
201
+ }
202
+ try {
203
+ const text = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
204
+ if (/^[\x20-\x7E\s]*$/.test(text)) return text;
205
+ } catch {}
206
+ return Buffer.from(bytes).toString("hex");
207
+ }
208
+
209
+ function debugReplacer(key: string, value: unknown): unknown {
210
+ if (
211
+ value instanceof Uint8Array ||
212
+ (value && typeof value === "object" && "type" in value && value.type === "Buffer")
213
+ ) {
214
+ const bytes = value instanceof Uint8Array ? value : new Uint8Array((value as any).data);
215
+ const asHex = key === "blobId" || key === "blob_id" || key.endsWith("Id") || key.endsWith("_id");
216
+ return debugBytes(bytes, asHex);
217
+ }
218
+ if (typeof value === "bigint") return value.toString();
219
+ return value;
220
+ }
221
+
222
+ function extractLogBytes(value: unknown): Uint8Array | null {
223
+ if (value instanceof Uint8Array) {
224
+ return value;
225
+ }
226
+ if (value && typeof value === "object" && "type" in value && value.type === "Buffer") {
227
+ const data = (value as { data?: number[] }).data;
228
+ if (Array.isArray(data)) {
229
+ return new Uint8Array(data);
230
+ }
231
+ }
232
+ return null;
233
+ }
234
+
235
+ function decodeMcpArgsForLog(args?: Record<string, unknown>): Record<string, unknown> | undefined {
236
+ if (!args) {
237
+ return undefined;
238
+ }
239
+ let mutated = false;
240
+ const decoded: Record<string, unknown> = {};
241
+ for (const [key, value] of Object.entries(args)) {
242
+ const bytes = extractLogBytes(value);
243
+ if (bytes) {
244
+ decoded[key] = decodeMcpArgValue(bytes);
245
+ mutated = true;
246
+ continue;
247
+ }
248
+ const normalizedValue = decodeLogData(value);
249
+ decoded[key] = normalizedValue;
250
+ if (normalizedValue !== value) {
251
+ mutated = true;
252
+ }
253
+ }
254
+ return mutated ? decoded : args;
255
+ }
256
+
257
+ function decodeLogData(value: unknown): unknown {
258
+ if (!value || typeof value !== "object") {
259
+ return value;
260
+ }
261
+ if (Array.isArray(value)) {
262
+ return value.map(entry => decodeLogData(entry));
263
+ }
264
+ const record = value as Record<string, unknown>;
265
+ const typeName = record.$typeName;
266
+ const stripTypeName = typeof typeName === "string" && typeName.startsWith("agent.v1.");
267
+
268
+ if (typeName === "agent.v1.McpArgs") {
269
+ const decodedArgs = decodeMcpArgsForLog(record.args as Record<string, unknown> | undefined);
270
+ const base = stripTypeName ? omitTypeName(record) : record;
271
+ return decodedArgs ? { ...base, args: decodedArgs } : base;
272
+ }
273
+ if (typeName === "agent.v1.McpToolCall") {
274
+ const argsRecord = record.args as Record<string, unknown> | undefined;
275
+ const decodedArgs = decodeMcpArgsForLog(argsRecord?.args as Record<string, unknown> | undefined);
276
+ const base = stripTypeName ? omitTypeName(record) : record;
277
+ if (decodedArgs && argsRecord) {
278
+ return { ...base, args: { ...argsRecord, args: decodedArgs } };
279
+ }
280
+ return base;
281
+ }
282
+
283
+ let mutated = stripTypeName;
284
+ const decoded: Record<string, unknown> = {};
285
+ for (const [key, entry] of Object.entries(record)) {
286
+ if (stripTypeName && key === "$typeName") {
287
+ continue;
288
+ }
289
+ const normalizedEntry = decodeLogData(entry);
290
+ decoded[key] = normalizedEntry;
291
+ if (normalizedEntry !== entry) {
292
+ mutated = true;
293
+ }
294
+ }
295
+ return mutated ? decoded : record;
296
+ }
297
+
298
+ function omitTypeName(record: Record<string, unknown>): Record<string, unknown> {
299
+ const { $typeName: _, ...rest } = record;
300
+ return rest;
301
+ }
302
+
303
+ export const streamCursor: StreamFunction<"cursor-agent"> = (
304
+ model: Model<"cursor-agent">,
305
+ context: Context,
306
+ options?: CursorOptions,
307
+ ): AssistantMessageEventStream => {
308
+ const stream = new AssistantMessageEventStream();
309
+
310
+ (async () => {
311
+ const startTime = Date.now();
312
+ let firstTokenTime: number | undefined;
313
+
314
+ const output: AssistantMessage = {
315
+ role: "assistant",
316
+ content: [],
317
+ api: "cursor-agent" as Api,
318
+ provider: model.provider,
319
+ model: model.id,
320
+ usage: {
321
+ input: 0,
322
+ output: 0,
323
+ cacheRead: 0,
324
+ cacheWrite: 0,
325
+ totalTokens: 0,
326
+ cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
327
+ },
328
+ stopReason: "stop",
329
+ timestamp: Date.now(),
330
+ };
331
+
332
+ let h2Client: http2.ClientHttp2Session | null = null;
333
+ let h2Request: http2.ClientHttp2Stream | null = null;
334
+ let heartbeatTimer: NodeJS.Timeout | null = null;
335
+ let debugResponseLogPromise: Promise<RequestDebugResponseLog | undefined> | undefined;
336
+
337
+ try {
338
+ const apiKey = options?.apiKey;
339
+ if (!apiKey) {
340
+ throw new Error("Cursor API key (access token) is required");
341
+ }
342
+
343
+ const conversationId = options?.conversationId ?? options?.sessionId ?? crypto.randomUUID();
344
+ const blobStore = conversationBlobStores.get(conversationId) ?? new Map<string, Uint8Array>();
345
+ conversationBlobStores.set(conversationId, blobStore);
346
+ const cachedState = conversationStateCache.get(conversationId);
347
+ const { requestBytes, conversationState } = buildGrpcRequest(model, context, options, {
348
+ conversationId,
349
+ blobStore,
350
+ conversationState: cachedState,
351
+ });
352
+ conversationStateCache.set(conversationId, conversationState);
353
+ const requestContextTools = buildMcpToolDefinitions(context.tools);
354
+
355
+ const baseUrl = model.baseUrl || CURSOR_API_URL;
356
+ const requestPath = "/agent.v1.AgentService/Run";
357
+ const requestHeaders = {
358
+ ":method": "POST",
359
+ ":path": requestPath,
360
+ "content-type": "application/connect+proto",
361
+ "connect-protocol-version": "1",
362
+ te: "trailers",
363
+ authorization: `Bearer ${apiKey}`,
364
+ "x-ghost-mode": "true",
365
+ "x-cursor-client-version": CURSOR_CLIENT_VERSION,
366
+ "x-cursor-client-type": "cli",
367
+ "x-request-id": crypto.randomUUID(),
368
+ };
369
+ const debugSession = isRequestDebugEnabled()
370
+ ? await createRequestDebugSession({
371
+ protocol: "http2",
372
+ method: "POST",
373
+ url: new URL(requestPath, baseUrl).toString(),
374
+ headers: requestHeaders,
375
+ bodyBase64: Buffer.from(requestBytes).toString("base64"),
376
+ })
377
+ : undefined;
378
+
379
+ h2Client = http2.connect(baseUrl);
380
+
381
+ h2Request = h2Client.request(requestHeaders);
382
+
383
+ stream.push({ type: "start", partial: output });
384
+
385
+ let pendingBuffer = Buffer.alloc(0);
386
+ let endStreamError: Error | null = null;
387
+ let currentTextBlock: (TextContent & { index: number }) | null = null;
388
+ let currentThinkingBlock: (ThinkingContent & { index: number }) | null = null;
389
+ let currentToolCall: ToolCallState | null = null;
390
+ const usageState: UsageState = { sawTokenDelta: false };
391
+
392
+ const state: BlockState = {
393
+ get currentTextBlock() {
394
+ return currentTextBlock;
395
+ },
396
+ get currentThinkingBlock() {
397
+ return currentThinkingBlock;
398
+ },
399
+ get currentToolCall() {
400
+ return currentToolCall;
401
+ },
402
+ get firstTokenTime() {
403
+ return firstTokenTime;
404
+ },
405
+ setTextBlock: b => {
406
+ currentTextBlock = b;
407
+ },
408
+ setThinkingBlock: b => {
409
+ currentThinkingBlock = b;
410
+ },
411
+ setToolCall: t => {
412
+ currentToolCall = t;
413
+ },
414
+ setFirstTokenTime: () => {
415
+ if (!firstTokenTime) firstTokenTime = Date.now();
416
+ },
417
+ };
418
+
419
+ const onConversationCheckpoint = (checkpoint: ConversationStateStructure) => {
420
+ conversationStateCache.set(conversationId, checkpoint);
421
+ };
422
+
423
+ let resolveH2: (() => void) | undefined;
424
+
425
+ h2Request.on("response", headers => {
426
+ debugResponseLogPromise = debugSession?.openResponseLog(
427
+ `HTTP/2 ${headers[":status"] ?? ""}`.trim(),
428
+ headers,
429
+ );
430
+ });
431
+
432
+ h2Request.on("data", (chunk: Buffer) => {
433
+ if (debugResponseLogPromise) {
434
+ void debugResponseLogPromise.then(log => {
435
+ log?.write(chunk);
436
+ });
437
+ }
438
+ pendingBuffer = Buffer.concat([pendingBuffer, chunk]);
439
+
440
+ while (pendingBuffer.length >= 5) {
441
+ const flags = pendingBuffer[0];
442
+ const msgLen = pendingBuffer.readUInt32BE(1);
443
+ if (pendingBuffer.length < 5 + msgLen) break;
444
+
445
+ const messageBytes = pendingBuffer.subarray(5, 5 + msgLen);
446
+ pendingBuffer = pendingBuffer.subarray(5 + msgLen);
447
+
448
+ if (flags & CONNECT_END_STREAM_FLAG) {
449
+ const endError = parseConnectEndStream(messageBytes);
450
+ if (endError) {
451
+ endStreamError = endError;
452
+ h2Request?.close();
453
+ }
454
+ continue;
455
+ }
456
+
457
+ try {
458
+ const serverMessage = fromBinary(AgentServerMessageSchema, messageBytes);
459
+ const isTurnEnded =
460
+ serverMessage.message.case === "interactionUpdate" &&
461
+ serverMessage.message.value.message?.case === "turnEnded";
462
+ void handleServerMessage(
463
+ serverMessage,
464
+ output,
465
+ stream,
466
+ state,
467
+ blobStore,
468
+ h2Request!,
469
+ options?.execHandlers,
470
+ options?.onToolResult,
471
+ usageState,
472
+ requestContextTools,
473
+ onConversationCheckpoint,
474
+ ).catch(error => {
475
+ log("error", "handleServerMessage", { error: String(error) });
476
+ });
477
+
478
+ // Resolve only on explicit turnEnded. stopReason defaults to "stop"
479
+ // and is not a reliable signal for stream completion.
480
+ if (isTurnEnded && resolveH2) {
481
+ const r = resolveH2;
482
+ resolveH2 = undefined;
483
+ r();
484
+ }
485
+ } catch (e) {
486
+ log("error", "parseServerMessage", { error: String(e) });
487
+ }
488
+ }
489
+ });
490
+
491
+ h2Request.write(frameConnectMessage(requestBytes));
492
+
493
+ const sendHeartbeat = () => {
494
+ if (!h2Request || h2Request.closed) {
495
+ return;
496
+ }
497
+ const heartbeatMessage = create(AgentClientMessageSchema, {
498
+ message: { case: "clientHeartbeat", value: create(ClientHeartbeatSchema, {}) },
499
+ });
500
+ const heartbeatBytes = toBinary(AgentClientMessageSchema, heartbeatMessage);
501
+ h2Request.write(frameConnectMessage(heartbeatBytes));
502
+ };
503
+
504
+ heartbeatTimer = setInterval(sendHeartbeat, 5000);
505
+
506
+ await new Promise<void>((resolve, reject) => {
507
+ resolveH2 = resolve;
508
+
509
+ const closeDebugLog = async (): Promise<void> => {
510
+ const log = await debugResponseLogPromise;
511
+ await log?.close();
512
+ };
513
+
514
+ h2Request!.on("trailers", trailers => {
515
+ const status = trailers["grpc-status"];
516
+ const msg = trailers["grpc-message"];
517
+ if (status && status !== "0") {
518
+ void closeDebugLog().finally(() => {
519
+ reject(new Error(`gRPC error ${status}: ${decodeURIComponent(String(msg || ""))}`));
520
+ });
521
+ }
522
+ });
523
+
524
+ h2Request!.on("end", () => {
525
+ resolveH2 = undefined;
526
+ void closeDebugLog()
527
+ .then(() => {
528
+ if (endStreamError) {
529
+ reject(endStreamError);
530
+ return;
531
+ }
532
+ resolve();
533
+ })
534
+ .catch(reject);
535
+ });
536
+
537
+ h2Request!.on("error", error => {
538
+ void closeDebugLog().finally(() => reject(error));
539
+ });
540
+
541
+ if (options?.signal) {
542
+ options.signal.addEventListener("abort", () => {
543
+ h2Request?.close();
544
+ void closeDebugLog().finally(() => {
545
+ reject(new Error("Request was aborted"));
546
+ });
547
+ });
548
+ }
549
+ });
550
+
551
+ if (state.currentTextBlock) {
552
+ const idx = output.content.indexOf(state.currentTextBlock);
553
+ stream.push({
554
+ type: "text_end",
555
+ contentIndex: idx,
556
+ content: state.currentTextBlock.text,
557
+ partial: output,
558
+ });
559
+ }
560
+ if (state.currentThinkingBlock) {
561
+ const idx = output.content.indexOf(state.currentThinkingBlock);
562
+ stream.push({
563
+ type: "thinking_end",
564
+ contentIndex: idx,
565
+ content: state.currentThinkingBlock.thinking,
566
+ partial: output,
567
+ });
568
+ }
569
+ if (state.currentToolCall) {
570
+ const idx = output.content.indexOf(state.currentToolCall);
571
+ state.currentToolCall.arguments = parseStreamingJson(state.currentToolCall.partialJson);
572
+ delete (state.currentToolCall as any).partialJson;
573
+ delete (state.currentToolCall as any).index;
574
+ stream.push({
575
+ type: "toolcall_end",
576
+ contentIndex: idx,
577
+ toolCall: state.currentToolCall,
578
+ partial: output,
579
+ });
580
+ }
581
+
582
+ calculateCost(model, output.usage);
583
+
584
+ output.duration = Date.now() - startTime;
585
+ if (firstTokenTime) output.ttft = firstTokenTime - startTime;
586
+ stream.push({
587
+ type: "done",
588
+ reason: output.stopReason as "stop" | "length" | "toolUse",
589
+ message: output,
590
+ });
591
+ stream.end();
592
+ } catch (error) {
593
+ output.stopReason = options?.signal?.aborted ? "aborted" : "error";
594
+ output.errorStatus = extractHttpStatusFromError(error);
595
+ output.errorMessage = formatErrorMessageWithRetryAfter(error);
596
+ output.duration = Date.now() - startTime;
597
+ if (firstTokenTime) output.ttft = firstTokenTime - startTime;
598
+ stream.push({ type: "error", reason: output.stopReason, error: output });
599
+ stream.end();
600
+ } finally {
601
+ const log = await debugResponseLogPromise;
602
+ await log?.close();
603
+ if (heartbeatTimer) {
604
+ clearInterval(heartbeatTimer);
605
+ heartbeatTimer = null;
606
+ }
607
+ h2Request?.close();
608
+ h2Client?.close();
609
+ }
610
+ })();
611
+
612
+ return stream;
613
+ };
614
+
615
+ type ToolCallState = ToolCall & { index: number; partialJson?: string; kind: "mcp" | "todo_write" };
616
+
617
+ interface BlockState {
618
+ currentTextBlock: (TextContent & { index: number }) | null;
619
+ currentThinkingBlock: (ThinkingContent & { index: number }) | null;
620
+ currentToolCall: ToolCallState | null;
621
+ firstTokenTime: number | undefined;
622
+ setTextBlock: (b: (TextContent & { index: number }) | null) => void;
623
+ setThinkingBlock: (b: (ThinkingContent & { index: number }) | null) => void;
624
+ setToolCall: (t: ToolCallState | null) => void;
625
+ setFirstTokenTime: () => void;
626
+ }
627
+
628
+ interface UsageState {
629
+ sawTokenDelta: boolean;
630
+ }
631
+
632
+ async function handleServerMessage(
633
+ msg: AgentServerMessage,
634
+ output: AssistantMessage,
635
+ stream: AssistantMessageEventStream,
636
+ state: BlockState,
637
+ blobStore: Map<string, Uint8Array>,
638
+ h2Request: http2.ClientHttp2Stream,
639
+ execHandlers: CursorExecHandlers | undefined,
640
+ onToolResult: CursorToolResultHandler | undefined,
641
+ usageState: UsageState,
642
+ requestContextTools: McpToolDefinition[],
643
+ onConversationCheckpoint?: (checkpoint: ConversationStateStructure) => void,
644
+ ): Promise<void> {
645
+ const msgCase = msg.message.case;
646
+
647
+ log("serverMessage", msgCase, msg.message.value);
648
+
649
+ if (msgCase === "interactionUpdate") {
650
+ processInteractionUpdate(msg.message.value, output, stream, state, usageState);
651
+ } else if (msgCase === "kvServerMessage") {
652
+ handleKvServerMessage(msg.message.value as KvServerMessage, blobStore, h2Request);
653
+ } else if (msgCase === "execServerMessage") {
654
+ await handleExecServerMessage(
655
+ msg.message.value as ExecServerMessage,
656
+ h2Request,
657
+ execHandlers,
658
+ onToolResult,
659
+ requestContextTools,
660
+ );
661
+ } else if (msgCase === "conversationCheckpointUpdate") {
662
+ handleConversationCheckpointUpdate(msg.message.value, output, usageState, onConversationCheckpoint);
663
+ }
664
+ }
665
+
666
+ function handleKvServerMessage(
667
+ kvMsg: KvServerMessage,
668
+ blobStore: Map<string, Uint8Array>,
669
+ h2Request: http2.ClientHttp2Stream,
670
+ ): void {
671
+ const kvCase = kvMsg.message.case;
672
+
673
+ if (kvCase === "getBlobArgs") {
674
+ const blobId = kvMsg.message.value.blobId;
675
+ const blobIdKey = Buffer.from(blobId).toString("hex");
676
+
677
+ const blobData = blobStore.get(blobIdKey);
678
+
679
+ const response = create(KvClientMessageSchema, {
680
+ id: kvMsg.id,
681
+ message: {
682
+ case: "getBlobResult",
683
+ value: create(GetBlobResultSchema, blobData ? { blobData } : {}),
684
+ },
685
+ });
686
+
687
+ const kvClientMessage = create(AgentClientMessageSchema, {
688
+ message: { case: "kvClientMessage", value: response },
689
+ });
690
+
691
+ const responseBytes = toBinary(AgentClientMessageSchema, kvClientMessage);
692
+ h2Request.write(frameConnectMessage(responseBytes));
693
+
694
+ log("kvClient", "getBlobResult", { blobId: blobIdKey.slice(0, 40) });
695
+ } else if (kvCase === "setBlobArgs") {
696
+ const { blobId, blobData } = kvMsg.message.value;
697
+ const blobIdKey = Buffer.from(blobId).toString("hex");
698
+ blobStore.set(blobIdKey, blobData);
699
+
700
+ const response = create(KvClientMessageSchema, {
701
+ id: kvMsg.id,
702
+ message: {
703
+ case: "setBlobResult",
704
+ value: create(SetBlobResultSchema, {}),
705
+ },
706
+ });
707
+
708
+ const kvClientMessage = create(AgentClientMessageSchema, {
709
+ message: { case: "kvClientMessage", value: response },
710
+ });
711
+
712
+ const responseBytes = toBinary(AgentClientMessageSchema, kvClientMessage);
713
+ h2Request.write(frameConnectMessage(responseBytes));
714
+
715
+ log("kvClient", "setBlobResult", { blobId: blobIdKey.slice(0, 40) });
716
+ }
717
+ }
718
+
719
+ function sendShellStreamEvent(
720
+ h2Request: http2.ClientHttp2Stream,
721
+ execMsg: ExecServerMessage,
722
+ event: ShellStream["event"],
723
+ ): void {
724
+ sendExecClientMessage(h2Request, execMsg, "shellStream", create(ShellStreamSchema, { event }));
725
+ }
726
+
727
+ function sanitizeShellExecResult(execResult: ShellResult): ShellResult {
728
+ const result = execResult.result;
729
+ if (!result) return execResult;
730
+
731
+ switch (result.case) {
732
+ case "success":
733
+ case "failure": {
734
+ const value = result.value;
735
+ return {
736
+ ...execResult,
737
+ result: {
738
+ case: result.case,
739
+ value: {
740
+ ...value,
741
+ stdout: value.stdout ? sanitizeText(value.stdout) : value.stdout,
742
+ stderr: value.stderr ? sanitizeText(value.stderr) : value.stderr,
743
+ },
744
+ },
745
+ } as ShellResult;
746
+ }
747
+ default:
748
+ return execResult;
749
+ }
750
+ }
751
+
752
+ async function handleShellStreamArgs(
753
+ args: ShellArgs,
754
+ execMsg: ExecServerMessage,
755
+ h2Request: http2.ClientHttp2Stream,
756
+ execHandlers: CursorExecHandlers | undefined,
757
+ onToolResult: CursorToolResultHandler | undefined,
758
+ ): Promise<void> {
759
+ const normalizedWorkingDirectory = args.workingDirectory || process.cwd();
760
+ const normalizedArgs: ShellArgs = { ...args, workingDirectory: normalizedWorkingDirectory };
761
+ const startTs = Date.now();
762
+ log("shellStream", "start", {
763
+ command: (args as any).command,
764
+ workingDirectory: normalizedWorkingDirectory,
765
+ execId: execMsg.execId,
766
+ hasExecHandlers: !!execHandlers,
767
+ hasShell: !!execHandlers?.shell,
768
+ hasShellStream: !!execHandlers?.shellStream,
769
+ });
770
+
771
+ sendShellStreamEvent(h2Request, execMsg, { case: "start", value: create(ShellStreamStartSchema, {}) });
772
+
773
+ // Buffer for incomplete ANSI sequences across chunks
774
+ let stdoutBuffer = "";
775
+ let stderrBuffer = "";
776
+
777
+ const incompleteEscapeRegex = /\x1b(|\[|\[\d*|\[\?|\[\?\d*|\]\d*;?)$/;
778
+
779
+ const flushStdout = () => {
780
+ if (stdoutBuffer) {
781
+ let safeEnd = stdoutBuffer.length;
782
+ const match = stdoutBuffer.match(incompleteEscapeRegex);
783
+ if (match && match[0].length > 0) {
784
+ safeEnd = stdoutBuffer.length - match[0].length;
785
+ }
786
+ const toSend = stdoutBuffer.slice(0, safeEnd);
787
+ const remaining = stdoutBuffer.slice(safeEnd);
788
+ if (toSend) {
789
+ sendShellStreamEvent(h2Request, execMsg, {
790
+ case: "stdout",
791
+ value: create(ShellStreamStdoutSchema, { data: sanitizeText(toSend) }),
792
+ });
793
+ }
794
+ stdoutBuffer = remaining;
795
+ }
796
+ };
797
+
798
+ const flushStderr = () => {
799
+ if (stderrBuffer) {
800
+ let safeEnd = stderrBuffer.length;
801
+ const match = stderrBuffer.match(incompleteEscapeRegex);
802
+ if (match && match[0].length > 0) {
803
+ safeEnd = stderrBuffer.length - match[0].length;
804
+ }
805
+ const toSend = stderrBuffer.slice(0, safeEnd);
806
+ const remaining = stderrBuffer.slice(safeEnd);
807
+ if (toSend) {
808
+ sendShellStreamEvent(h2Request, execMsg, {
809
+ case: "stderr",
810
+ value: create(ShellStreamStderrSchema, { data: sanitizeText(toSend) }),
811
+ });
812
+ }
813
+ stderrBuffer = remaining;
814
+ }
815
+ };
816
+
817
+ let stdoutFlushTimer: NodeJS.Timeout | null = null;
818
+ let stderrFlushTimer: NodeJS.Timeout | null = null;
819
+
820
+ const scheduleStdoutFlush = () => {
821
+ if (!stdoutFlushTimer) {
822
+ stdoutFlushTimer = setTimeout(() => {
823
+ stdoutFlushTimer = null;
824
+ flushStdout();
825
+ }, 100);
826
+ }
827
+ };
828
+
829
+ const scheduleStderrFlush = () => {
830
+ if (!stderrFlushTimer) {
831
+ stderrFlushTimer = setTimeout(() => {
832
+ stderrFlushTimer = null;
833
+ flushStderr();
834
+ }, 100);
835
+ }
836
+ };
837
+
838
+ const streamCallbacks: CursorShellStreamCallbacks = {
839
+ onStdout(data: string) {
840
+ stdoutBuffer += data;
841
+ if (stdoutBuffer.includes("\n") || stdoutBuffer.length > 4096) {
842
+ if (stdoutFlushTimer) {
843
+ clearTimeout(stdoutFlushTimer);
844
+ stdoutFlushTimer = null;
845
+ }
846
+ flushStdout();
847
+ } else {
848
+ scheduleStdoutFlush();
849
+ }
850
+ },
851
+ onStderr(data: string) {
852
+ stderrBuffer += data;
853
+ if (stderrBuffer.includes("\n") || stderrBuffer.length > 4096) {
854
+ if (stderrFlushTimer) {
855
+ clearTimeout(stderrFlushTimer);
856
+ stderrFlushTimer = null;
857
+ }
858
+ flushStderr();
859
+ } else {
860
+ scheduleStderrFlush();
861
+ }
862
+ },
863
+ };
864
+
865
+ // Prefer the streaming handler — it forwards output chunks in real time.
866
+ // Falls back to the batch shell handler otherwise.
867
+ const streamHandler = execHandlers?.shellStream?.bind(execHandlers);
868
+ const batchHandler = execHandlers?.shell?.bind(execHandlers);
869
+ const handler = streamHandler ? (shellArgs: ShellArgs) => streamHandler(shellArgs, streamCallbacks) : batchHandler;
870
+
871
+ const { execResult } = await resolveExecHandler(
872
+ args as any,
873
+ handler as typeof batchHandler,
874
+ onToolResult,
875
+ toolResult => buildShellResultFromToolResult(normalizedArgs as any, toolResult),
876
+ reason =>
877
+ buildShellRejectedResult((normalizedArgs as any).command, (normalizedArgs as any).workingDirectory, reason),
878
+ error =>
879
+ buildShellFailureResult((normalizedArgs as any).command, (normalizedArgs as any).workingDirectory, error),
880
+ );
881
+
882
+ // When using the batch handler (no shellStream), send buffered stdout/stderr
883
+ // after execution completes. With shellStream these were already sent in real time.
884
+ const sendBufferedOutput = !streamHandler;
885
+ const sanitizedExecResult = sanitizeShellExecResult(execResult);
886
+
887
+ // Flush any remaining buffered output before sending results
888
+ if (stdoutFlushTimer) clearTimeout(stdoutFlushTimer);
889
+ if (stderrFlushTimer) clearTimeout(stderrFlushTimer);
890
+ flushStdout();
891
+ flushStderr();
892
+
893
+ sendShellStreamExitFromResult(h2Request, execMsg, sanitizedExecResult, sendBufferedOutput);
894
+ // Cursor can keep the turn pending when it receives only stream deltas.
895
+ // Send the final structured shellResult as completion acknowledgement.
896
+ sendExecClientMessage(h2Request, execMsg, "shellResult", sanitizedExecResult);
897
+ sendExecClientStreamClose(h2Request, execMsg);
898
+
899
+ log("shellStream", "done", { elapsed: Date.now() - startTs });
900
+ }
901
+
902
+ function sendShellStreamExitFromResult(
903
+ h2Request: http2.ClientHttp2Stream,
904
+ execMsg: ExecServerMessage,
905
+ execResult: ShellResult,
906
+ sendBufferedOutput: boolean,
907
+ ): void {
908
+ const result = execResult.result;
909
+ switch (result.case) {
910
+ case "success": {
911
+ const value = result.value;
912
+ if (sendBufferedOutput) {
913
+ if (value.stdout) {
914
+ sendShellStreamEvent(h2Request, execMsg, {
915
+ case: "stdout",
916
+ value: create(ShellStreamStdoutSchema, { data: sanitizeText(value.stdout) }),
917
+ });
918
+ }
919
+ if (value.stderr) {
920
+ sendShellStreamEvent(h2Request, execMsg, {
921
+ case: "stderr",
922
+ value: create(ShellStreamStderrSchema, { data: sanitizeText(value.stderr) }),
923
+ });
924
+ }
925
+ }
926
+ sendShellStreamEvent(h2Request, execMsg, {
927
+ case: "exit",
928
+ value: create(ShellStreamExitSchema, {
929
+ code: value.exitCode,
930
+ cwd: value.workingDirectory,
931
+ aborted: false,
932
+ }),
933
+ });
934
+ return;
935
+ }
936
+ case "failure": {
937
+ const value = result.value;
938
+ if (sendBufferedOutput) {
939
+ if (value.stdout) {
940
+ sendShellStreamEvent(h2Request, execMsg, {
941
+ case: "stdout",
942
+ value: create(ShellStreamStdoutSchema, { data: sanitizeText(value.stdout) }),
943
+ });
944
+ }
945
+ if (value.stderr) {
946
+ sendShellStreamEvent(h2Request, execMsg, {
947
+ case: "stderr",
948
+ value: create(ShellStreamStderrSchema, { data: sanitizeText(value.stderr) }),
949
+ });
950
+ }
951
+ }
952
+ sendShellStreamEvent(h2Request, execMsg, {
953
+ case: "exit",
954
+ value: create(ShellStreamExitSchema, {
955
+ code: value.exitCode,
956
+ cwd: value.workingDirectory,
957
+ aborted: value.aborted,
958
+ abortReason: value.abortReason,
959
+ }),
960
+ });
961
+ return;
962
+ }
963
+ case "rejected": {
964
+ sendShellStreamEvent(h2Request, execMsg, { case: "rejected", value: result.value });
965
+ sendShellStreamEvent(h2Request, execMsg, {
966
+ case: "exit",
967
+ value: create(ShellStreamExitSchema, {
968
+ code: 1,
969
+ cwd: result.value.workingDirectory,
970
+ aborted: false,
971
+ }),
972
+ });
973
+ return;
974
+ }
975
+ case "timeout": {
976
+ const value = result.value;
977
+ sendShellStreamEvent(h2Request, execMsg, {
978
+ case: "stderr",
979
+ value: create(ShellStreamStderrSchema, {
980
+ data: `Command timed out after ${value.timeoutMs}ms`,
981
+ }),
982
+ });
983
+ sendShellStreamEvent(h2Request, execMsg, {
984
+ case: "exit",
985
+ value: create(ShellStreamExitSchema, {
986
+ code: 1,
987
+ cwd: value.workingDirectory,
988
+ aborted: true,
989
+ }),
990
+ });
991
+ return;
992
+ }
993
+ case "permissionDenied": {
994
+ sendShellStreamEvent(h2Request, execMsg, { case: "permissionDenied", value: result.value });
995
+ sendShellStreamEvent(h2Request, execMsg, {
996
+ case: "exit",
997
+ value: create(ShellStreamExitSchema, {
998
+ code: 1,
999
+ cwd: result.value.workingDirectory,
1000
+ aborted: false,
1001
+ }),
1002
+ });
1003
+ return;
1004
+ }
1005
+ default:
1006
+ return;
1007
+ }
1008
+ }
1009
+
1010
+ async function handleExecServerMessage(
1011
+ execMsg: ExecServerMessage,
1012
+ h2Request: http2.ClientHttp2Stream,
1013
+ execHandlers: CursorExecHandlers | undefined,
1014
+ onToolResult: CursorToolResultHandler | undefined,
1015
+ requestContextTools: McpToolDefinition[],
1016
+ ): Promise<void> {
1017
+ const execCase = execMsg.message.case;
1018
+ log("exec", "dispatch", { execCase, execId: execMsg.execId, hasHandlers: !!execHandlers });
1019
+ if (execCase === "requestContextArgs") {
1020
+ const requestContext = create(RequestContextSchema, {
1021
+ rules: [],
1022
+ repositoryInfo: [],
1023
+ tools: requestContextTools,
1024
+ gitRepos: [],
1025
+ projectLayouts: [],
1026
+ mcpInstructions: [],
1027
+ fileContents: {},
1028
+ customSubagents: [],
1029
+ });
1030
+
1031
+ const requestContextResult = create(RequestContextResultSchema, {
1032
+ result: {
1033
+ case: "success",
1034
+ value: create(RequestContextSuccessSchema, { requestContext }),
1035
+ },
1036
+ });
1037
+
1038
+ sendExecClientMessage(h2Request, execMsg, "requestContextResult", requestContextResult);
1039
+ log("execClient", "requestContextResult");
1040
+ return;
1041
+ }
1042
+
1043
+ if (!execCase) {
1044
+ return;
1045
+ }
1046
+
1047
+ switch (execCase) {
1048
+ case "readArgs": {
1049
+ const args = execMsg.message.value;
1050
+ const { execResult } = await resolveExecHandler(
1051
+ args,
1052
+ execHandlers?.read?.bind(execHandlers),
1053
+ onToolResult,
1054
+ toolResult => buildReadResultFromToolResult(args.path, toolResult),
1055
+ reason => buildReadRejectedResult(args.path, reason),
1056
+ error => buildReadErrorResult(args.path, error),
1057
+ );
1058
+ sendExecClientMessage(h2Request, execMsg, "readResult", execResult);
1059
+ return;
1060
+ }
1061
+ case "lsArgs": {
1062
+ const args = execMsg.message.value;
1063
+ const { execResult } = await resolveExecHandler(
1064
+ args,
1065
+ execHandlers?.ls?.bind(execHandlers),
1066
+ onToolResult,
1067
+ toolResult => buildLsResultFromToolResult(args.path, toolResult),
1068
+ reason => buildLsRejectedResult(args.path, reason),
1069
+ error => buildLsErrorResult(args.path, error),
1070
+ );
1071
+ sendExecClientMessage(h2Request, execMsg, "lsResult", execResult);
1072
+ return;
1073
+ }
1074
+ case "grepArgs": {
1075
+ const args = execMsg.message.value;
1076
+ const { execResult } = await resolveExecHandler(
1077
+ args,
1078
+ execHandlers?.grep?.bind(execHandlers),
1079
+ onToolResult,
1080
+ toolResult => buildGrepResultFromToolResult(args, toolResult),
1081
+ reason => buildGrepErrorResult(reason),
1082
+ error => buildGrepErrorResult(error),
1083
+ );
1084
+ sendExecClientMessage(h2Request, execMsg, "grepResult", execResult);
1085
+ return;
1086
+ }
1087
+ case "writeArgs": {
1088
+ const args = execMsg.message.value;
1089
+ const { execResult } = await resolveExecHandler(
1090
+ args,
1091
+ execHandlers?.write?.bind(execHandlers),
1092
+ onToolResult,
1093
+ toolResult =>
1094
+ buildWriteResultFromToolResult(
1095
+ {
1096
+ path: args.path,
1097
+ fileText: args.fileText,
1098
+ fileBytes: args.fileBytes,
1099
+ returnFileContentAfterWrite: args.returnFileContentAfterWrite,
1100
+ },
1101
+ toolResult,
1102
+ ),
1103
+ reason => buildWriteRejectedResult(args.path, reason),
1104
+ error => buildWriteErrorResult(args.path, error),
1105
+ );
1106
+ sendExecClientMessage(h2Request, execMsg, "writeResult", execResult);
1107
+ return;
1108
+ }
1109
+ case "deleteArgs": {
1110
+ const args = execMsg.message.value;
1111
+ const { execResult } = await resolveExecHandler(
1112
+ args,
1113
+ execHandlers?.delete?.bind(execHandlers),
1114
+ onToolResult,
1115
+ toolResult => buildDeleteResultFromToolResult(args.path, toolResult),
1116
+ reason => buildDeleteRejectedResult(args.path, reason),
1117
+ error => buildDeleteErrorResult(args.path, error),
1118
+ );
1119
+ sendExecClientMessage(h2Request, execMsg, "deleteResult", execResult);
1120
+ return;
1121
+ }
1122
+ case "shellArgs": {
1123
+ const args = execMsg.message.value;
1124
+ const normalizedArgs: ShellArgs = { ...args, workingDirectory: args.workingDirectory || process.cwd() };
1125
+ const { execResult } = await resolveExecHandler(
1126
+ args,
1127
+ execHandlers?.shell?.bind(execHandlers),
1128
+ onToolResult,
1129
+ toolResult => buildShellResultFromToolResult(normalizedArgs, toolResult),
1130
+ reason => buildShellRejectedResult(normalizedArgs.command, normalizedArgs.workingDirectory, reason),
1131
+ error => buildShellFailureResult(normalizedArgs.command, normalizedArgs.workingDirectory, error),
1132
+ );
1133
+ const sanitizedExecResult = sanitizeShellExecResult(execResult);
1134
+ sendExecClientMessage(h2Request, execMsg, "shellResult", sanitizedExecResult);
1135
+ return;
1136
+ }
1137
+ case "shellStreamArgs": {
1138
+ const args = execMsg.message.value;
1139
+ await handleShellStreamArgs(args, execMsg, h2Request, execHandlers, onToolResult);
1140
+ return;
1141
+ }
1142
+ case "backgroundShellSpawnArgs": {
1143
+ const args = execMsg.message.value;
1144
+ const execResult = create(BackgroundShellSpawnResultSchema, {
1145
+ result: {
1146
+ case: "rejected",
1147
+ value: create(ShellRejectedSchema, {
1148
+ command: args.command,
1149
+ workingDirectory: args.workingDirectory,
1150
+ reason: "Not implemented",
1151
+ isReadonly: false,
1152
+ }),
1153
+ },
1154
+ });
1155
+ sendExecClientMessage(h2Request, execMsg, "backgroundShellSpawnResult", execResult);
1156
+ return;
1157
+ }
1158
+ case "writeShellStdinArgs": {
1159
+ const execResult = create(WriteShellStdinResultSchema, {
1160
+ result: {
1161
+ case: "error",
1162
+ value: create(WriteShellStdinErrorSchema, {
1163
+ error: "Not implemented",
1164
+ }),
1165
+ },
1166
+ });
1167
+ sendExecClientMessage(h2Request, execMsg, "writeShellStdinResult", execResult);
1168
+ return;
1169
+ }
1170
+ case "fetchArgs": {
1171
+ const args = execMsg.message.value;
1172
+ const execResult = create(FetchResultSchema, {
1173
+ result: {
1174
+ case: "error",
1175
+ value: create(FetchErrorSchema, {
1176
+ url: args.url,
1177
+ error: "Not implemented",
1178
+ }),
1179
+ },
1180
+ });
1181
+ sendExecClientMessage(h2Request, execMsg, "fetchResult", execResult);
1182
+ return;
1183
+ }
1184
+ case "diagnosticsArgs": {
1185
+ const args = execMsg.message.value;
1186
+ const { execResult } = await resolveExecHandler(
1187
+ args,
1188
+ execHandlers?.diagnostics?.bind(execHandlers),
1189
+ onToolResult,
1190
+ toolResult => buildDiagnosticsResultFromToolResult(args.path, toolResult),
1191
+ reason => buildDiagnosticsRejectedResult(args.path, reason),
1192
+ error => buildDiagnosticsErrorResult(args.path, error),
1193
+ );
1194
+ sendExecClientMessage(h2Request, execMsg, "diagnosticsResult", execResult);
1195
+ return;
1196
+ }
1197
+ case "mcpArgs": {
1198
+ const args = execMsg.message.value;
1199
+ const mcpCall = decodeMcpCall(args);
1200
+ const { execResult } = await resolveExecHandler(
1201
+ mcpCall,
1202
+ execHandlers?.mcp?.bind(execHandlers),
1203
+ onToolResult,
1204
+ toolResult => buildMcpResultFromToolResult(mcpCall, toolResult),
1205
+ _reason => buildMcpToolNotFoundResult(mcpCall),
1206
+ error => buildMcpErrorResult(error),
1207
+ );
1208
+ sendExecClientMessage(h2Request, execMsg, "mcpResult", execResult);
1209
+ return;
1210
+ }
1211
+ case "listMcpResourcesExecArgs": {
1212
+ const execResult = create(ListMcpResourcesExecResultSchema, {});
1213
+ sendExecClientMessage(h2Request, execMsg, "listMcpResourcesExecResult", execResult);
1214
+ return;
1215
+ }
1216
+ case "readMcpResourceExecArgs": {
1217
+ const execResult = create(ReadMcpResourceExecResultSchema, {});
1218
+ sendExecClientMessage(h2Request, execMsg, "readMcpResourceExecResult", execResult);
1219
+ return;
1220
+ }
1221
+ case "recordScreenArgs": {
1222
+ const execResult = create(RecordScreenResultSchema, {});
1223
+ sendExecClientMessage(h2Request, execMsg, "recordScreenResult", execResult);
1224
+ return;
1225
+ }
1226
+ case "computerUseArgs": {
1227
+ const execResult = create(ComputerUseResultSchema, {});
1228
+ sendExecClientMessage(h2Request, execMsg, "computerUseResult", execResult);
1229
+ return;
1230
+ }
1231
+ default: {
1232
+ log("warn", "unhandledExecMessage", { execCase });
1233
+ // Send a bare ExecClientMessage (id + execId only, no typed result) so the
1234
+ // server gets an acknowledgement and doesn't hang waiting forever.
1235
+ const ack = create(ExecClientMessageSchema, {
1236
+ id: execMsg.id,
1237
+ execId: execMsg.execId,
1238
+ });
1239
+ const clientMessage = create(AgentClientMessageSchema, {
1240
+ message: { case: "execClientMessage", value: ack },
1241
+ });
1242
+ h2Request.write(frameConnectMessage(toBinary(AgentClientMessageSchema, clientMessage)));
1243
+ }
1244
+ }
1245
+ }
1246
+
1247
+ function sendExecClientMessage<T>(
1248
+ h2Request: http2.ClientHttp2Stream,
1249
+ execMsg: ExecServerMessage,
1250
+ messageCase: ExecClientMessage["message"]["case"],
1251
+ value: T,
1252
+ ): void {
1253
+ const execClientMessage = create(ExecClientMessageSchema, {
1254
+ id: execMsg.id,
1255
+ execId: execMsg.execId,
1256
+ message: {
1257
+ case: messageCase,
1258
+ value: value as any,
1259
+ },
1260
+ });
1261
+
1262
+ const clientMessage = create(AgentClientMessageSchema, {
1263
+ message: { case: "execClientMessage", value: execClientMessage },
1264
+ });
1265
+
1266
+ const responseBytes = toBinary(AgentClientMessageSchema, clientMessage);
1267
+ h2Request.write(frameConnectMessage(responseBytes));
1268
+
1269
+ log("execClientMessage", messageCase, value);
1270
+ }
1271
+
1272
+ function sendExecClientStreamClose(h2Request: http2.ClientHttp2Stream, execMsg: ExecServerMessage): void {
1273
+ const closeMessage = create(ExecClientControlMessageSchema, {
1274
+ message: {
1275
+ case: "streamClose",
1276
+ value: create(ExecClientStreamCloseSchema, {
1277
+ id: execMsg.id,
1278
+ }),
1279
+ },
1280
+ });
1281
+ const clientMessage = create(AgentClientMessageSchema, {
1282
+ message: { case: "execClientControlMessage", value: closeMessage },
1283
+ });
1284
+ const responseBytes = toBinary(AgentClientMessageSchema, clientMessage);
1285
+ h2Request.write(frameConnectMessage(responseBytes));
1286
+ log("execClientControl", "streamClose", { id: execMsg.id, execId: execMsg.execId });
1287
+ }
1288
+
1289
+ /** Exported for tests: verifies handler is invoked with correct `this` when passed as bound. */
1290
+ export async function resolveExecHandler<TArgs, TResult>(
1291
+ args: TArgs,
1292
+ handler: ((args: TArgs) => Promise<CursorExecHandlerResult<TResult>>) | undefined,
1293
+ onToolResult: CursorToolResultHandler | undefined,
1294
+ buildFromToolResult: (toolResult: ToolResultMessage) => TResult,
1295
+ buildRejected: (reason: string) => TResult,
1296
+ buildError: (error: string) => TResult,
1297
+ ): Promise<{ execResult: TResult; toolResult?: ToolResultMessage }> {
1298
+ if (!handler) {
1299
+ return { execResult: buildRejected("Tool not available") };
1300
+ }
1301
+
1302
+ try {
1303
+ const handlerResult = await handler(args);
1304
+ const { execResult, toolResult } = splitExecHandlerResult(handlerResult);
1305
+ const finalToolResult = await applyToolResultHandler(toolResult, onToolResult);
1306
+
1307
+ if (execResult) {
1308
+ return { execResult, toolResult: finalToolResult };
1309
+ }
1310
+ if (finalToolResult) {
1311
+ return { execResult: buildFromToolResult(finalToolResult), toolResult: finalToolResult };
1312
+ }
1313
+ return { execResult: buildRejected("Tool returned no result") };
1314
+ } catch (error) {
1315
+ const message = error instanceof Error ? error.message : String(error);
1316
+ return { execResult: buildError(message) };
1317
+ }
1318
+ }
1319
+
1320
+ function splitExecHandlerResult<TResult>(result: CursorExecHandlerResult<TResult>): {
1321
+ execResult?: TResult;
1322
+ toolResult?: ToolResultMessage;
1323
+ } {
1324
+ if (isToolResultMessage(result)) {
1325
+ return { toolResult: result };
1326
+ }
1327
+ if (result && typeof result === "object") {
1328
+ const record = result as Record<string, unknown>;
1329
+ if ("execResult" in record) {
1330
+ const { execResult, toolResult } = record as {
1331
+ execResult: TResult;
1332
+ toolResult?: ToolResultMessage;
1333
+ };
1334
+ return { execResult, toolResult };
1335
+ }
1336
+ if ("toolResult" in record && !isToolResultMessage(record)) {
1337
+ const { result: execResult, toolResult } = record as {
1338
+ result?: TResult;
1339
+ toolResult?: ToolResultMessage;
1340
+ };
1341
+ return { execResult, toolResult };
1342
+ }
1343
+ if ("result" in record && !("$typeName" in record)) {
1344
+ const { result: execResult, toolResult } = record as {
1345
+ result: TResult;
1346
+ toolResult?: ToolResultMessage;
1347
+ };
1348
+ return { execResult, toolResult };
1349
+ }
1350
+ }
1351
+ return { execResult: result as TResult };
1352
+ }
1353
+
1354
+ function isToolResultMessage(value: unknown): value is ToolResultMessage {
1355
+ return !!value && typeof value === "object" && (value as ToolResultMessage).role === "toolResult";
1356
+ }
1357
+
1358
+ async function applyToolResultHandler(
1359
+ toolResult: ToolResultMessage | undefined,
1360
+ onToolResult: CursorToolResultHandler | undefined,
1361
+ ): Promise<ToolResultMessage | undefined> {
1362
+ if (!toolResult || !onToolResult) {
1363
+ return toolResult;
1364
+ }
1365
+ const updated = await onToolResult(toolResult);
1366
+ return updated ?? toolResult;
1367
+ }
1368
+
1369
+ function toolResultToText(toolResult: ToolResultMessage): string {
1370
+ return toolResult.content.map(item => (item.type === "text" ? item.text : `[${item.mimeType} image]`)).join("\n");
1371
+ }
1372
+
1373
+ function toolResultWasTruncated(toolResult: ToolResultMessage): boolean {
1374
+ if (!toolResult.details || typeof toolResult.details !== "object") {
1375
+ return false;
1376
+ }
1377
+ const truncation = (toolResult.details as { truncation?: { truncated?: boolean } }).truncation;
1378
+ return !!truncation?.truncated;
1379
+ }
1380
+
1381
+ function toolResultDetailBoolean(toolResult: ToolResultMessage, key: string): boolean {
1382
+ if (!toolResult.details || typeof toolResult.details !== "object") {
1383
+ return false;
1384
+ }
1385
+ const value = (toolResult.details as Record<string, unknown>)[key];
1386
+ return typeof value === "boolean" ? value : false;
1387
+ }
1388
+
1389
+ function buildReadResultFromToolResult(path: string, toolResult: ToolResultMessage) {
1390
+ const text = toolResultToText(toolResult);
1391
+ if (toolResult.isError) {
1392
+ return buildReadErrorResult(path, text || "Read failed");
1393
+ }
1394
+ const totalLines = text ? text.split("\n").length : 0;
1395
+ return create(ReadResultSchema, {
1396
+ result: {
1397
+ case: "success",
1398
+ value: create(ReadSuccessSchema, {
1399
+ path,
1400
+ totalLines,
1401
+ fileSize: BigInt(Buffer.byteLength(text, "utf-8")),
1402
+ truncated: toolResultWasTruncated(toolResult),
1403
+ output: { case: "content", value: text },
1404
+ }),
1405
+ },
1406
+ });
1407
+ }
1408
+
1409
+ function buildReadErrorResult(path: string, error: string) {
1410
+ return create(ReadResultSchema, {
1411
+ result: {
1412
+ case: "error",
1413
+ value: create(ReadErrorSchema, { path, error }),
1414
+ },
1415
+ });
1416
+ }
1417
+
1418
+ function buildReadRejectedResult(path: string, reason: string) {
1419
+ return create(ReadResultSchema, {
1420
+ result: {
1421
+ case: "rejected",
1422
+ value: create(ReadRejectedSchema, { path, reason }),
1423
+ },
1424
+ });
1425
+ }
1426
+
1427
+ function buildWriteResultFromToolResult(
1428
+ args: { path: string; fileText?: string; fileBytes?: Uint8Array; returnFileContentAfterWrite?: boolean },
1429
+ toolResult: ToolResultMessage,
1430
+ ) {
1431
+ const text = toolResultToText(toolResult);
1432
+ if (toolResult.isError) {
1433
+ return buildWriteErrorResult(args.path, text || "Write failed");
1434
+ }
1435
+ const fileText = args.fileText ?? "";
1436
+ const fileSize = args.fileBytes?.length ?? Buffer.byteLength(fileText, "utf-8");
1437
+ const linesCreated = fileText ? fileText.split("\n").length : 0;
1438
+ return create(WriteResultSchema, {
1439
+ result: {
1440
+ case: "success",
1441
+ value: create(WriteSuccessSchema, {
1442
+ path: args.path,
1443
+ linesCreated,
1444
+ fileSize,
1445
+ fileContentAfterWrite: args.returnFileContentAfterWrite ? fileText : undefined,
1446
+ }),
1447
+ },
1448
+ });
1449
+ }
1450
+
1451
+ function buildWriteErrorResult(path: string, error: string) {
1452
+ return create(WriteResultSchema, {
1453
+ result: {
1454
+ case: "error",
1455
+ value: create(WriteErrorSchema, { path, error }),
1456
+ },
1457
+ });
1458
+ }
1459
+
1460
+ function buildWriteRejectedResult(path: string, reason: string) {
1461
+ return create(WriteResultSchema, {
1462
+ result: {
1463
+ case: "rejected",
1464
+ value: create(WriteRejectedSchema, { path, reason }),
1465
+ },
1466
+ });
1467
+ }
1468
+
1469
+ function buildDeleteResultFromToolResult(path: string, toolResult: ToolResultMessage) {
1470
+ const text = toolResultToText(toolResult);
1471
+ if (toolResult.isError) {
1472
+ return buildDeleteErrorResult(path, text || "Delete failed");
1473
+ }
1474
+ return create(DeleteResultSchema, {
1475
+ result: {
1476
+ case: "success",
1477
+ value: create(DeleteSuccessSchema, {
1478
+ path,
1479
+ deletedFile: path,
1480
+ fileSize: BigInt(0),
1481
+ prevContent: "",
1482
+ }),
1483
+ },
1484
+ });
1485
+ }
1486
+
1487
+ function buildDeleteErrorResult(path: string, error: string) {
1488
+ return create(DeleteResultSchema, {
1489
+ result: {
1490
+ case: "error",
1491
+ value: create(DeleteErrorSchema, { path, error }),
1492
+ },
1493
+ });
1494
+ }
1495
+
1496
+ function buildDeleteRejectedResult(path: string, reason: string) {
1497
+ return create(DeleteResultSchema, {
1498
+ result: {
1499
+ case: "rejected",
1500
+ value: create(DeleteRejectedSchema, { path, reason }),
1501
+ },
1502
+ });
1503
+ }
1504
+
1505
+ function buildShellResultFromToolResult(
1506
+ args: { command: string; workingDirectory: string },
1507
+ toolResult: ToolResultMessage,
1508
+ ) {
1509
+ const output = toolResultToText(toolResult);
1510
+ if (toolResult.isError) {
1511
+ return buildShellFailureResult(args.command, args.workingDirectory, output || "Shell failed");
1512
+ }
1513
+ return create(ShellResultSchema, {
1514
+ result: {
1515
+ case: "success",
1516
+ value: create(ShellSuccessSchema, {
1517
+ command: args.command,
1518
+ workingDirectory: args.workingDirectory,
1519
+ exitCode: 0,
1520
+ signal: "",
1521
+ stdout: output,
1522
+ stderr: "",
1523
+ executionTime: 0,
1524
+ }),
1525
+ },
1526
+ });
1527
+ }
1528
+
1529
+ function buildShellFailureResult(command: string, workingDirectory: string, error: string) {
1530
+ return create(ShellResultSchema, {
1531
+ result: {
1532
+ case: "failure",
1533
+ value: create(ShellFailureSchema, {
1534
+ command,
1535
+ workingDirectory,
1536
+ exitCode: 1,
1537
+ signal: "",
1538
+ stdout: "",
1539
+ stderr: error,
1540
+ executionTime: 0,
1541
+ aborted: false,
1542
+ }),
1543
+ },
1544
+ });
1545
+ }
1546
+
1547
+ function buildShellRejectedResult(command: string, workingDirectory: string, reason: string) {
1548
+ return create(ShellResultSchema, {
1549
+ result: {
1550
+ case: "rejected",
1551
+ value: create(ShellRejectedSchema, {
1552
+ command,
1553
+ workingDirectory,
1554
+ reason,
1555
+ isReadonly: false,
1556
+ }),
1557
+ },
1558
+ });
1559
+ }
1560
+
1561
+ function buildLsResultFromToolResult(path: string, toolResult: ToolResultMessage) {
1562
+ const text = toolResultToText(toolResult);
1563
+ if (toolResult.isError) {
1564
+ return buildLsErrorResult(path, text || "Ls failed");
1565
+ }
1566
+ const rootPath = path || ".";
1567
+ const entries = text
1568
+ .split("\n")
1569
+ .map(line => line.trim())
1570
+ .filter(line => line.length > 0 && !line.startsWith("["));
1571
+ const childrenDirs: LsDirectoryTreeNode[] = [];
1572
+ const childrenFiles: LsDirectoryTreeNode_File[] = [];
1573
+
1574
+ for (const entry of entries) {
1575
+ const name = entry.split(" (")[0];
1576
+ if (name.endsWith("/")) {
1577
+ const dirName = name.slice(0, -1);
1578
+ childrenDirs.push(
1579
+ create(LsDirectoryTreeNodeSchema, {
1580
+ absPath: `${rootPath.replace(/\/$/, "")}/${dirName}`,
1581
+ childrenDirs: [],
1582
+ childrenFiles: [],
1583
+ childrenWereProcessed: false,
1584
+ fullSubtreeExtensionCounts: {},
1585
+ numFiles: 0,
1586
+ }),
1587
+ );
1588
+ } else {
1589
+ childrenFiles.push(create(LsDirectoryTreeNode_FileSchema, { name }));
1590
+ }
1591
+ }
1592
+
1593
+ const root = create(LsDirectoryTreeNodeSchema, {
1594
+ absPath: rootPath,
1595
+ childrenDirs,
1596
+ childrenFiles,
1597
+ childrenWereProcessed: true,
1598
+ fullSubtreeExtensionCounts: {},
1599
+ numFiles: childrenFiles.length,
1600
+ });
1601
+
1602
+ return create(LsResultSchema, {
1603
+ result: {
1604
+ case: "success",
1605
+ value: create(LsSuccessSchema, { directoryTreeRoot: root }),
1606
+ },
1607
+ });
1608
+ }
1609
+
1610
+ function buildLsErrorResult(path: string, error: string) {
1611
+ return create(LsResultSchema, {
1612
+ result: {
1613
+ case: "error",
1614
+ value: create(LsErrorSchema, { path, error }),
1615
+ },
1616
+ });
1617
+ }
1618
+
1619
+ function buildLsRejectedResult(path: string, reason: string) {
1620
+ return create(LsResultSchema, {
1621
+ result: {
1622
+ case: "rejected",
1623
+ value: create(LsRejectedSchema, { path, reason }),
1624
+ },
1625
+ });
1626
+ }
1627
+
1628
+ function buildGrepResultFromToolResult(
1629
+ args: { pattern: string; path?: string; outputMode?: string },
1630
+ toolResult: ToolResultMessage,
1631
+ ) {
1632
+ const text = toolResultToText(toolResult);
1633
+ if (toolResult.isError) {
1634
+ return buildGrepErrorResult(text || "Grep failed");
1635
+ }
1636
+
1637
+ const outputMode = args.outputMode || "content";
1638
+ const clientTruncated = toolResultDetailBoolean(toolResult, "truncated");
1639
+ const lines = text
1640
+ .split("\n")
1641
+ .map(line => line.trimEnd())
1642
+ .filter(line => line.length > 0 && !line.startsWith("[") && !line.toLowerCase().startsWith("no matches"));
1643
+
1644
+ const workspaceKey = args.path || ".";
1645
+ let unionResult: GrepUnionResult;
1646
+
1647
+ if (outputMode === "files_with_matches") {
1648
+ const files = lines;
1649
+ unionResult = create(GrepUnionResultSchema, {
1650
+ result: {
1651
+ case: "files",
1652
+ value: create(GrepFilesResultSchema, {
1653
+ files,
1654
+ totalFiles: files.length,
1655
+ clientTruncated,
1656
+ ripgrepTruncated: false,
1657
+ }),
1658
+ },
1659
+ });
1660
+ } else if (outputMode === "count") {
1661
+ const counts = lines
1662
+ .map(line => {
1663
+ const separatorIndex = line.lastIndexOf(":");
1664
+ if (separatorIndex === -1) {
1665
+ return null;
1666
+ }
1667
+ const file = line.slice(0, separatorIndex);
1668
+ const count = Number.parseInt(line.slice(separatorIndex + 1), 10);
1669
+ if (!file || Number.isNaN(count)) {
1670
+ return null;
1671
+ }
1672
+ return create(GrepFileCountSchema, { file, count });
1673
+ })
1674
+ .filter((entry): entry is GrepFileCount => entry !== null);
1675
+ const totalMatches = counts.reduce((sum, entry) => sum + entry.count, 0);
1676
+ unionResult = create(GrepUnionResultSchema, {
1677
+ result: {
1678
+ case: "count",
1679
+ value: create(GrepCountResultSchema, {
1680
+ counts,
1681
+ totalFiles: counts.length,
1682
+ totalMatches,
1683
+ clientTruncated,
1684
+ ripgrepTruncated: false,
1685
+ }),
1686
+ },
1687
+ });
1688
+ } else {
1689
+ const matchMap = new Map<string, Array<{ line: number; content: string; isContextLine: boolean }>>();
1690
+ let totalMatchedLines = 0;
1691
+
1692
+ for (const line of lines) {
1693
+ const matchLine = line.match(/^(.+?):(\d+):\s?(.*)$/);
1694
+ const contextLine = line.match(/^(.+?)-(\d+)-\s?(.*)$/);
1695
+ const match = matchLine ?? contextLine;
1696
+ if (!match) {
1697
+ continue;
1698
+ }
1699
+ const [, file, lineNumber, content] = match;
1700
+ const isContextLine = Boolean(contextLine);
1701
+ const list = matchMap.get(file) ?? [];
1702
+ list.push({ line: Number(lineNumber), content, isContextLine });
1703
+ matchMap.set(file, list);
1704
+ if (!isContextLine) {
1705
+ totalMatchedLines += 1;
1706
+ }
1707
+ }
1708
+
1709
+ const matches = Array.from(matchMap.entries()).map(([file, matches]) =>
1710
+ create(GrepFileMatchSchema, {
1711
+ file,
1712
+ matches: matches.map(entry =>
1713
+ create(GrepContentMatchSchema, {
1714
+ lineNumber: entry.line,
1715
+ content: entry.content,
1716
+ contentTruncated: false,
1717
+ isContextLine: entry.isContextLine,
1718
+ }),
1719
+ ),
1720
+ }),
1721
+ );
1722
+ const totalLines = matches.reduce((sum, entry) => sum + entry.matches.length, 0);
1723
+ unionResult = create(GrepUnionResultSchema, {
1724
+ result: {
1725
+ case: "content",
1726
+ value: create(GrepContentResultSchema, {
1727
+ matches,
1728
+ totalLines,
1729
+ totalMatchedLines,
1730
+ clientTruncated,
1731
+ ripgrepTruncated: false,
1732
+ }),
1733
+ },
1734
+ });
1735
+ }
1736
+
1737
+ return create(GrepResultSchema, {
1738
+ result: {
1739
+ case: "success",
1740
+ value: create(GrepSuccessSchema, {
1741
+ pattern: args.pattern,
1742
+ path: args.path || "",
1743
+ outputMode,
1744
+ workspaceResults: { [workspaceKey]: unionResult },
1745
+ }),
1746
+ },
1747
+ });
1748
+ }
1749
+
1750
+ function buildGrepErrorResult(error: string) {
1751
+ return create(GrepResultSchema, {
1752
+ result: {
1753
+ case: "error",
1754
+ value: create(GrepErrorSchema, { error }),
1755
+ },
1756
+ });
1757
+ }
1758
+
1759
+ function buildDiagnosticsResultFromToolResult(path: string, toolResult: ToolResultMessage) {
1760
+ const text = toolResultToText(toolResult);
1761
+ if (toolResult.isError) {
1762
+ return buildDiagnosticsErrorResult(path, text || "Diagnostics failed");
1763
+ }
1764
+ return create(DiagnosticsResultSchema, {
1765
+ result: {
1766
+ case: "success",
1767
+ value: create(DiagnosticsSuccessSchema, {
1768
+ path,
1769
+ diagnostics: [],
1770
+ totalDiagnostics: 0,
1771
+ }),
1772
+ },
1773
+ });
1774
+ }
1775
+
1776
+ function buildDiagnosticsErrorResult(_path: string, error: string) {
1777
+ return create(DiagnosticsResultSchema, {
1778
+ result: {
1779
+ case: "error",
1780
+ value: create(DiagnosticsErrorSchema, { error }),
1781
+ },
1782
+ });
1783
+ }
1784
+
1785
+ function buildDiagnosticsRejectedResult(path: string, reason: string) {
1786
+ return create(DiagnosticsResultSchema, {
1787
+ result: {
1788
+ case: "rejected",
1789
+ value: create(DiagnosticsRejectedSchema, { path, reason }),
1790
+ },
1791
+ });
1792
+ }
1793
+
1794
+ function parseToolArgsJson(text: string): unknown {
1795
+ const trimmed = text.trim();
1796
+ if (!trimmed) {
1797
+ return text;
1798
+ }
1799
+ try {
1800
+ const normalized = trimmed
1801
+ .replace(/\bNone\b/g, "null")
1802
+ .replace(/\bTrue\b/g, "true")
1803
+ .replace(/\bFalse\b/g, "false");
1804
+ return Bun.JSON5.parse(normalized);
1805
+ } catch {}
1806
+ return text;
1807
+ }
1808
+
1809
+ function decodeMcpArgValue(value: Uint8Array): unknown {
1810
+ try {
1811
+ const parsedValue = fromBinary(ValueSchema, value);
1812
+ const jsonValue = toJson(ValueSchema, parsedValue) as JsonValue;
1813
+ if (typeof jsonValue === "string") {
1814
+ return parseToolArgsJson(jsonValue);
1815
+ }
1816
+ return jsonValue;
1817
+ } catch {}
1818
+ const text = new TextDecoder().decode(value);
1819
+ return parseToolArgsJson(text);
1820
+ }
1821
+
1822
+ function decodeMcpArgsMap(args?: Record<string, Uint8Array>): Record<string, unknown> | undefined {
1823
+ if (!args) {
1824
+ return undefined;
1825
+ }
1826
+ const decoded: Record<string, unknown> = {};
1827
+ for (const [key, value] of Object.entries(args)) {
1828
+ decoded[key] = decodeMcpArgValue(value);
1829
+ }
1830
+ return decoded;
1831
+ }
1832
+
1833
+ function decodeMcpCall(args: {
1834
+ name: string;
1835
+ args: Record<string, Uint8Array>;
1836
+ toolCallId: string;
1837
+ providerIdentifier: string;
1838
+ toolName: string;
1839
+ }): CursorMcpCall {
1840
+ const decodedArgs: Record<string, unknown> = {};
1841
+ for (const [key, value] of Object.entries(args.args ?? {})) {
1842
+ decodedArgs[key] = decodeMcpArgValue(value);
1843
+ }
1844
+ return {
1845
+ name: args.name,
1846
+ providerIdentifier: args.providerIdentifier,
1847
+ toolName: args.toolName || args.name,
1848
+ toolCallId: args.toolCallId,
1849
+ args: decodedArgs,
1850
+ rawArgs: args.args ?? {},
1851
+ };
1852
+ }
1853
+
1854
+ function mapTodoStatusValue(status?: number): "pending" | "in_progress" | "completed" {
1855
+ switch (status) {
1856
+ case 2:
1857
+ return "in_progress";
1858
+ case 3:
1859
+ return "completed";
1860
+ default:
1861
+ return "pending";
1862
+ }
1863
+ }
1864
+
1865
+ interface CursorTodoItem {
1866
+ id?: string;
1867
+ content?: string;
1868
+ status?: number;
1869
+ }
1870
+
1871
+ interface CursorUpdateTodosToolCall {
1872
+ updateTodosToolCall?: { args?: { todos?: CursorTodoItem[] } };
1873
+ }
1874
+
1875
+ function buildTodoWriteArgs(toolCall: CursorUpdateTodosToolCall): {
1876
+ todos: Array<{ id?: string; content: string; activeForm: string; status: "pending" | "in_progress" | "completed" }>;
1877
+ } | null {
1878
+ const todos = toolCall.updateTodosToolCall?.args?.todos;
1879
+ if (!todos) return null;
1880
+ return {
1881
+ todos: todos.map(todo => ({
1882
+ id: typeof todo.id === "string" && todo.id.length > 0 ? todo.id : undefined,
1883
+ content: typeof todo.content === "string" ? todo.content : "",
1884
+ activeForm: typeof todo.content === "string" ? todo.content : "",
1885
+ status: mapTodoStatusValue(typeof todo.status === "number" ? todo.status : undefined),
1886
+ })),
1887
+ };
1888
+ }
1889
+
1890
+ function buildMcpResultFromToolResult(_mcpCall: CursorMcpCall, toolResult: ToolResultMessage) {
1891
+ if (toolResult.isError) {
1892
+ return buildMcpErrorResult(toolResultToText(toolResult) || "MCP tool failed");
1893
+ }
1894
+ const content = toolResult.content.map(item => {
1895
+ if (item.type === "image") {
1896
+ return create(McpToolResultContentItemSchema, {
1897
+ content: {
1898
+ case: "image",
1899
+ value: create(McpImageContentSchema, {
1900
+ data: Uint8Array.from(Buffer.from(item.data, "base64")),
1901
+ mimeType: item.mimeType,
1902
+ }),
1903
+ },
1904
+ });
1905
+ }
1906
+ return create(McpToolResultContentItemSchema, {
1907
+ content: {
1908
+ case: "text",
1909
+ value: create(McpTextContentSchema, { text: item.text }),
1910
+ },
1911
+ });
1912
+ });
1913
+
1914
+ return create(McpResultSchema, {
1915
+ result: {
1916
+ case: "success",
1917
+ value: create(McpSuccessSchema, {
1918
+ content,
1919
+ isError: false,
1920
+ }),
1921
+ },
1922
+ });
1923
+ }
1924
+
1925
+ function buildMcpToolNotFoundResult(mcpCall: CursorMcpCall) {
1926
+ return create(McpResultSchema, {
1927
+ result: {
1928
+ case: "toolNotFound",
1929
+ value: create(McpToolNotFoundSchema, { name: mcpCall.toolName, availableTools: [] }),
1930
+ },
1931
+ });
1932
+ }
1933
+
1934
+ function buildMcpErrorResult(error: string) {
1935
+ return create(McpResultSchema, {
1936
+ result: {
1937
+ case: "error",
1938
+ value: create(McpErrorSchema, { error }),
1939
+ },
1940
+ });
1941
+ }
1942
+
1943
+ function processInteractionUpdate(
1944
+ update: any,
1945
+ output: AssistantMessage,
1946
+ stream: AssistantMessageEventStream,
1947
+ state: BlockState,
1948
+ usageState: UsageState,
1949
+ ): void {
1950
+ const updateCase = update.message?.case;
1951
+
1952
+ log("interactionUpdate", updateCase, update.message?.value);
1953
+
1954
+ if (updateCase === "textDelta") {
1955
+ state.setFirstTokenTime();
1956
+ const delta = update.message.value.text || "";
1957
+ if (!state.currentTextBlock) {
1958
+ const block: TextContent & { index: number } = {
1959
+ type: "text",
1960
+ text: "",
1961
+ index: output.content.length,
1962
+ };
1963
+ output.content.push(block);
1964
+ state.setTextBlock(block);
1965
+ stream.push({ type: "text_start", contentIndex: output.content.length - 1, partial: output });
1966
+ }
1967
+ state.currentTextBlock!.text += delta;
1968
+ const idx = output.content.indexOf(state.currentTextBlock!);
1969
+ stream.push({ type: "text_delta", contentIndex: idx, delta, partial: output });
1970
+ } else if (updateCase === "thinkingDelta") {
1971
+ state.setFirstTokenTime();
1972
+ const delta = update.message.value.text || "";
1973
+ if (!state.currentThinkingBlock) {
1974
+ const block: ThinkingContent & { index: number } = {
1975
+ type: "thinking",
1976
+ thinking: "",
1977
+ index: output.content.length,
1978
+ };
1979
+ output.content.push(block);
1980
+ state.setThinkingBlock(block);
1981
+ stream.push({ type: "thinking_start", contentIndex: output.content.length - 1, partial: output });
1982
+ }
1983
+ state.currentThinkingBlock!.thinking += delta;
1984
+ const idx = output.content.indexOf(state.currentThinkingBlock!);
1985
+ stream.push({ type: "thinking_delta", contentIndex: idx, delta, partial: output });
1986
+ } else if (updateCase === "thinkingCompleted") {
1987
+ if (state.currentThinkingBlock) {
1988
+ const idx = output.content.indexOf(state.currentThinkingBlock);
1989
+ delete (state.currentThinkingBlock as any).index;
1990
+ stream.push({
1991
+ type: "thinking_end",
1992
+ contentIndex: idx,
1993
+ content: state.currentThinkingBlock.thinking,
1994
+ partial: output,
1995
+ });
1996
+ state.setThinkingBlock(null);
1997
+ }
1998
+ } else if (updateCase === "toolCallStarted") {
1999
+ const toolCall = update.message.value.toolCall;
2000
+ if (toolCall) {
2001
+ const mcpCall = toolCall.mcpToolCall;
2002
+ if (mcpCall) {
2003
+ const args = mcpCall.args || {};
2004
+ const block: ToolCallState = {
2005
+ type: "toolCall",
2006
+ id: args.toolCallId || crypto.randomUUID(),
2007
+ name: args.name || args.toolName || "",
2008
+ arguments: {},
2009
+ index: output.content.length,
2010
+ partialJson: "",
2011
+ kind: "mcp",
2012
+ };
2013
+ output.content.push(block);
2014
+ state.setToolCall(block);
2015
+ stream.push({ type: "toolcall_start", contentIndex: output.content.length - 1, partial: output });
2016
+ return;
2017
+ }
2018
+
2019
+ const todoArgs = buildTodoWriteArgs(toolCall);
2020
+ if (todoArgs) {
2021
+ const callId = update.message.value.callId || crypto.randomUUID();
2022
+ const block: ToolCallState = {
2023
+ type: "toolCall",
2024
+ id: callId,
2025
+ name: "todo_write",
2026
+ arguments: todoArgs,
2027
+ index: output.content.length,
2028
+ kind: "todo_write",
2029
+ };
2030
+ output.content.push(block);
2031
+ state.setToolCall(block);
2032
+ stream.push({ type: "toolcall_start", contentIndex: output.content.length - 1, partial: output });
2033
+ }
2034
+ }
2035
+ } else if (updateCase === "toolCallDelta" || updateCase === "partialToolCall") {
2036
+ if (state.currentToolCall?.kind === "mcp") {
2037
+ const delta = update.message.value.argsTextDelta || "";
2038
+ state.currentToolCall.partialJson = `${state.currentToolCall.partialJson ?? ""}${delta}`;
2039
+ state.currentToolCall.arguments = parseStreamingJson(state.currentToolCall.partialJson ?? "");
2040
+ const idx = output.content.indexOf(state.currentToolCall);
2041
+ stream.push({ type: "toolcall_delta", contentIndex: idx, delta, partial: output });
2042
+ }
2043
+ } else if (updateCase === "toolCallCompleted") {
2044
+ if (state.currentToolCall) {
2045
+ const toolCall = update.message.value.toolCall;
2046
+ if (state.currentToolCall.kind === "mcp") {
2047
+ const decodedArgs = decodeMcpArgsMap(toolCall?.mcpToolCall?.args?.args);
2048
+ if (decodedArgs) {
2049
+ state.currentToolCall.arguments = decodedArgs;
2050
+ }
2051
+ } else if (state.currentToolCall.kind === "todo_write" && toolCall) {
2052
+ const todoArgs = buildTodoWriteArgs(toolCall);
2053
+ if (todoArgs) {
2054
+ state.currentToolCall.arguments = todoArgs;
2055
+ }
2056
+ }
2057
+ const idx = output.content.indexOf(state.currentToolCall);
2058
+ delete (state.currentToolCall as any).partialJson;
2059
+ delete (state.currentToolCall as any).index;
2060
+ delete (state.currentToolCall as any).kind;
2061
+ stream.push({ type: "toolcall_end", contentIndex: idx, toolCall: state.currentToolCall, partial: output });
2062
+ state.setToolCall(null);
2063
+ }
2064
+ } else if (updateCase === "turnEnded") {
2065
+ output.stopReason = "stop";
2066
+ } else if (updateCase === "tokenDelta") {
2067
+ const tokenDelta = update.message.value;
2068
+ usageState.sawTokenDelta = true;
2069
+ output.usage.output += tokenDelta.tokens || 0;
2070
+ output.usage.totalTokens = output.usage.input + output.usage.output;
2071
+ }
2072
+ }
2073
+
2074
+ function handleConversationCheckpointUpdate(
2075
+ checkpoint: ConversationStateStructure,
2076
+ output: AssistantMessage,
2077
+ usageState: UsageState,
2078
+ onConversationCheckpoint?: (checkpoint: ConversationStateStructure) => void,
2079
+ ): void {
2080
+ onConversationCheckpoint?.(checkpoint);
2081
+ if (usageState.sawTokenDelta) {
2082
+ return;
2083
+ }
2084
+ const usedTokens = checkpoint.tokenDetails?.usedTokens ?? 0;
2085
+ if (usedTokens <= 0) {
2086
+ return;
2087
+ }
2088
+ if (output.usage.output !== usedTokens) {
2089
+ output.usage.output = usedTokens;
2090
+ output.usage.totalTokens = output.usage.input + output.usage.output;
2091
+ }
2092
+ }
2093
+
2094
+ function createBlobId(data: Uint8Array): Uint8Array {
2095
+ return new Uint8Array(createHash("sha256").update(data).digest());
2096
+ }
2097
+
2098
+ function storeCursorBlob(blobStore: Map<string, Uint8Array>, data: Uint8Array): Uint8Array {
2099
+ const blobId = createBlobId(data);
2100
+ blobStore.set(Buffer.from(blobId).toString("hex"), data);
2101
+ return blobId;
2102
+ }
2103
+
2104
+ function readCursorBlob(blobStore: Map<string, Uint8Array>, blobId: Uint8Array): Uint8Array {
2105
+ const data = blobStore.get(Buffer.from(blobId).toString("hex"));
2106
+ if (!data) {
2107
+ throw new Error("Cursor blob not found");
2108
+ }
2109
+ return data;
2110
+ }
2111
+
2112
+ const CURSOR_NATIVE_TOOL_NAMES = new Set(["bash", "read", "write", "delete", "ls", "grep", "lsp", "todo_write"]);
2113
+
2114
+ function buildMcpToolDefinitions(tools: Tool[] | undefined): McpToolDefinition[] {
2115
+ if (!tools || tools.length === 0) {
2116
+ return [];
2117
+ }
2118
+
2119
+ const advertisedTools = tools.filter(tool => !CURSOR_NATIVE_TOOL_NAMES.has(tool.name));
2120
+ if (advertisedTools.length === 0) {
2121
+ return [];
2122
+ }
2123
+
2124
+ return advertisedTools.map(tool => {
2125
+ const jsonSchema = toolWireSchema(tool);
2126
+ const schemaValue: JsonValue =
2127
+ jsonSchema && typeof jsonSchema === "object"
2128
+ ? (jsonSchema as JsonValue)
2129
+ : { type: "object", properties: {}, required: [] };
2130
+ const inputSchema = toBinary(ValueSchema, fromJson(ValueSchema, schemaValue));
2131
+ return create(McpToolDefinitionSchema, {
2132
+ name: tool.name,
2133
+ description: tool.description || "",
2134
+ providerIdentifier: "aery-agent",
2135
+ toolName: tool.name,
2136
+ inputSchema,
2137
+ });
2138
+ });
2139
+ }
2140
+
2141
+ /**
2142
+ * Extract text content from a user or developer message.
2143
+ */
2144
+ function extractUserMessageText(msg: Message): string {
2145
+ if (msg.role !== "user" && msg.role !== "developer") return "";
2146
+ const content = msg.content;
2147
+ if (typeof content === "string") return content.trim();
2148
+ const text = content
2149
+ .filter((c): c is TextContent => c.type === "text")
2150
+ .map(c => c.text)
2151
+ .join("\n");
2152
+ return text.trim();
2153
+ }
2154
+
2155
+ function hasUserMessageImages(msg: Message): boolean {
2156
+ return (
2157
+ (msg.role === "user" || msg.role === "developer") &&
2158
+ Array.isArray(msg.content) &&
2159
+ msg.content.some(item => item.type === "image")
2160
+ );
2161
+ }
2162
+
2163
+ type CursorRootPromptContentPart = { type: "text"; text: string } | { type: "image"; image: string; mediaType: string };
2164
+
2165
+ function buildCursorRootPromptContent(content: string | (TextContent | ImageContent)[]): CursorRootPromptContentPart[] {
2166
+ if (typeof content === "string") {
2167
+ const text = content.trim();
2168
+ return text ? [{ type: "text", text }] : [];
2169
+ }
2170
+ const parts: CursorRootPromptContentPart[] = [];
2171
+ for (const item of content) {
2172
+ if (item.type === "text") {
2173
+ const text = item.text.trim();
2174
+ if (text) {
2175
+ parts.push({ type: "text", text });
2176
+ }
2177
+ } else {
2178
+ parts.push({ type: "image", image: item.data, mediaType: item.mimeType });
2179
+ }
2180
+ }
2181
+ return parts;
2182
+ }
2183
+
2184
+ function cursorUserContentKey(content: string | (TextContent | ImageContent)[]): string {
2185
+ if (typeof content === "string") {
2186
+ return content.trim();
2187
+ }
2188
+ const hash = createHash("sha256");
2189
+ for (const item of content) {
2190
+ hash.update(item.type);
2191
+ if (item.type === "text") {
2192
+ hash.update(item.text);
2193
+ } else {
2194
+ hash.update(item.mimeType);
2195
+ hash.update(item.data);
2196
+ }
2197
+ }
2198
+ return hash.digest("hex");
2199
+ }
2200
+
2201
+ /**
2202
+ * Extract text content from an assistant message.
2203
+ */
2204
+ function extractAssistantMessageText(msg: Message): string {
2205
+ if (msg.role !== "assistant") return "";
2206
+ if (!Array.isArray(msg.content)) return "";
2207
+ return msg.content
2208
+ .filter((c): c is TextContent => c.type === "text")
2209
+ .map(c => c.text)
2210
+ .join("\n");
2211
+ }
2212
+
2213
+ /**
2214
+ * Derive a stable, UUID-formatted `message_id` from a content key.
2215
+ * Ensures identical historical messages hash to the same blob IDs across
2216
+ * requests, so `conversationBlobStores` does not grow unboundedly and
2217
+ * unchanged history reuses existing blob IDs.
2218
+ */
2219
+ type CursorMessageId = `${string}-${string}-${string}-${string}-${string}`;
2220
+
2221
+ function deterministicMessageId(key: string): CursorMessageId {
2222
+ const hex = createHash("sha256").update(key).digest("hex");
2223
+ return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20, 32)}`;
2224
+ }
2225
+
2226
+ /**
2227
+ * Index of the last user/developer message in `messages`, or -1 if none.
2228
+ * Used to exclude the current user turn from history builders — it goes in
2229
+ * `ConversationActionSchema.userMessageAction`, not in history structures.
2230
+ */
2231
+ function findLastUserMessageIndex(messages: Message[]): number {
2232
+ for (let i = messages.length - 1; i >= 0; i--) {
2233
+ const role = messages[i].role;
2234
+ if (role === "user" || role === "developer") {
2235
+ return i;
2236
+ }
2237
+ }
2238
+ return -1;
2239
+ }
2240
+
2241
+ /**
2242
+ * Build `ConversationStateStructure.rootPromptMessagesJson` blob IDs for the
2243
+ * system prompt plus prior conversation history, as JSON blobs matching
2244
+ * Cursor's internal Vercel-AI-SDK-shaped message format.
2245
+ *
2246
+ * Cursor's server uses `rootPromptMessagesJson` (not `turns[]`) to build the
2247
+ * actual model prompt. `turns[]` is UI/display metadata. Without populating
2248
+ * this field, multi-turn conversations lose prior context — the model sees
2249
+ * only an empty placeholder where historical user turns should be.
2250
+ * The active user message is excluded because it is sent in the action.
2251
+ */
2252
+ /**
2253
+ * Build one Cursor system-message JSON blob per ordered system prompt. Emitting separate blobs
2254
+ * (rather than a single `\n\n`-joined string) lets Cursor's blob cache hit independently per
2255
+ * entry: changing only the last prompt does not invalidate earlier blob ids, so the prefix
2256
+ * up to the changed prompt remains cached on the server side.
2257
+ *
2258
+ * When no system prompts are provided, returns a single default greeting so we never emit
2259
+ * an empty `rootPromptMessagesJson` head.
2260
+ */
2261
+ export function buildCursorSystemPromptJsons(systemPrompt: readonly string[] | undefined): string[] {
2262
+ const systemPrompts = normalizeSystemPrompts(systemPrompt);
2263
+ if (systemPrompts.length === 0) {
2264
+ return [JSON.stringify({ role: "system", content: "You are a helpful assistant." })];
2265
+ }
2266
+ return systemPrompts.map(content => JSON.stringify({ role: "system", content }));
2267
+ }
2268
+
2269
+ function buildRootPromptMessagesJson(
2270
+ messages: Message[],
2271
+ systemPromptIds: Uint8Array[],
2272
+ blobStore: Map<string, Uint8Array>,
2273
+ activeUserMessageIndex = findLastUserMessageIndex(messages),
2274
+ ): Uint8Array[] {
2275
+ const entries: Uint8Array[] = [...systemPromptIds];
2276
+ const pushJson = (obj: unknown) => {
2277
+ const bytes = new TextEncoder().encode(JSON.stringify(obj));
2278
+ entries.push(storeCursorBlob(blobStore, bytes));
2279
+ };
2280
+
2281
+ for (let i = 0; i < messages.length; i++) {
2282
+ if (i === activeUserMessageIndex) break;
2283
+ const msg = messages[i];
2284
+ if (msg.role === "user" || msg.role === "developer") {
2285
+ const content = buildCursorRootPromptContent(msg.content);
2286
+ if (content.length === 0) continue;
2287
+ pushJson({ role: "user", content });
2288
+ } else if (msg.role === "assistant") {
2289
+ const text = extractAssistantMessageText(msg);
2290
+ if (!text) continue;
2291
+ pushJson({ role: "assistant", content: [{ type: "text", text }] });
2292
+ } else if (msg.role === "toolResult") {
2293
+ const text = toolResultToText(msg);
2294
+ if (!text) continue;
2295
+ pushJson({
2296
+ role: "user",
2297
+ content: [{ type: "text", text: `[Tool Result]\n${text}` }],
2298
+ });
2299
+ }
2300
+ }
2301
+
2302
+ return entries;
2303
+ }
2304
+
2305
+ /**
2306
+ * Convert context.messages to Cursor's ConversationTurnStructure blob IDs.
2307
+ * Groups messages into turns: each turn is a user message followed by the assistant's response.
2308
+ * Excludes the active user message (which goes in the action).
2309
+ *
2310
+ * Each `AgentConversationTurnStructure.user_message`, `steps[]`, and the outer
2311
+ * `ConversationStateStructure.turns[]` entry is a blob ID into `blobStore`.
2312
+ */
2313
+ function buildConversationTurns(
2314
+ messages: Message[],
2315
+ blobStore: Map<string, Uint8Array>,
2316
+ activeUserMessageIndex = findLastUserMessageIndex(messages),
2317
+ ): Uint8Array[] {
2318
+ const turns: Uint8Array[] = [];
2319
+
2320
+ // Find turn boundaries - each turn starts with a user message
2321
+ let i = 0;
2322
+ while (i < messages.length) {
2323
+ const msg = messages[i];
2324
+
2325
+ // Skip non-user messages at the start
2326
+ if (msg.role !== "user" && msg.role !== "developer") {
2327
+ i++;
2328
+ continue;
2329
+ }
2330
+
2331
+ // The active user message goes in the action, not turns. A prior user
2332
+ // followed by assistant/tool-result messages is complete history and
2333
+ // must remain serialized for resume actions.
2334
+ if (i === activeUserMessageIndex) {
2335
+ break;
2336
+ }
2337
+
2338
+ // Create and serialize user message
2339
+ const userText = extractUserMessageText(msg);
2340
+ if (userText.length === 0 && !hasUserMessageImages(msg)) {
2341
+ i++;
2342
+ continue;
2343
+ }
2344
+
2345
+ const userMessage = createCursorUserMessage(
2346
+ msg.content,
2347
+ userText,
2348
+ deterministicMessageId(`u:${turns.length}:${cursorUserContentKey(msg.content)}`),
2349
+ );
2350
+ const userMessageBytes = toBinary(UserMessageSchema, userMessage);
2351
+ const userMessageBlobId = storeCursorBlob(blobStore, userMessageBytes);
2352
+
2353
+ // Collect and serialize steps until next user message
2354
+ const stepBlobIds: Uint8Array[] = [];
2355
+ i++;
2356
+
2357
+ while (i < messages.length && messages[i].role !== "user" && messages[i].role !== "developer") {
2358
+ const stepMsg = messages[i];
2359
+
2360
+ if (stepMsg.role === "assistant") {
2361
+ const text = extractAssistantMessageText(stepMsg);
2362
+ if (text) {
2363
+ const step = create(ConversationStepSchema, {
2364
+ message: {
2365
+ case: "assistantMessage",
2366
+ value: create(AssistantMessageSchema, { text }),
2367
+ },
2368
+ });
2369
+ stepBlobIds.push(storeCursorBlob(blobStore, toBinary(ConversationStepSchema, step)));
2370
+ }
2371
+ } else if (stepMsg.role === "toolResult") {
2372
+ // Include tool results as assistant text for context
2373
+ const text = toolResultToText(stepMsg);
2374
+ if (text) {
2375
+ const step = create(ConversationStepSchema, {
2376
+ message: {
2377
+ case: "assistantMessage",
2378
+ value: create(AssistantMessageSchema, { text: `[Tool Result]\n${text}` }),
2379
+ },
2380
+ });
2381
+ stepBlobIds.push(storeCursorBlob(blobStore, toBinary(ConversationStepSchema, step)));
2382
+ }
2383
+ }
2384
+
2385
+ i++;
2386
+ }
2387
+
2388
+ // Create the serialized turn using Structure types. The bytes fields
2389
+ // (user_message, steps) are blob IDs resolved through the KV store.
2390
+ const agentTurn = create(AgentConversationTurnStructureSchema, {
2391
+ userMessage: userMessageBlobId,
2392
+ steps: stepBlobIds,
2393
+ });
2394
+ const turn = create(ConversationTurnStructureSchema, {
2395
+ turn: {
2396
+ case: "agentConversationTurn",
2397
+ value: agentTurn,
2398
+ },
2399
+ });
2400
+ turns.push(storeCursorBlob(blobStore, toBinary(ConversationTurnStructureSchema, turn)));
2401
+ }
2402
+
2403
+ return turns;
2404
+ }
2405
+
2406
+ /** Exported for tests: decodes Cursor history blobs built from conversation messages. */
2407
+ export function buildCursorHistoryForTest(
2408
+ messages: Message[],
2409
+ activeUserMessageIndex = findLastUserMessageIndex(messages),
2410
+ ): {
2411
+ rootPromptMessagesJson: unknown[];
2412
+ turnUserMessagesJson: JsonValue[];
2413
+ turnStepMessagesJson: JsonValue[][];
2414
+ } {
2415
+ const blobStore = new Map<string, Uint8Array>();
2416
+ const rootPromptMessagesJson = buildRootPromptMessagesJson(messages, [], blobStore, activeUserMessageIndex).map(
2417
+ blobId => JSON.parse(new TextDecoder().decode(readCursorBlob(blobStore, blobId))),
2418
+ );
2419
+ const turnUserMessagesJson: JsonValue[] = [];
2420
+ const turnStepMessagesJson: JsonValue[][] = [];
2421
+ for (const turnBlobId of buildConversationTurns(messages, blobStore, activeUserMessageIndex)) {
2422
+ const turn = fromBinary(ConversationTurnStructureSchema, readCursorBlob(blobStore, turnBlobId));
2423
+ if (turn.turn.case !== "agentConversationTurn") {
2424
+ continue;
2425
+ }
2426
+ const userMessage = fromBinary(UserMessageSchema, readCursorBlob(blobStore, turn.turn.value.userMessage));
2427
+ turnUserMessagesJson.push(toJson(UserMessageSchema, userMessage));
2428
+ turnStepMessagesJson.push(
2429
+ turn.turn.value.steps.map(stepBlobId => {
2430
+ const step = fromBinary(ConversationStepSchema, readCursorBlob(blobStore, stepBlobId));
2431
+ return toJson(ConversationStepSchema, step);
2432
+ }),
2433
+ );
2434
+ }
2435
+ return { rootPromptMessagesJson, turnUserMessagesJson, turnStepMessagesJson };
2436
+ }
2437
+ function createCursorUserMessage(
2438
+ content: string | (TextContent | ImageContent)[],
2439
+ text: string,
2440
+ messageId = crypto.randomUUID(),
2441
+ ) {
2442
+ const images = typeof content === "string" ? [] : extractImages(content);
2443
+ return create(UserMessageSchema, {
2444
+ text,
2445
+ messageId,
2446
+ ...(images.length > 0
2447
+ ? {
2448
+ selectedContext: create(SelectedContextSchema, {
2449
+ selectedImages: images,
2450
+ }),
2451
+ }
2452
+ : {}),
2453
+ });
2454
+ }
2455
+
2456
+ function extractImages(content: (TextContent | ImageContent)[]) {
2457
+ return content
2458
+ .filter((item): item is ImageContent => item.type === "image")
2459
+ .map(image =>
2460
+ create(SelectedImageSchema, {
2461
+ uuid: crypto.randomUUID(),
2462
+ mimeType: image.mimeType,
2463
+ dataOrBlobId: {
2464
+ case: "data",
2465
+ value: Uint8Array.from(Buffer.from(image.data, "base64")),
2466
+ },
2467
+ }),
2468
+ );
2469
+ }
2470
+
2471
+ function buildGrpcRequest(
2472
+ model: Model<"cursor-agent">,
2473
+ context: Context,
2474
+ options: CursorOptions | undefined,
2475
+ state: {
2476
+ conversationId: string;
2477
+ blobStore: Map<string, Uint8Array>;
2478
+ conversationState?: ConversationStateStructure;
2479
+ },
2480
+ ): {
2481
+ requestBytes: Uint8Array;
2482
+ blobStore: Map<string, Uint8Array>;
2483
+ conversationState: ConversationStateStructure;
2484
+ } {
2485
+ const blobStore = state.blobStore;
2486
+
2487
+ const systemPromptIds = buildCursorSystemPromptJsons(context.systemPrompt).map(json =>
2488
+ storeCursorBlob(blobStore, new TextEncoder().encode(json)),
2489
+ );
2490
+
2491
+ const activeUserMessageIndex = context.messages.length - 1;
2492
+ const activeMessage = context.messages[activeUserMessageIndex];
2493
+ const activeUserMessage =
2494
+ activeMessage?.role === "user" || activeMessage?.role === "developer" ? activeMessage : undefined;
2495
+ let userContent: string | (TextContent | ImageContent)[] | undefined;
2496
+ let userText = "";
2497
+ let hasUserImages = false;
2498
+ if (activeUserMessage?.role === "user" || activeUserMessage?.role === "developer") {
2499
+ userContent = activeUserMessage.content;
2500
+ if (typeof userContent === "string") {
2501
+ userText = userContent.trim();
2502
+ } else {
2503
+ userText = extractText(userContent);
2504
+ hasUserImages = hasImages(userContent);
2505
+ }
2506
+ }
2507
+
2508
+ const action = create(ConversationActionSchema, {
2509
+ action:
2510
+ userContent && (userText.trim().length > 0 || hasUserImages)
2511
+ ? {
2512
+ case: "userMessageAction",
2513
+ value: create(UserMessageActionSchema, {
2514
+ userMessage: createCursorUserMessage(userContent, userText),
2515
+ }),
2516
+ }
2517
+ : {
2518
+ case: "resumeAction",
2519
+ value: create(ResumeActionSchema, {}),
2520
+ },
2521
+ });
2522
+
2523
+ // Build conversation turns from prior messages, excluding only the active user message
2524
+ // when the request is sending one. Resume actions must preserve trailing tool results.
2525
+ const turns = buildConversationTurns(context.messages, blobStore, activeUserMessage ? activeUserMessageIndex : -1);
2526
+
2527
+ // Build `rootPromptMessagesJson` from prior messages. Cursor's server uses this
2528
+ // field (not `turns[]`) to construct the actual model prompt; if we only send the
2529
+ // system prompt here, multi-turn conversations lose prior context and the model
2530
+ // sees only the current user message.
2531
+ const rootPromptMessagesJson = buildRootPromptMessagesJson(
2532
+ context.messages,
2533
+ systemPromptIds,
2534
+ blobStore,
2535
+ activeUserMessage ? activeUserMessageIndex : -1,
2536
+ );
2537
+
2538
+ // Preserve cached non-history state fields (todos, file states, summaries, etc.)
2539
+ // when the system prompt is unchanged; otherwise start fresh.
2540
+ const cachedPromptHead = state.conversationState?.rootPromptMessagesJson?.slice(0, systemPromptIds.length) ?? [];
2541
+ const hasMatchingPrompt =
2542
+ cachedPromptHead.length === systemPromptIds.length &&
2543
+ systemPromptIds.every((id, idx) => Buffer.from(cachedPromptHead[idx]).equals(id));
2544
+ const baseState =
2545
+ state.conversationState && hasMatchingPrompt
2546
+ ? state.conversationState
2547
+ : create(ConversationStateStructureSchema, {
2548
+ rootPromptMessagesJson: systemPromptIds,
2549
+ turns: [],
2550
+ todos: [],
2551
+ pendingToolCalls: [],
2552
+ previousWorkspaceUris: [],
2553
+ fileStates: {},
2554
+ fileStatesV2: {},
2555
+ summaryArchives: [],
2556
+ turnTimings: [],
2557
+ subagentStates: {},
2558
+ selfSummaryCount: 0,
2559
+ readPaths: [],
2560
+ });
2561
+
2562
+ // Always override `rootPromptMessagesJson` and `turns` with content freshly built from
2563
+ // `context.messages`. The server-echoed checkpoint replaces historical user entries
2564
+ // with empty placeholders, so we cannot rely on the cached `rootPromptMessagesJson`.
2565
+ const conversationState = create(ConversationStateStructureSchema, {
2566
+ ...baseState,
2567
+ rootPromptMessagesJson,
2568
+ turns,
2569
+ });
2570
+
2571
+ const modelDetails = create(ModelDetailsSchema, {
2572
+ modelId: model.id,
2573
+ displayModelId: model.id,
2574
+ displayName: model.name,
2575
+ });
2576
+
2577
+ const runRequest = create(AgentRunRequestSchema, {
2578
+ conversationState,
2579
+ action,
2580
+ modelDetails,
2581
+ conversationId: state.conversationId,
2582
+ });
2583
+
2584
+ options?.onPayload?.(runRequest);
2585
+
2586
+ // Tools are sent later via requestContext (exec handshake)
2587
+
2588
+ if (options?.customSystemPrompt) {
2589
+ runRequest.customSystemPrompt = options.customSystemPrompt;
2590
+ }
2591
+
2592
+ const clientMessage = create(AgentClientMessageSchema, {
2593
+ message: { case: "runRequest", value: runRequest },
2594
+ });
2595
+
2596
+ const requestBytes = toBinary(AgentClientMessageSchema, clientMessage);
2597
+
2598
+ const toolNames = context.tools?.map(tool => tool.name) ?? [];
2599
+ const detail =
2600
+ $env.DEBUG_CURSOR === "2"
2601
+ ? ` ${JSON.stringify(clientMessage.message.value, debugReplacer, 2)?.slice(0, 2000)}`
2602
+ : "";
2603
+ log("info", "builtRunRequest", {
2604
+ bytes: requestBytes.length,
2605
+ tools: toolNames.length,
2606
+ toolNames: toolNames.slice(0, 20),
2607
+ detail: detail || undefined,
2608
+ });
2609
+
2610
+ return { requestBytes, blobStore, conversationState };
2611
+ }
2612
+
2613
+ function hasImages(content: (TextContent | ImageContent)[]): boolean {
2614
+ return content.some(item => item.type === "image");
2615
+ }
2616
+ function extractText(content: (TextContent | ImageContent)[]): string {
2617
+ return content
2618
+ .filter((c): c is TextContent => c.type === "text")
2619
+ .map(c => c.text)
2620
+ .join("\n");
2621
+ }