@vib-rato/ai 0.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (525) hide show
  1. package/CHANGELOG.md +3354 -0
  2. package/README.md +1194 -0
  3. package/dist/types/adapter-internals/provider-safety-stop.d.ts +45 -0
  4. package/dist/types/api-registry.d.ts +30 -0
  5. package/dist/types/auth-broker/client.d.ts +80 -0
  6. package/dist/types/auth-broker/index.d.ts +5 -0
  7. package/dist/types/auth-broker/redact.d.ts +7 -0
  8. package/dist/types/auth-broker/refresher.d.ts +25 -0
  9. package/dist/types/auth-broker/remote-store.d.ts +153 -0
  10. package/dist/types/auth-broker/server.d.ts +32 -0
  11. package/dist/types/auth-broker/types.d.ts +132 -0
  12. package/dist/types/auth-broker/wire-schemas.d.ts +555 -0
  13. package/dist/types/auth-gateway/http.d.ts +40 -0
  14. package/dist/types/auth-gateway/index.d.ts +3 -0
  15. package/dist/types/auth-gateway/server.d.ts +70 -0
  16. package/dist/types/auth-gateway/types.d.ts +129 -0
  17. package/dist/types/auth-storage.d.ts +1074 -0
  18. package/dist/types/cli.d.ts +2 -0
  19. package/dist/types/codex-tools.d.ts +4 -0
  20. package/dist/types/context-cap-policy.d.ts +68 -0
  21. package/dist/types/core.d.ts +35 -0
  22. package/dist/types/index.d.ts +55 -0
  23. package/dist/types/model-cache.d.ts +24 -0
  24. package/dist/types/model-manager.d.ts +77 -0
  25. package/dist/types/model-pricing.d.ts +3 -0
  26. package/dist/types/model-retirements.d.ts +6 -0
  27. package/dist/types/model-thinking.d.ts +100 -0
  28. package/dist/types/models.d.ts +21 -0
  29. package/dist/types/openai-completions-compat.d.ts +34 -0
  30. package/dist/types/provider-details.d.ts +24 -0
  31. package/dist/types/provider-models/bundled-references.d.ts +4 -0
  32. package/dist/types/provider-models/descriptors.d.ts +48 -0
  33. package/dist/types/provider-models/google.d.ts +20 -0
  34. package/dist/types/provider-models/index.d.ts +5 -0
  35. package/dist/types/provider-models/ollama.d.ts +7 -0
  36. package/dist/types/provider-models/openai-compat.d.ts +293 -0
  37. package/dist/types/provider-models/special.d.ts +29 -0
  38. package/dist/types/providers/amazon-bedrock.d.ts +60 -0
  39. package/dist/types/providers/anthropic-messages-server-schema.d.ts +450 -0
  40. package/dist/types/providers/anthropic-messages-server.d.ts +17 -0
  41. package/dist/types/providers/anthropic.d.ts +280 -0
  42. package/dist/types/providers/aws-credential-config.d.ts +19 -0
  43. package/dist/types/providers/aws-credentials.d.ts +43 -0
  44. package/dist/types/providers/aws-eventstream.d.ts +38 -0
  45. package/dist/types/providers/aws-sigv4.d.ts +55 -0
  46. package/dist/types/providers/azure-openai-responses.d.ts +22 -0
  47. package/dist/types/providers/composer-discipline.d.ts +32 -0
  48. package/dist/types/providers/cursor/client-version.d.ts +10 -0
  49. package/dist/types/providers/cursor/exec-modern.d.ts +98 -0
  50. package/dist/types/providers/cursor/gen/agent_pb.d.ts +16769 -0
  51. package/dist/types/providers/cursor-pi-args.d.ts +119 -0
  52. package/dist/types/providers/cursor.d.ts +72 -0
  53. package/dist/types/providers/dashscope-token-plan-headers.d.ts +57 -0
  54. package/dist/types/providers/error-message.d.ts +27 -0
  55. package/dist/types/providers/github-copilot-headers.d.ts +40 -0
  56. package/dist/types/providers/gitlab-duo.d.ts +27 -0
  57. package/dist/types/providers/google-auth.d.ts +26 -0
  58. package/dist/types/providers/google-gemini-cli.d.ts +75 -0
  59. package/dist/types/providers/google-gemini-headers.d.ts +43 -0
  60. package/dist/types/providers/google-shared.d.ts +183 -0
  61. package/dist/types/providers/google-types.d.ts +138 -0
  62. package/dist/types/providers/google-vertex.d.ts +11 -0
  63. package/dist/types/providers/google.d.ts +4 -0
  64. package/dist/types/providers/grammar.d.ts +1 -0
  65. package/dist/types/providers/kimi.d.ts +27 -0
  66. package/dist/types/providers/kiro-api-key.d.ts +50 -0
  67. package/dist/types/providers/kiro-codewhisperer.d.ts +11 -0
  68. package/dist/types/providers/mock.d.ts +189 -0
  69. package/dist/types/providers/ollama.d.ts +41 -0
  70. package/dist/types/providers/openai-anthropic-shim.d.ts +31 -0
  71. package/dist/types/providers/openai-bounded-rate-limits.d.ts +3 -0
  72. package/dist/types/providers/openai-chat-server-schema.d.ts +1733 -0
  73. package/dist/types/providers/openai-chat-server.d.ts +16 -0
  74. package/dist/types/providers/openai-codex/constants.d.ts +26 -0
  75. package/dist/types/providers/openai-codex/request-transformer.d.ts +50 -0
  76. package/dist/types/providers/openai-codex/response-handler.d.ts +18 -0
  77. package/dist/types/providers/openai-codex-responses.d.ts +71 -0
  78. package/dist/types/providers/openai-completions-compat.d.ts +6 -0
  79. package/dist/types/providers/openai-completions.d.ts +35 -0
  80. package/dist/types/providers/openai-opencodex-responses.d.ts +10 -0
  81. package/dist/types/providers/openai-request-transform.d.ts +4 -0
  82. package/dist/types/providers/openai-responses-server-schema.d.ts +392 -0
  83. package/dist/types/providers/openai-responses-server.d.ts +17 -0
  84. package/dist/types/providers/openai-responses-shared.d.ts +106 -0
  85. package/dist/types/providers/openai-responses.d.ts +37 -0
  86. package/dist/types/providers/pi-native-client.d.ts +29 -0
  87. package/dist/types/providers/pi-native-server.d.ts +60 -0
  88. package/dist/types/providers/register-builtins.d.ts +59 -0
  89. package/dist/types/providers/synthetic.d.ts +26 -0
  90. package/dist/types/providers/transform-messages.d.ts +33 -0
  91. package/dist/types/providers/vision-guard.d.ts +8 -0
  92. package/dist/types/rate-limit-utils.d.ts +19 -0
  93. package/dist/types/stream.d.ts +45 -0
  94. package/dist/types/types.d.ts +1073 -0
  95. package/dist/types/usage/claude.d.ts +3 -0
  96. package/dist/types/usage/gemini.d.ts +2 -0
  97. package/dist/types/usage/github-copilot.d.ts +7 -0
  98. package/dist/types/usage/google-antigravity.d.ts +2 -0
  99. package/dist/types/usage/grok-cli.d.ts +17 -0
  100. package/dist/types/usage/kimi.d.ts +4 -0
  101. package/dist/types/usage/minimax-code.d.ts +2 -0
  102. package/dist/types/usage/openai-codex.d.ts +3 -0
  103. package/dist/types/usage/shared.d.ts +1 -0
  104. package/dist/types/usage/zai.d.ts +2 -0
  105. package/dist/types/usage.d.ts +264 -0
  106. package/dist/types/utils/abort.d.ts +19 -0
  107. package/dist/types/utils/anthropic-auth.d.ts +39 -0
  108. package/dist/types/utils/block-symbols.d.ts +6 -0
  109. package/dist/types/utils/discovery/antigravity.d.ts +67 -0
  110. package/dist/types/utils/discovery/codex.d.ts +38 -0
  111. package/dist/types/utils/discovery/cursor.d.ts +49 -0
  112. package/dist/types/utils/discovery/gemini.d.ts +25 -0
  113. package/dist/types/utils/discovery/index.d.ts +4 -0
  114. package/dist/types/utils/discovery/openai-compatible.d.ts +81 -0
  115. package/dist/types/utils/event-stream.d.ts +41 -0
  116. package/dist/types/utils/fallback-transport.d.ts +110 -0
  117. package/dist/types/utils/fireworks-model-id.d.ts +10 -0
  118. package/dist/types/utils/foundry.d.ts +11 -0
  119. package/dist/types/utils/h2-fetch.d.ts +22 -0
  120. package/dist/types/utils/http-inspector.d.ts +59 -0
  121. package/dist/types/utils/idle-iterator.d.ts +122 -0
  122. package/dist/types/utils/json-parse.d.ts +98 -0
  123. package/dist/types/utils/oauth/alibaba-token-plan.d.ts +19 -0
  124. package/dist/types/utils/oauth/anthropic.d.ts +41 -0
  125. package/dist/types/utils/oauth/api-key-login.d.ts +38 -0
  126. package/dist/types/utils/oauth/api-key-validation.d.ts +39 -0
  127. package/dist/types/utils/oauth/bizrouter.d.ts +1 -0
  128. package/dist/types/utils/oauth/callback-server.d.ts +80 -0
  129. package/dist/types/utils/oauth/cerebras.d.ts +1 -0
  130. package/dist/types/utils/oauth/cloudflare-ai-gateway.d.ts +18 -0
  131. package/dist/types/utils/oauth/commandcode.d.ts +1 -0
  132. package/dist/types/utils/oauth/cursor.d.ts +15 -0
  133. package/dist/types/utils/oauth/deepinfra.d.ts +1 -0
  134. package/dist/types/utils/oauth/deepseek.d.ts +10 -0
  135. package/dist/types/utils/oauth/firepass.d.ts +1 -0
  136. package/dist/types/utils/oauth/fireworks.d.ts +1 -0
  137. package/dist/types/utils/oauth/fugu.d.ts +1 -0
  138. package/dist/types/utils/oauth/github-copilot.d.ts +38 -0
  139. package/dist/types/utils/oauth/gitlab-duo.d.ts +3 -0
  140. package/dist/types/utils/oauth/glm-zcode.d.ts +71 -0
  141. package/dist/types/utils/oauth/google-antigravity.d.ts +11 -0
  142. package/dist/types/utils/oauth/google-gemini-cli.d.ts +10 -0
  143. package/dist/types/utils/oauth/google-oauth-shared.d.ts +28 -0
  144. package/dist/types/utils/oauth/huggingface.d.ts +19 -0
  145. package/dist/types/utils/oauth/index.d.ts +39 -0
  146. package/dist/types/utils/oauth/kagi.d.ts +17 -0
  147. package/dist/types/utils/oauth/kilo.d.ts +5 -0
  148. package/dist/types/utils/oauth/kimi.d.ts +17 -0
  149. package/dist/types/utils/oauth/kiro.d.ts +71 -0
  150. package/dist/types/utils/oauth/litellm.d.ts +18 -0
  151. package/dist/types/utils/oauth/lm-studio.d.ts +17 -0
  152. package/dist/types/utils/oauth/mara.d.ts +1 -0
  153. package/dist/types/utils/oauth/minimax-code.d.ts +28 -0
  154. package/dist/types/utils/oauth/moonshot.d.ts +1 -0
  155. package/dist/types/utils/oauth/nanogpt.d.ts +1 -0
  156. package/dist/types/utils/oauth/nvidia.d.ts +18 -0
  157. package/dist/types/utils/oauth/ollama-cloud.d.ts +2 -0
  158. package/dist/types/utils/oauth/ollama.d.ts +18 -0
  159. package/dist/types/utils/oauth/openai-codex.d.ts +21 -0
  160. package/dist/types/utils/oauth/opencode.d.ts +18 -0
  161. package/dist/types/utils/oauth/opengateway.d.ts +1 -0
  162. package/dist/types/utils/oauth/openrouter.d.ts +1 -0
  163. package/dist/types/utils/oauth/parallel.d.ts +17 -0
  164. package/dist/types/utils/oauth/perplexity.d.ts +4 -0
  165. package/dist/types/utils/oauth/pkce.d.ts +8 -0
  166. package/dist/types/utils/oauth/qianfan.d.ts +17 -0
  167. package/dist/types/utils/oauth/qwen-portal.d.ts +19 -0
  168. package/dist/types/utils/oauth/sglang.d.ts +16 -0
  169. package/dist/types/utils/oauth/synthetic.d.ts +1 -0
  170. package/dist/types/utils/oauth/tavily.d.ts +17 -0
  171. package/dist/types/utils/oauth/together.d.ts +1 -0
  172. package/dist/types/utils/oauth/types.d.ts +56 -0
  173. package/dist/types/utils/oauth/venice.d.ts +18 -0
  174. package/dist/types/utils/oauth/vercel-ai-gateway.d.ts +18 -0
  175. package/dist/types/utils/oauth/vllm.d.ts +16 -0
  176. package/dist/types/utils/oauth/xai.d.ts +30 -0
  177. package/dist/types/utils/oauth/xiaomi.d.ts +25 -0
  178. package/dist/types/utils/oauth/zai.d.ts +18 -0
  179. package/dist/types/utils/oauth/zenmux.d.ts +1 -0
  180. package/dist/types/utils/overflow.d.ts +14 -0
  181. package/dist/types/utils/parse-bind.d.ts +26 -0
  182. package/dist/types/utils/provider-response.d.ts +6 -0
  183. package/dist/types/utils/provider-safety-stop.d.ts +8 -0
  184. package/dist/types/utils/proxy.d.ts +7 -0
  185. package/dist/types/utils/retry-after.d.ts +3 -0
  186. package/dist/types/utils/retry-budget.d.ts +1 -0
  187. package/dist/types/utils/retry.d.ts +29 -0
  188. package/dist/types/utils/schema/adapt.d.ts +24 -0
  189. package/dist/types/utils/schema/compatibility.d.ts +30 -0
  190. package/dist/types/utils/schema/dereference.d.ts +11 -0
  191. package/dist/types/utils/schema/draft.d.ts +10 -0
  192. package/dist/types/utils/schema/equality.d.ts +4 -0
  193. package/dist/types/utils/schema/fields.d.ts +49 -0
  194. package/dist/types/utils/schema/index.d.ts +14 -0
  195. package/dist/types/utils/schema/json-schema-validator.d.ts +12 -0
  196. package/dist/types/utils/schema/meta-validator.d.ts +2 -0
  197. package/dist/types/utils/schema/normalize.d.ts +93 -0
  198. package/dist/types/utils/schema/root-combinator.d.ts +12 -0
  199. package/dist/types/utils/schema/spill.d.ts +8 -0
  200. package/dist/types/utils/schema/stamps.d.ts +25 -0
  201. package/dist/types/utils/schema/types.d.ts +4 -0
  202. package/dist/types/utils/schema/wire.d.ts +54 -0
  203. package/dist/types/utils/schema/zod-decontaminate.d.ts +31 -0
  204. package/dist/types/utils/sse-debug.d.ts +10 -0
  205. package/dist/types/utils/tool-call-healing.d.ts +80 -0
  206. package/dist/types/utils/tool-choice-capability.d.ts +57 -0
  207. package/dist/types/utils/tool-choice.d.ts +50 -0
  208. package/dist/types/utils/validation.d.ts +17 -0
  209. package/dist/types/utils.d.ts +117 -0
  210. package/package.json +152 -0
  211. package/src/adapter-internals/provider-safety-stop.d.ts +45 -0
  212. package/src/adapter-internals/provider-safety-stop.ts +156 -0
  213. package/src/api-registry.d.ts +30 -0
  214. package/src/api-registry.ts +96 -0
  215. package/src/auth-broker/client.ts +444 -0
  216. package/src/auth-broker/index.ts +5 -0
  217. package/src/auth-broker/redact.ts +39 -0
  218. package/src/auth-broker/refresher.ts +130 -0
  219. package/src/auth-broker/remote-store.ts +1576 -0
  220. package/src/auth-broker/server.ts +764 -0
  221. package/src/auth-broker/types.ts +164 -0
  222. package/src/auth-broker/wire-schemas.ts +261 -0
  223. package/src/auth-gateway/http.ts +198 -0
  224. package/src/auth-gateway/index.ts +3 -0
  225. package/src/auth-gateway/server.ts +1315 -0
  226. package/src/auth-gateway/types.ts +160 -0
  227. package/src/auth-storage.ts +7312 -0
  228. package/src/cli.ts +269 -0
  229. package/src/codex-tools.d.ts +4 -0
  230. package/src/codex-tools.ts +24 -0
  231. package/src/context-cap-policy.d.ts +68 -0
  232. package/src/context-cap-policy.ts +123 -0
  233. package/src/core.ts +44 -0
  234. package/src/index.ts +61 -0
  235. package/src/model-cache.ts +236 -0
  236. package/src/model-manager.ts +744 -0
  237. package/src/model-pricing.d.ts +3 -0
  238. package/src/model-pricing.ts +68 -0
  239. package/src/model-retirements.d.ts +6 -0
  240. package/src/model-retirements.ts +19 -0
  241. package/src/model-thinking.d.ts +100 -0
  242. package/src/model-thinking.ts +1054 -0
  243. package/src/models.d.ts +21 -0
  244. package/src/models.json +94672 -0
  245. package/src/models.json.d.ts +9 -0
  246. package/src/models.ts +126 -0
  247. package/src/openai-completions-compat.d.ts +34 -0
  248. package/src/openai-completions-compat.ts +383 -0
  249. package/src/prompts/composer-bash-policy-recovery.md +1 -0
  250. package/src/prompts/cursor-composer-bash-policy-recovery.md +1 -0
  251. package/src/prompts/cursor-composer-edit-discipline.md +7 -0
  252. package/src/prompts/turn-aborted-guidance.md +4 -0
  253. package/src/provider-details.ts +90 -0
  254. package/src/provider-models/bundled-references.ts +38 -0
  255. package/src/provider-models/descriptors.ts +392 -0
  256. package/src/provider-models/google.ts +92 -0
  257. package/src/provider-models/index.ts +5 -0
  258. package/src/provider-models/ollama.ts +159 -0
  259. package/src/provider-models/openai-compat.ts +2920 -0
  260. package/src/provider-models/special.ts +185 -0
  261. package/src/providers/amazon-bedrock.d.ts +60 -0
  262. package/src/providers/amazon-bedrock.ts +939 -0
  263. package/src/providers/anthropic-messages-server-schema.ts +229 -0
  264. package/src/providers/anthropic-messages-server.ts +839 -0
  265. package/src/providers/anthropic.d.ts +280 -0
  266. package/src/providers/anthropic.ts +4421 -0
  267. package/src/providers/aws-credential-config.d.ts +19 -0
  268. package/src/providers/aws-credential-config.ts +179 -0
  269. package/src/providers/aws-credentials.d.ts +43 -0
  270. package/src/providers/aws-credentials.ts +457 -0
  271. package/src/providers/aws-eventstream.d.ts +38 -0
  272. package/src/providers/aws-eventstream.ts +185 -0
  273. package/src/providers/aws-sigv4.d.ts +55 -0
  274. package/src/providers/aws-sigv4.ts +218 -0
  275. package/src/providers/azure-openai-responses.d.ts +22 -0
  276. package/src/providers/azure-openai-responses.ts +447 -0
  277. package/src/providers/composer-discipline.d.ts +32 -0
  278. package/src/providers/composer-discipline.ts +95 -0
  279. package/src/providers/cursor/client-version.d.ts +10 -0
  280. package/src/providers/cursor/client-version.ts +10 -0
  281. package/src/providers/cursor/exec-modern.d.ts +98 -0
  282. package/src/providers/cursor/exec-modern.ts +497 -0
  283. package/src/providers/cursor/gen/agent_pb.d.ts +16769 -0
  284. package/src/providers/cursor/gen/agent_pb.ts +19780 -0
  285. package/src/providers/cursor/proto/agent.proto +4533 -0
  286. package/src/providers/cursor/proto/buf.gen.yaml +6 -0
  287. package/src/providers/cursor/proto/buf.yaml +17 -0
  288. package/src/providers/cursor-pi-args.d.ts +119 -0
  289. package/src/providers/cursor-pi-args.ts +187 -0
  290. package/src/providers/cursor.d.ts +72 -0
  291. package/src/providers/cursor.ts +3396 -0
  292. package/src/providers/dashscope-token-plan-headers.d.ts +57 -0
  293. package/src/providers/dashscope-token-plan-headers.ts +84 -0
  294. package/src/providers/error-message.d.ts +27 -0
  295. package/src/providers/error-message.ts +21 -0
  296. package/src/providers/github-copilot-headers.d.ts +40 -0
  297. package/src/providers/github-copilot-headers.ts +140 -0
  298. package/src/providers/gitlab-duo.d.ts +27 -0
  299. package/src/providers/gitlab-duo.ts +393 -0
  300. package/src/providers/google-auth.d.ts +26 -0
  301. package/src/providers/google-auth.ts +262 -0
  302. package/src/providers/google-gemini-cli.d.ts +75 -0
  303. package/src/providers/google-gemini-cli.ts +969 -0
  304. package/src/providers/google-gemini-headers.d.ts +43 -0
  305. package/src/providers/google-gemini-headers.ts +100 -0
  306. package/src/providers/google-shared.d.ts +183 -0
  307. package/src/providers/google-shared.ts +1105 -0
  308. package/src/providers/google-types.d.ts +138 -0
  309. package/src/providers/google-types.ts +167 -0
  310. package/src/providers/google-vertex.d.ts +11 -0
  311. package/src/providers/google-vertex.ts +124 -0
  312. package/src/providers/google.d.ts +4 -0
  313. package/src/providers/google.ts +41 -0
  314. package/src/providers/grammar.d.ts +1 -0
  315. package/src/providers/grammar.ts +70 -0
  316. package/src/providers/kimi.d.ts +27 -0
  317. package/src/providers/kimi.ts +52 -0
  318. package/src/providers/kiro-api-key.d.ts +50 -0
  319. package/src/providers/kiro-api-key.ts +786 -0
  320. package/src/providers/kiro-codewhisperer.d.ts +11 -0
  321. package/src/providers/kiro-codewhisperer.ts +600 -0
  322. package/src/providers/mock.ts +526 -0
  323. package/src/providers/ollama.d.ts +41 -0
  324. package/src/providers/ollama.ts +645 -0
  325. package/src/providers/openai-anthropic-shim.d.ts +31 -0
  326. package/src/providers/openai-anthropic-shim.ts +156 -0
  327. package/src/providers/openai-bounded-rate-limits.d.ts +3 -0
  328. package/src/providers/openai-bounded-rate-limits.ts +57 -0
  329. package/src/providers/openai-chat-server-schema.ts +254 -0
  330. package/src/providers/openai-chat-server.ts +724 -0
  331. package/src/providers/openai-codex/constants.d.ts +26 -0
  332. package/src/providers/openai-codex/constants.ts +43 -0
  333. package/src/providers/openai-codex/request-transformer.d.ts +50 -0
  334. package/src/providers/openai-codex/request-transformer.ts +219 -0
  335. package/src/providers/openai-codex/response-handler.d.ts +18 -0
  336. package/src/providers/openai-codex/response-handler.ts +111 -0
  337. package/src/providers/openai-codex-responses.d.ts +71 -0
  338. package/src/providers/openai-codex-responses.ts +3288 -0
  339. package/src/providers/openai-completions-compat.d.ts +6 -0
  340. package/src/providers/openai-completions-compat.ts +6 -0
  341. package/src/providers/openai-completions.d.ts +35 -0
  342. package/src/providers/openai-completions.ts +2294 -0
  343. package/src/providers/openai-opencodex-responses.ts +174 -0
  344. package/src/providers/openai-request-transform.d.ts +4 -0
  345. package/src/providers/openai-request-transform.ts +136 -0
  346. package/src/providers/openai-responses-server-schema.ts +290 -0
  347. package/src/providers/openai-responses-server.ts +1268 -0
  348. package/src/providers/openai-responses-shared.d.ts +106 -0
  349. package/src/providers/openai-responses-shared.ts +1253 -0
  350. package/src/providers/openai-responses.d.ts +37 -0
  351. package/src/providers/openai-responses.ts +989 -0
  352. package/src/providers/pi-native-client.d.ts +29 -0
  353. package/src/providers/pi-native-client.ts +243 -0
  354. package/src/providers/pi-native-server.ts +488 -0
  355. package/src/providers/register-builtins.d.ts +59 -0
  356. package/src/providers/register-builtins.ts +544 -0
  357. package/src/providers/synthetic.d.ts +26 -0
  358. package/src/providers/synthetic.ts +50 -0
  359. package/src/providers/transform-messages.d.ts +33 -0
  360. package/src/providers/transform-messages.ts +408 -0
  361. package/src/providers/vision-guard.d.ts +8 -0
  362. package/src/providers/vision-guard.ts +31 -0
  363. package/src/rate-limit-utils.d.ts +19 -0
  364. package/src/rate-limit-utils.ts +102 -0
  365. package/src/stream.d.ts +45 -0
  366. package/src/stream.ts +1306 -0
  367. package/src/types.d.ts +1073 -0
  368. package/src/types.ts +1305 -0
  369. package/src/usage/claude.ts +449 -0
  370. package/src/usage/gemini.ts +250 -0
  371. package/src/usage/github-copilot.ts +421 -0
  372. package/src/usage/google-antigravity.ts +201 -0
  373. package/src/usage/grok-cli.ts +259 -0
  374. package/src/usage/kimi.ts +285 -0
  375. package/src/usage/minimax-code.ts +31 -0
  376. package/src/usage/openai-codex.ts +503 -0
  377. package/src/usage/shared.ts +10 -0
  378. package/src/usage/zai.ts +247 -0
  379. package/src/usage.ts +190 -0
  380. package/src/utils/abort.d.ts +19 -0
  381. package/src/utils/abort.ts +51 -0
  382. package/src/utils/anthropic-auth.ts +95 -0
  383. package/src/utils/block-symbols.d.ts +6 -0
  384. package/src/utils/block-symbols.ts +11 -0
  385. package/src/utils/discovery/antigravity.ts +275 -0
  386. package/src/utils/discovery/codex.ts +362 -0
  387. package/src/utils/discovery/cursor.ts +388 -0
  388. package/src/utils/discovery/gemini.ts +248 -0
  389. package/src/utils/discovery/index.ts +4 -0
  390. package/src/utils/discovery/openai-compatible.ts +379 -0
  391. package/src/utils/event-stream.d.ts +41 -0
  392. package/src/utils/event-stream.ts +269 -0
  393. package/src/utils/fallback-transport.d.ts +110 -0
  394. package/src/utils/fallback-transport.ts +411 -0
  395. package/src/utils/fireworks-model-id.d.ts +10 -0
  396. package/src/utils/fireworks-model-id.ts +30 -0
  397. package/src/utils/foundry.d.ts +11 -0
  398. package/src/utils/foundry.ts +18 -0
  399. package/src/utils/h2-fetch.ts +60 -0
  400. package/src/utils/http-inspector.d.ts +59 -0
  401. package/src/utils/http-inspector.ts +380 -0
  402. package/src/utils/idle-iterator.d.ts +122 -0
  403. package/src/utils/idle-iterator.ts +410 -0
  404. package/src/utils/json-parse.d.ts +98 -0
  405. package/src/utils/json-parse.ts +607 -0
  406. package/src/utils/oauth/alibaba-token-plan.ts +60 -0
  407. package/src/utils/oauth/anthropic.ts +233 -0
  408. package/src/utils/oauth/api-key-login.ts +98 -0
  409. package/src/utils/oauth/api-key-validation.ts +344 -0
  410. package/src/utils/oauth/bizrouter.ts +15 -0
  411. package/src/utils/oauth/callback-server.d.ts +80 -0
  412. package/src/utils/oauth/callback-server.ts +359 -0
  413. package/src/utils/oauth/cerebras.ts +16 -0
  414. package/src/utils/oauth/cloudflare-ai-gateway.ts +48 -0
  415. package/src/utils/oauth/commandcode.ts +17 -0
  416. package/src/utils/oauth/cursor.ts +157 -0
  417. package/src/utils/oauth/deepinfra.ts +15 -0
  418. package/src/utils/oauth/deepseek.ts +53 -0
  419. package/src/utils/oauth/firepass.ts +24 -0
  420. package/src/utils/oauth/fireworks.ts +15 -0
  421. package/src/utils/oauth/fugu.ts +15 -0
  422. package/src/utils/oauth/github-copilot.d.ts +38 -0
  423. package/src/utils/oauth/github-copilot.ts +362 -0
  424. package/src/utils/oauth/gitlab-duo.ts +123 -0
  425. package/src/utils/oauth/glm-zcode.d.ts +71 -0
  426. package/src/utils/oauth/glm-zcode.ts +433 -0
  427. package/src/utils/oauth/google-antigravity.ts +200 -0
  428. package/src/utils/oauth/google-gemini-cli.ts +256 -0
  429. package/src/utils/oauth/google-oauth-shared.ts +110 -0
  430. package/src/utils/oauth/huggingface.ts +62 -0
  431. package/src/utils/oauth/index.ts +558 -0
  432. package/src/utils/oauth/kagi.ts +47 -0
  433. package/src/utils/oauth/kilo.ts +87 -0
  434. package/src/utils/oauth/kimi.d.ts +17 -0
  435. package/src/utils/oauth/kimi.ts +275 -0
  436. package/src/utils/oauth/kiro.ts +448 -0
  437. package/src/utils/oauth/litellm.ts +47 -0
  438. package/src/utils/oauth/lm-studio.ts +38 -0
  439. package/src/utils/oauth/mara.ts +16 -0
  440. package/src/utils/oauth/minimax-code.ts +78 -0
  441. package/src/utils/oauth/moonshot.ts +16 -0
  442. package/src/utils/oauth/nanogpt.ts +15 -0
  443. package/src/utils/oauth/nvidia.ts +70 -0
  444. package/src/utils/oauth/oauth.html +199 -0
  445. package/src/utils/oauth/ollama-cloud.ts +28 -0
  446. package/src/utils/oauth/ollama.ts +47 -0
  447. package/src/utils/oauth/openai-codex.ts +299 -0
  448. package/src/utils/oauth/opencode.ts +49 -0
  449. package/src/utils/oauth/opengateway.ts +15 -0
  450. package/src/utils/oauth/openrouter.ts +16 -0
  451. package/src/utils/oauth/parallel.ts +46 -0
  452. package/src/utils/oauth/perplexity.ts +225 -0
  453. package/src/utils/oauth/pkce.ts +18 -0
  454. package/src/utils/oauth/qianfan.ts +58 -0
  455. package/src/utils/oauth/qwen-portal.ts +60 -0
  456. package/src/utils/oauth/sglang.ts +42 -0
  457. package/src/utils/oauth/synthetic.ts +15 -0
  458. package/src/utils/oauth/tavily.ts +46 -0
  459. package/src/utils/oauth/together.ts +16 -0
  460. package/src/utils/oauth/types.d.ts +56 -0
  461. package/src/utils/oauth/types.ts +122 -0
  462. package/src/utils/oauth/venice.ts +59 -0
  463. package/src/utils/oauth/vercel-ai-gateway.ts +47 -0
  464. package/src/utils/oauth/vllm.ts +42 -0
  465. package/src/utils/oauth/xai.ts +246 -0
  466. package/src/utils/oauth/xiaomi.ts +199 -0
  467. package/src/utils/oauth/zai.ts +60 -0
  468. package/src/utils/oauth/zenmux.ts +15 -0
  469. package/src/utils/overflow.ts +275 -0
  470. package/src/utils/parse-bind.ts +81 -0
  471. package/src/utils/provider-response.d.ts +6 -0
  472. package/src/utils/provider-response.ts +30 -0
  473. package/src/utils/provider-safety-stop.ts +8 -0
  474. package/src/utils/proxy.d.ts +7 -0
  475. package/src/utils/proxy.ts +652 -0
  476. package/src/utils/retry-after.d.ts +3 -0
  477. package/src/utils/retry-after.ts +110 -0
  478. package/src/utils/retry-budget.d.ts +1 -0
  479. package/src/utils/retry-budget.ts +4 -0
  480. package/src/utils/retry.d.ts +29 -0
  481. package/src/utils/retry.ts +67 -0
  482. package/src/utils/schema/CONSTRAINTS.md +164 -0
  483. package/src/utils/schema/adapt.d.ts +24 -0
  484. package/src/utils/schema/adapt.ts +36 -0
  485. package/src/utils/schema/compatibility.d.ts +30 -0
  486. package/src/utils/schema/compatibility.ts +435 -0
  487. package/src/utils/schema/dereference.d.ts +11 -0
  488. package/src/utils/schema/dereference.ts +98 -0
  489. package/src/utils/schema/draft.d.ts +10 -0
  490. package/src/utils/schema/draft.ts +341 -0
  491. package/src/utils/schema/equality.d.ts +4 -0
  492. package/src/utils/schema/equality.ts +97 -0
  493. package/src/utils/schema/fields.d.ts +49 -0
  494. package/src/utils/schema/fields.ts +190 -0
  495. package/src/utils/schema/index.d.ts +14 -0
  496. package/src/utils/schema/index.ts +14 -0
  497. package/src/utils/schema/json-schema-validator.d.ts +12 -0
  498. package/src/utils/schema/json-schema-validator.ts +577 -0
  499. package/src/utils/schema/meta-validator.d.ts +2 -0
  500. package/src/utils/schema/meta-validator.ts +167 -0
  501. package/src/utils/schema/normalize.d.ts +93 -0
  502. package/src/utils/schema/normalize.ts +1588 -0
  503. package/src/utils/schema/root-combinator.d.ts +12 -0
  504. package/src/utils/schema/root-combinator.ts +143 -0
  505. package/src/utils/schema/spill.d.ts +8 -0
  506. package/src/utils/schema/spill.ts +43 -0
  507. package/src/utils/schema/stamps.d.ts +25 -0
  508. package/src/utils/schema/stamps.ts +97 -0
  509. package/src/utils/schema/types.d.ts +4 -0
  510. package/src/utils/schema/types.ts +11 -0
  511. package/src/utils/schema/wire.d.ts +54 -0
  512. package/src/utils/schema/wire.ts +213 -0
  513. package/src/utils/schema/zod-decontaminate.d.ts +31 -0
  514. package/src/utils/schema/zod-decontaminate.ts +331 -0
  515. package/src/utils/sse-debug.d.ts +10 -0
  516. package/src/utils/sse-debug.ts +289 -0
  517. package/src/utils/tool-call-healing.d.ts +80 -0
  518. package/src/utils/tool-call-healing.ts +298 -0
  519. package/src/utils/tool-choice-capability.d.ts +57 -0
  520. package/src/utils/tool-choice-capability.ts +633 -0
  521. package/src/utils/tool-choice.d.ts +50 -0
  522. package/src/utils/tool-choice.ts +99 -0
  523. package/src/utils/validation.ts +1080 -0
  524. package/src/utils.d.ts +117 -0
  525. package/src/utils.ts +523 -0
@@ -0,0 +1,3396 @@
1
+ import { createHash } from "node:crypto";
2
+ import * as fs from "node:fs/promises";
3
+ import http2 from "node:http2";
4
+ import { create, fromBinary, fromJson, type JsonValue, toBinary, toJson } from "@bufbuild/protobuf";
5
+ import { ValueSchema } from "@bufbuild/protobuf/wkt";
6
+ import { $env, extractHttpStatusFromError, sanitizeText } from "@vib-rato/utils";
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 { kCursorExecResolved } from "../utils/block-symbols";
30
+ import { AssistantMessageEventStream } from "../utils/event-stream";
31
+ import { getStreamIdleTimeoutMs } from "../utils/idle-iterator";
32
+ import { captureUnicodeEscapeEvidence, parseStreamingJson } from "../utils/json-parse";
33
+ import { connectProxiedSocket, getProxyForUrl } from "../utils/proxy";
34
+ import { formatErrorMessageWithRetryAfter } from "../utils/retry-after";
35
+ import { flattenToolRootCombinators, toolWireSchema } from "../utils/schema";
36
+ import { CURSOR_COMPOSER_EDIT_DISCIPLINE_PROMPT, isComposerHarnessModel } from "./composer-discipline";
37
+ import { CURSOR_CLIENT_VERSION } from "./cursor/client-version";
38
+ import {
39
+ buildMcpStateResult,
40
+ buildNeutralHookResult,
41
+ buildPiBashError,
42
+ buildPiBashResult,
43
+ buildPiEditError,
44
+ buildPiEditRejected,
45
+ buildPiEditResult,
46
+ buildPiFindError,
47
+ buildPiFindResult,
48
+ buildPiGrepError,
49
+ buildPiGrepResult,
50
+ buildPiLsError,
51
+ buildPiLsResult,
52
+ buildPiReadError,
53
+ buildPiReadResult,
54
+ buildPiWriteError,
55
+ buildPiWriteRejected,
56
+ buildPiWriteResult,
57
+ piEscapeRegexLiteral,
58
+ piJoinPath,
59
+ piLimit,
60
+ piLsPath,
61
+ piReadDisplayPath,
62
+ piTimeout,
63
+ } from "./cursor/exec-modern";
64
+ import type { CursorRule, McpToolDefinition, RequestedModel_ModelParameterbytes } from "./cursor/gen/agent_pb";
65
+ import {
66
+ AgentClientMessageSchema,
67
+ AgentConversationTurnStructureSchema,
68
+ AgentRunRequestSchema,
69
+ type AgentServerMessage,
70
+ AgentServerMessageSchema,
71
+ AssistantMessageSchema,
72
+ BackgroundShellSpawnResultSchema,
73
+ ClientHeartbeatSchema,
74
+ ComputerUseResultSchema,
75
+ ConversationActionSchema,
76
+ type ConversationStateStructure,
77
+ ConversationStateStructureSchema,
78
+ ConversationStepSchema,
79
+ ConversationTurnStructureSchema,
80
+ CursorRuleSchema,
81
+ CursorRuleSource,
82
+ CursorRuleTypeGlobalSchema,
83
+ CursorRuleTypeSchema,
84
+ DeleteErrorSchema,
85
+ DeleteRejectedSchema,
86
+ DeleteResultSchema,
87
+ DeleteSuccessSchema,
88
+ DiagnosticsErrorSchema,
89
+ DiagnosticsRejectedSchema,
90
+ DiagnosticsResultSchema,
91
+ DiagnosticsSuccessSchema,
92
+ ExecClientControlMessageSchema,
93
+ type ExecClientMessage,
94
+ ExecClientMessageSchema,
95
+ ExecClientStreamCloseSchema,
96
+ ExecClientThrowSchema,
97
+ type ExecServerMessage,
98
+ FetchErrorSchema,
99
+ FetchResultSchema,
100
+ GetBlobResultSchema,
101
+ GrepContentMatchSchema,
102
+ GrepContentResultSchema,
103
+ GrepCountResultSchema,
104
+ GrepErrorSchema,
105
+ type GrepFileCount,
106
+ GrepFileCountSchema,
107
+ GrepFileMatchSchema,
108
+ GrepFilesResultSchema,
109
+ GrepResultSchema,
110
+ GrepSuccessSchema,
111
+ type GrepUnionResult,
112
+ GrepUnionResultSchema,
113
+ KvClientMessageSchema,
114
+ type KvServerMessage,
115
+ ListMcpResourcesExecResultSchema,
116
+ type LsDirectoryTreeNode,
117
+ type LsDirectoryTreeNode_File,
118
+ LsDirectoryTreeNode_FileSchema,
119
+ LsDirectoryTreeNodeSchema,
120
+ LsErrorSchema,
121
+ LsRejectedSchema,
122
+ LsResultSchema,
123
+ LsSuccessSchema,
124
+ McpAllowlistPrecheckResultSchema,
125
+ McpErrorSchema,
126
+ McpImageContentSchema,
127
+ McpResultSchema,
128
+ McpSuccessSchema,
129
+ McpTextContentSchema,
130
+ McpToolDefinitionSchema,
131
+ McpToolNotFoundSchema,
132
+ McpToolResultContentItemSchema,
133
+ ModelDetailsSchema,
134
+ ReadErrorSchema,
135
+ ReadMcpResourceExecResultSchema,
136
+ ReadRejectedSchema,
137
+ ReadResultSchema,
138
+ ReadSuccessSchema,
139
+ RecordScreenResultSchema,
140
+ RequestContextResultSchema,
141
+ RequestContextSchema,
142
+ RequestContextSuccessSchema,
143
+ RequestedModel_ModelParameterbytesSchema,
144
+ RequestedModelSchema,
145
+ ResumeActionSchema,
146
+ SelectedContextSchema,
147
+ SelectedImageSchema,
148
+ SetBlobResultSchema,
149
+ ShellAllowlistPrecheckResultSchema,
150
+ type ShellArgs,
151
+ ShellFailureSchema,
152
+ ShellRejectedSchema,
153
+ type ShellResult,
154
+ ShellResultSchema,
155
+ type ShellStream,
156
+ ShellStreamExitSchema,
157
+ ShellStreamSchema,
158
+ ShellStreamStartSchema,
159
+ ShellStreamStderrSchema,
160
+ ShellStreamStdoutSchema,
161
+ ShellSuccessSchema,
162
+ UserMessageActionSchema,
163
+ UserMessageSchema,
164
+ WebFetchAllowlistPrecheckResultSchema,
165
+ WriteErrorSchema,
166
+ WriteRejectedSchema,
167
+ WriteResultSchema,
168
+ WriteShellStdinErrorSchema,
169
+ WriteShellStdinResultSchema,
170
+ WriteSuccessSchema,
171
+ } from "./cursor/gen/agent_pb";
172
+
173
+ export const CURSOR_API_URL = "https://api2.cursor.sh";
174
+ export { CURSOR_CLIENT_VERSION };
175
+
176
+ const conversationStateCache = new Map<string, ConversationStateStructure>();
177
+ const conversationBlobStores = new Map<string, Map<string, Uint8Array>>();
178
+
179
+ // F15: bound the module-global conversation caches so long-lived / many-session use cannot
180
+ // grow them without limit. LRU by conversation count + TTL on idle conversations.
181
+ const CURSOR_MAX_CONVERSATIONS = 64;
182
+ const CURSOR_CONVERSATION_TTL_MS = 60 * 60 * 1000;
183
+ const conversationLastAccess = new Map<string, number>();
184
+
185
+ /** Drop all cached state + blob bytes for a conversation (F15 bound + session-teardown hook). */
186
+ export function disposeCursorConversation(conversationId: string): void {
187
+ conversationStateCache.delete(conversationId);
188
+ conversationBlobStores.delete(conversationId);
189
+ conversationLastAccess.delete(conversationId);
190
+ }
191
+
192
+ /** Refresh recency for a conversation and evict TTL-stale / LRU-overflow entries (F15). */
193
+ function touchCursorConversation(conversationId: string): void {
194
+ const now = Date.now();
195
+ for (const [id, ts] of conversationLastAccess) {
196
+ if (id !== conversationId && now - ts > CURSOR_CONVERSATION_TTL_MS) disposeCursorConversation(id);
197
+ }
198
+ conversationLastAccess.set(conversationId, now);
199
+ const state = conversationStateCache.get(conversationId);
200
+ if (state !== undefined) {
201
+ conversationStateCache.delete(conversationId);
202
+ conversationStateCache.set(conversationId, state);
203
+ }
204
+ while (conversationStateCache.size > CURSOR_MAX_CONVERSATIONS) {
205
+ const oldest = conversationStateCache.keys().next().value;
206
+ if (oldest === undefined || oldest === conversationId) break;
207
+ disposeCursorConversation(oldest);
208
+ }
209
+ }
210
+
211
+ export interface CursorOptions extends StreamOptions {
212
+ customSystemPrompt?: string;
213
+ conversationId?: string;
214
+ execHandlers?: CursorExecHandlers;
215
+ onToolResult?: CursorToolResultHandler;
216
+ }
217
+
218
+ const CONNECT_END_STREAM_FLAG = 0b00000010;
219
+
220
+ interface CursorLogEntry {
221
+ ts: number;
222
+ type: string;
223
+ subtype?: string;
224
+ data?: unknown;
225
+ }
226
+
227
+ async function appendCursorDebugLog(entry: CursorLogEntry): Promise<void> {
228
+ const logPath = $env.DEBUG_CURSOR_LOG;
229
+ if (!logPath) return;
230
+ try {
231
+ await fs.appendFile(logPath, `${JSON.stringify(entry, debugReplacer)}\n`);
232
+ } catch {
233
+ // Ignore debug log failures
234
+ }
235
+ }
236
+
237
+ function log(type: string, subtype?: string, data?: unknown): void {
238
+ if (!$env.DEBUG_CURSOR) return;
239
+ const normalizedData = data ? decodeLogData(data) : data;
240
+ const entry: CursorLogEntry = { ts: Date.now(), type, subtype, data: normalizedData };
241
+ const verbose = $env.DEBUG_CURSOR === "2" || $env.DEBUG_CURSOR === "verbose";
242
+ const dataStr = verbose && normalizedData ? ` ${JSON.stringify(normalizedData, debugReplacer)?.slice(0, 500)}` : "";
243
+ console.error(`[CURSOR] ${type}${subtype ? `: ${subtype}` : ""}${dataStr}`);
244
+ void appendCursorDebugLog(entry);
245
+ }
246
+
247
+ function frameConnectMessage(data: Uint8Array, flags = 0): Buffer {
248
+ const frame = Buffer.alloc(5 + data.length);
249
+ frame[0] = flags;
250
+ frame.writeUInt32BE(data.length, 1);
251
+ frame.set(data, 5);
252
+ return frame;
253
+ }
254
+
255
+ function parseConnectEndStream(data: Uint8Array): Error | null {
256
+ try {
257
+ const payload = JSON.parse(new TextDecoder().decode(data));
258
+ const error = payload?.error;
259
+ if (error) {
260
+ const code = typeof error.code === "string" ? error.code : "unknown";
261
+ const message = typeof error.message === "string" ? error.message : "Unknown error";
262
+ return new Error(`Connect error ${code}: ${message}`);
263
+ }
264
+ return null;
265
+ } catch {
266
+ return new Error("Failed to parse Connect end stream");
267
+ }
268
+ }
269
+
270
+ interface CursorRequestWriter {
271
+ enqueue(frame: Uint8Array): void;
272
+ isActive(): boolean;
273
+ registerShellGate(close: () => void): () => void;
274
+ }
275
+
276
+ class CursorRequestCoordinator implements CursorRequestWriter {
277
+ #state: "open" | "draining" | "failed" | "succeeded" = "open";
278
+ #tasks = new Set<Promise<void>>();
279
+ #taskChain = Promise.resolve();
280
+ #hasAdmittedTask = false;
281
+ #frames: Uint8Array[] = [];
282
+ #writing = false;
283
+ #drainWaiters: Array<() => void> = [];
284
+ #failure: Error | null = null;
285
+ #shellGates = new Set<() => void>();
286
+ #request: http2.ClientHttp2Stream;
287
+ #stopHeartbeat: () => void;
288
+ #onSuccess: () => void;
289
+ #onFailure: (error: Error) => void;
290
+ #drainTimer: NodeJS.Timeout | null = null;
291
+ #drainTimeoutMs: number | undefined;
292
+
293
+ constructor(
294
+ request: http2.ClientHttp2Stream,
295
+ stopHeartbeat: () => void,
296
+ onSuccess: () => void,
297
+ onFailure: (error: Error) => void,
298
+ drainTimeoutMs: number | undefined,
299
+ ) {
300
+ this.#request = request;
301
+ this.#stopHeartbeat = stopHeartbeat;
302
+ this.#onSuccess = onSuccess;
303
+ this.#onFailure = onFailure;
304
+ this.#drainTimeoutMs = drainTimeoutMs;
305
+ }
306
+
307
+ isActive(): boolean {
308
+ return this.#state === "open" || this.#state === "draining";
309
+ }
310
+
311
+ canAdmitTask(): boolean {
312
+ return this.#state === "open";
313
+ }
314
+
315
+ hasTurnEnded(): boolean {
316
+ return this.#state === "draining" || this.#state === "succeeded";
317
+ }
318
+
319
+ failureError(): Error | null {
320
+ return this.#failure;
321
+ }
322
+
323
+ enqueue(frame: Uint8Array): void {
324
+ if (!this.isActive()) return;
325
+ this.#frames.push(Buffer.from(frame));
326
+ this.#writeNext();
327
+ }
328
+
329
+ registerShellGate(close: () => void): () => void {
330
+ if (!this.isActive()) {
331
+ close();
332
+ return () => {};
333
+ }
334
+ this.#shellGates.add(close);
335
+ return () => this.#shellGates.delete(close);
336
+ }
337
+
338
+ admit(taskFactory: () => Promise<void>): void {
339
+ if (!this.canAdmitTask()) return;
340
+ const orderedTask = this.#hasAdmittedTask
341
+ ? this.#taskChain.then(() => {
342
+ if (this.#state === "failed" || this.#state === "succeeded") return;
343
+ return taskFactory();
344
+ })
345
+ : taskFactory();
346
+ this.#hasAdmittedTask = true;
347
+ this.#taskChain = orderedTask.then(
348
+ () => {},
349
+ error => {
350
+ this.fail(error instanceof Error ? error : new Error(String(error)));
351
+ },
352
+ );
353
+ this.#tasks.add(orderedTask);
354
+ void orderedTask.then(
355
+ () => this.#tasks.delete(orderedTask),
356
+ () => this.#tasks.delete(orderedTask),
357
+ );
358
+ }
359
+
360
+ turnEnded(): void {
361
+ if (this.#state !== "open") return;
362
+ this.#state = "draining";
363
+ this.#stopHeartbeat();
364
+ if (this.#drainTimeoutMs !== undefined && this.#drainTimeoutMs > 0) {
365
+ this.#drainTimer = setTimeout(() => {
366
+ this.fail(new Error(`Cursor admitted work drain timed out after ${this.#drainTimeoutMs}ms`));
367
+ }, this.#drainTimeoutMs);
368
+ }
369
+ void Promise.all([...this.#tasks]).then(
370
+ () => {
371
+ if (this.#state !== "draining") return;
372
+ this.#drain(() => {
373
+ if (this.#state !== "draining") return;
374
+ if (this.#drainTimer) {
375
+ clearTimeout(this.#drainTimer);
376
+ this.#drainTimer = null;
377
+ }
378
+ this.#state = "succeeded";
379
+ this.#onSuccess();
380
+ });
381
+ },
382
+ error => this.fail(error instanceof Error ? error : new Error(String(error))),
383
+ );
384
+ }
385
+
386
+ fail(error: Error): void {
387
+ if (this.#state === "failed" || this.#state === "succeeded") return;
388
+ this.#state = "failed";
389
+ this.#failure = error;
390
+ if (this.#drainTimer) {
391
+ clearTimeout(this.#drainTimer);
392
+ this.#drainTimer = null;
393
+ }
394
+ this.#stopHeartbeat();
395
+ for (const close of this.#shellGates) close();
396
+ this.#shellGates.clear();
397
+ this.#frames = [];
398
+ this.#writing = false;
399
+ this.#releaseDrains();
400
+ this.#request.close();
401
+ this.#onFailure(error);
402
+ }
403
+
404
+ #writeNext(): void {
405
+ if (this.#writing || !this.isActive()) return;
406
+ const frame = this.#frames.shift();
407
+ if (!frame) {
408
+ this.#releaseDrains();
409
+ return;
410
+ }
411
+ this.#writing = true;
412
+ try {
413
+ this.#request.write(frame, error => {
414
+ this.#writing = false;
415
+ if (error) {
416
+ this.fail(error);
417
+ return;
418
+ }
419
+ this.#writeNext();
420
+ });
421
+ } catch (error) {
422
+ this.#writing = false;
423
+ this.fail(error instanceof Error ? error : new Error(String(error)));
424
+ }
425
+ }
426
+
427
+ #drain(resolve: () => void): void {
428
+ if (!this.#writing && this.#frames.length === 0) {
429
+ resolve();
430
+ return;
431
+ }
432
+ this.#drainWaiters.push(resolve);
433
+ }
434
+
435
+ #releaseDrains(): void {
436
+ if (this.#writing || this.#frames.length > 0) return;
437
+ const waiters = this.#drainWaiters;
438
+ this.#drainWaiters = [];
439
+ for (const resolve of waiters) resolve();
440
+ }
441
+ }
442
+
443
+ function debugBytes(bytes: Uint8Array, asHex: boolean): string {
444
+ if (asHex) {
445
+ return Buffer.from(bytes).toString("hex");
446
+ }
447
+ try {
448
+ const text = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
449
+ if (/^[\x20-\x7E\s]*$/.test(text)) return text;
450
+ } catch {}
451
+ return Buffer.from(bytes).toString("hex");
452
+ }
453
+
454
+ function debugReplacer(key: string, value: unknown): unknown {
455
+ if (
456
+ value instanceof Uint8Array ||
457
+ (value && typeof value === "object" && "type" in value && value.type === "Buffer")
458
+ ) {
459
+ const bytes = value instanceof Uint8Array ? value : new Uint8Array((value as any).data);
460
+ const asHex = key === "blobId" || key === "blob_id" || key.endsWith("Id") || key.endsWith("_id");
461
+ return debugBytes(bytes, asHex);
462
+ }
463
+ if (typeof value === "bigint") return value.toString();
464
+ return value;
465
+ }
466
+
467
+ function extractLogBytes(value: unknown): Uint8Array | null {
468
+ if (value instanceof Uint8Array) {
469
+ return value;
470
+ }
471
+ if (value && typeof value === "object" && "type" in value && value.type === "Buffer") {
472
+ const data = (value as { data?: number[] }).data;
473
+ if (Array.isArray(data)) {
474
+ return new Uint8Array(data);
475
+ }
476
+ }
477
+ return null;
478
+ }
479
+
480
+ function decodeMcpArgsForLog(args?: Record<string, unknown>): Record<string, unknown> | undefined {
481
+ if (!args) {
482
+ return undefined;
483
+ }
484
+ let mutated = false;
485
+ const decoded: Record<string, unknown> = {};
486
+ for (const [key, value] of Object.entries(args)) {
487
+ const bytes = extractLogBytes(value);
488
+ if (bytes) {
489
+ decoded[key] = decodeMcpArgValue(bytes);
490
+ mutated = true;
491
+ continue;
492
+ }
493
+ const normalizedValue = decodeLogData(value);
494
+ decoded[key] = normalizedValue;
495
+ if (normalizedValue !== value) {
496
+ mutated = true;
497
+ }
498
+ }
499
+ return mutated ? decoded : args;
500
+ }
501
+
502
+ function decodeLogData(value: unknown): unknown {
503
+ if (!value || typeof value !== "object") {
504
+ return value;
505
+ }
506
+ if (Array.isArray(value)) {
507
+ return value.map(entry => decodeLogData(entry));
508
+ }
509
+ const record = value as Record<string, unknown>;
510
+ const typeName = record.$typeName;
511
+ const stripTypeName = typeof typeName === "string" && typeName.startsWith("agent.v1.");
512
+
513
+ if (typeName === "agent.v1.McpArgs") {
514
+ const decodedArgs = decodeMcpArgsForLog(record.args as Record<string, unknown> | undefined);
515
+ const base = stripTypeName ? omitTypeName(record) : record;
516
+ return decodedArgs ? { ...base, args: decodedArgs } : base;
517
+ }
518
+ if (typeName === "agent.v1.McpToolCall") {
519
+ const argsRecord = record.args as Record<string, unknown> | undefined;
520
+ const decodedArgs = decodeMcpArgsForLog(argsRecord?.args as Record<string, unknown> | undefined);
521
+ const base = stripTypeName ? omitTypeName(record) : record;
522
+ if (decodedArgs && argsRecord) {
523
+ return { ...base, args: { ...argsRecord, args: decodedArgs } };
524
+ }
525
+ return base;
526
+ }
527
+
528
+ let mutated = stripTypeName;
529
+ const decoded: Record<string, unknown> = {};
530
+ for (const [key, entry] of Object.entries(record)) {
531
+ if (stripTypeName && key === "$typeName") {
532
+ continue;
533
+ }
534
+ const normalizedEntry = decodeLogData(entry);
535
+ decoded[key] = normalizedEntry;
536
+ if (normalizedEntry !== entry) {
537
+ mutated = true;
538
+ }
539
+ }
540
+ return mutated ? decoded : record;
541
+ }
542
+
543
+ function omitTypeName(record: Record<string, unknown>): Record<string, unknown> {
544
+ const { $typeName: _, ...rest } = record;
545
+ return rest;
546
+ }
547
+
548
+ const CURSOR_MODEL_EFFORT_RE = /^(.*)-(minimal|low|medium|high|xhigh|max)(-fast)?$/;
549
+ const CURSOR_GPT_MODEL_RE =
550
+ /^gpt-\d+(?:\.\d+){0,2}(?:-(?:codex-spark|codex-mini|codex-max|codex|luna|mini|max|nano|sol|terra))?$/;
551
+ // `codex-max` is itself an intrinsic Cursor model, not an effort suffix. Keep
552
+ // its canonical and canonical-fast forms intact before parsing effort aliases.
553
+ const CURSOR_INTRINSIC_CODEX_MAX_RE = /^gpt-\d+(?:\.\d+){0,2}-codex-max(?:-fast)?$/;
554
+
555
+ /** Build the ordered global USER rules Cursor expects for the current system prompt. */
556
+ export function buildCursorRequestContextRules(systemPrompt: readonly string[] | undefined): CursorRule[] {
557
+ return normalizeSystemPrompts(systemPrompt).map((content, index) =>
558
+ create(CursorRuleSchema, {
559
+ fullPath: `/vib/system-prompt/${index}.mdc`,
560
+ content,
561
+ type: create(CursorRuleTypeSchema, {
562
+ type: {
563
+ case: "global",
564
+ value: create(CursorRuleTypeGlobalSchema, {}),
565
+ },
566
+ }),
567
+ source: CursorRuleSource.USER,
568
+ }),
569
+ );
570
+ }
571
+
572
+ export interface CursorWireModelResolution {
573
+ modelId: string;
574
+ parameters: RequestedModel_ModelParameterbytes[];
575
+ translated: boolean;
576
+ }
577
+
578
+ /** Resolve a Cursor model's GPT effort suffix into its wire model and parameter. */
579
+ export function resolveCursorWireModelForTest(
580
+ model: Pick<Model<"cursor-agent">, "id" | "wireModelId">,
581
+ ): CursorWireModelResolution {
582
+ const wireModelId = model.wireModelId ?? model.id;
583
+ if (CURSOR_INTRINSIC_CODEX_MAX_RE.test(wireModelId)) {
584
+ return { modelId: wireModelId, parameters: [], translated: false };
585
+ }
586
+ const match = CURSOR_MODEL_EFFORT_RE.exec(wireModelId);
587
+ if (!match || !CURSOR_GPT_MODEL_RE.test(match[1])) {
588
+ return { modelId: wireModelId, parameters: [], translated: false };
589
+ }
590
+
591
+ const [, baseModelId, effort, fastSuffix] = match;
592
+ const modelId = `${baseModelId}${fastSuffix ?? ""}`;
593
+ return {
594
+ modelId,
595
+ parameters: [
596
+ create(RequestedModel_ModelParameterbytesSchema, {
597
+ id: "reasoning",
598
+ value: effort,
599
+ }),
600
+ ],
601
+ translated: true,
602
+ };
603
+ }
604
+
605
+ /** Turn Cursor's opaque HTTP/2 failure into a useful transport diagnosis. */
606
+ export function mapH2TransportError(error: unknown, baseUrl: string): unknown {
607
+ if (!error || typeof error !== "object") return error;
608
+ const candidate = error as { code?: unknown; message?: unknown };
609
+ if (candidate.code !== "ERR_HTTP2_ERROR" || typeof candidate.message !== "string") return error;
610
+ if (!/h2 is not supported/i.test(candidate.message)) return error;
611
+ return new Error(
612
+ `Cursor HTTP/2 is not supported by ${baseUrl}. Use an HTTP/2-capable endpoint, configure a proxy tunnel that preserves ALPN h2, or set providers.cursor.baseUrl to an HTTP/2 endpoint.`,
613
+ { cause: error },
614
+ );
615
+ }
616
+
617
+ export const streamCursor: StreamFunction<"cursor-agent"> = (
618
+ model: Model<"cursor-agent">,
619
+ context: Context,
620
+ options?: CursorOptions,
621
+ ): AssistantMessageEventStream => {
622
+ const stream = new AssistantMessageEventStream();
623
+ const requestContextRules = buildCursorRequestContextRules(context.systemPrompt);
624
+
625
+ (async () => {
626
+ const startTime = Date.now();
627
+ let firstTokenTime: number | undefined;
628
+
629
+ const output: AssistantMessage = {
630
+ role: "assistant",
631
+ content: [],
632
+ api: "cursor-agent" as Api,
633
+ provider: model.provider,
634
+ model: model.id,
635
+ usage: {
636
+ input: 0,
637
+ output: 0,
638
+ cacheRead: 0,
639
+ cacheWrite: 0,
640
+ totalTokens: 0,
641
+ cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
642
+ },
643
+ stopReason: "stop",
644
+ timestamp: Date.now(),
645
+ };
646
+
647
+ let h2Client: http2.ClientHttp2Session | null = null;
648
+ let h2Request: http2.ClientHttp2Stream | null = null;
649
+ let proxiedSocket: Awaited<ReturnType<typeof connectProxiedSocket>> | null = null;
650
+ let heartbeatTimer: NodeJS.Timeout | null = null;
651
+ let onAbort: (() => void) | undefined;
652
+ let coordinator: CursorRequestCoordinator = undefined!;
653
+ const baseUrl = model.baseUrl || CURSOR_API_URL;
654
+
655
+ try {
656
+ const apiKey = options?.apiKey;
657
+ if (!apiKey) {
658
+ throw new Error("Cursor API key (access token) is required");
659
+ }
660
+ if (options?.signal?.aborted) {
661
+ throw new Error("Request was aborted");
662
+ }
663
+
664
+ const conversationId = options?.conversationId ?? options?.sessionId ?? crypto.randomUUID();
665
+ const blobStore = conversationBlobStores.get(conversationId) ?? new Map<string, Uint8Array>();
666
+ conversationBlobStores.set(conversationId, blobStore);
667
+ const cachedState = conversationStateCache.get(conversationId);
668
+ const { requestBytes, conversationState } = buildGrpcRequest(model, context, options, {
669
+ conversationId,
670
+ blobStore,
671
+ conversationState: cachedState,
672
+ });
673
+ conversationStateCache.set(conversationId, conversationState);
674
+ touchCursorConversation(conversationId);
675
+ const requestContextTools = buildMcpToolDefinitions(context.tools);
676
+ const targetUrl = new URL(baseUrl);
677
+ const proxyUrl = getProxyForUrl(model.provider, targetUrl);
678
+ if (options?.signal?.aborted) {
679
+ throw new Error("Request was aborted");
680
+ }
681
+ if (proxyUrl) {
682
+ proxiedSocket = await connectProxiedSocket(proxyUrl, baseUrl, {
683
+ signal: options?.signal,
684
+ timeoutMs: 30_000,
685
+ });
686
+ h2Client = http2.connect(baseUrl, { createConnection: () => proxiedSocket! });
687
+ } else {
688
+ h2Client = http2.connect(baseUrl);
689
+ }
690
+
691
+ options?.onStreamCreated?.();
692
+ h2Request = h2Client.request({
693
+ ":method": "POST",
694
+ ":path": "/agent.v1.AgentService/Run",
695
+ "content-type": "application/connect+proto",
696
+ "connect-protocol-version": "1",
697
+ te: "trailers",
698
+ authorization: `Bearer ${apiKey}`,
699
+ "x-ghost-mode": "true",
700
+ "x-cursor-client-version": CURSOR_CLIENT_VERSION,
701
+ "x-cursor-client-type": "cli",
702
+ "x-request-id": crypto.randomUUID(),
703
+ });
704
+ const stopHeartbeat = () => {
705
+ if (heartbeatTimer) {
706
+ clearInterval(heartbeatTimer);
707
+ heartbeatTimer = null;
708
+ }
709
+ };
710
+ let resolveH2: (() => void) | undefined;
711
+ let rejectH2: ((error: Error) => void) | undefined;
712
+ coordinator = new CursorRequestCoordinator(
713
+ h2Request,
714
+ stopHeartbeat,
715
+ () => {
716
+ const resolve = resolveH2;
717
+ resolveH2 = undefined;
718
+ resolve?.();
719
+ },
720
+ error => {
721
+ const reject = rejectH2;
722
+ rejectH2 = undefined;
723
+ reject?.(error);
724
+ },
725
+ options?.streamIdleTimeoutMs ?? getStreamIdleTimeoutMs(),
726
+ );
727
+ h2Client.on("error", error => coordinator.fail(error));
728
+ h2Request.on("error", error => coordinator.fail(error));
729
+
730
+ stream.push({ type: "start", partial: output });
731
+
732
+ let pendingBuffer = Buffer.alloc(0);
733
+ let currentTextBlock: (TextContent & { index: number }) | null = null;
734
+ let currentThinkingBlock: (ThinkingContent & { index: number }) | null = null;
735
+ let currentToolCall: ToolCallState | null = null;
736
+ const usageState: UsageState = { sawTokenDelta: false };
737
+
738
+ const state: BlockState = {
739
+ get currentTextBlock() {
740
+ return currentTextBlock;
741
+ },
742
+ get currentThinkingBlock() {
743
+ return currentThinkingBlock;
744
+ },
745
+ get currentToolCall() {
746
+ return currentToolCall;
747
+ },
748
+ get firstTokenTime() {
749
+ return firstTokenTime;
750
+ },
751
+ setTextBlock: b => {
752
+ currentTextBlock = b;
753
+ },
754
+ setThinkingBlock: b => {
755
+ currentThinkingBlock = b;
756
+ },
757
+ setToolCall: t => {
758
+ currentToolCall = t;
759
+ },
760
+ setFirstTokenTime: () => {
761
+ if (!firstTokenTime) firstTokenTime = Date.now();
762
+ },
763
+ };
764
+
765
+ const onConversationCheckpoint = (checkpoint: ConversationStateStructure) => {
766
+ conversationStateCache.set(conversationId, checkpoint);
767
+ touchCursorConversation(conversationId);
768
+ };
769
+
770
+ h2Request.on("trailers", trailers => {
771
+ const status = trailers["grpc-status"];
772
+ const msg = trailers["grpc-message"];
773
+ if (status && status !== "0") {
774
+ coordinator.fail(new Error(`gRPC error ${status}: ${decodeURIComponent(String(msg || ""))}`));
775
+ }
776
+ });
777
+ h2Request.on("end", () => {
778
+ if (!coordinator.hasTurnEnded()) {
779
+ coordinator.fail(new Error("Cursor stream ended before turnEnded"));
780
+ }
781
+ });
782
+ onAbort = () => {
783
+ coordinator.fail(new Error("Request was aborted"));
784
+ };
785
+ if (options?.signal) {
786
+ options.signal.addEventListener("abort", onAbort, { once: true });
787
+ if (options.signal.aborted) onAbort();
788
+ }
789
+
790
+ h2Request.on("data", (chunk: Buffer) => {
791
+ pendingBuffer = Buffer.concat([pendingBuffer, chunk]);
792
+
793
+ while (pendingBuffer.length >= 5) {
794
+ const flags = pendingBuffer[0];
795
+ const msgLen = pendingBuffer.readUInt32BE(1);
796
+ if (pendingBuffer.length < 5 + msgLen) break;
797
+
798
+ const messageBytes = pendingBuffer.subarray(5, 5 + msgLen);
799
+ pendingBuffer = pendingBuffer.subarray(5 + msgLen);
800
+
801
+ if (flags & CONNECT_END_STREAM_FLAG) {
802
+ const endError = parseConnectEndStream(messageBytes);
803
+ if (endError) {
804
+ coordinator.fail(endError);
805
+ } else {
806
+ coordinator.turnEnded();
807
+ }
808
+ continue;
809
+ }
810
+
811
+ try {
812
+ const serverMessage = fromBinary(AgentServerMessageSchema, messageBytes);
813
+ const isTurnEnded =
814
+ serverMessage.message.case === "interactionUpdate" &&
815
+ serverMessage.message.value.message?.case === "turnEnded";
816
+ // Serialize handlers: exec messages can be asynchronous, and resolving the
817
+ // request on turnEnded before prior handlers finish loses their responses.
818
+ if (!coordinator.canAdmitTask()) continue;
819
+ coordinator.admit(() =>
820
+ handleServerMessage(
821
+ serverMessage,
822
+ output,
823
+ stream,
824
+ state,
825
+ blobStore,
826
+ coordinator,
827
+ options?.execHandlers,
828
+ options?.onToolResult,
829
+ usageState,
830
+ requestContextTools,
831
+ onConversationCheckpoint,
832
+ requestContextRules,
833
+ ),
834
+ );
835
+
836
+ if (isTurnEnded) {
837
+ coordinator.turnEnded();
838
+ }
839
+ } catch (e) {
840
+ log("error", "parseServerMessage", { error: String(e) });
841
+ }
842
+ }
843
+ });
844
+
845
+ coordinator.enqueue(frameConnectMessage(requestBytes));
846
+
847
+ const sendHeartbeat = () => {
848
+ if (!coordinator.isActive()) return;
849
+ const heartbeatMessage = create(AgentClientMessageSchema, {
850
+ message: { case: "clientHeartbeat", value: create(ClientHeartbeatSchema, {}) },
851
+ });
852
+ const heartbeatBytes = toBinary(AgentClientMessageSchema, heartbeatMessage);
853
+ coordinator.enqueue(frameConnectMessage(heartbeatBytes));
854
+ };
855
+
856
+ heartbeatTimer = setInterval(sendHeartbeat, 5000);
857
+ await new Promise<void>((resolve, reject) => {
858
+ resolveH2 = resolve;
859
+ rejectH2 = reject;
860
+ const initialFailure = coordinator.failureError();
861
+ if (initialFailure) {
862
+ rejectH2 = undefined;
863
+ reject(initialFailure);
864
+ } else if (coordinator.hasTurnEnded() && !coordinator.isActive()) {
865
+ resolveH2 = undefined;
866
+ resolve();
867
+ }
868
+ });
869
+
870
+ if (state.currentTextBlock) {
871
+ const idx = output.content.indexOf(state.currentTextBlock);
872
+ stream.push({
873
+ type: "text_end",
874
+ contentIndex: idx,
875
+ content: state.currentTextBlock.text,
876
+ partial: output,
877
+ });
878
+ }
879
+ if (state.currentThinkingBlock) {
880
+ const idx = output.content.indexOf(state.currentThinkingBlock);
881
+ stream.push({
882
+ type: "thinking_end",
883
+ contentIndex: idx,
884
+ content: state.currentThinkingBlock.thinking,
885
+ partial: output,
886
+ });
887
+ }
888
+ if (state.currentToolCall) {
889
+ const idx = output.content.indexOf(state.currentToolCall);
890
+ state.currentToolCall.arguments = parseStreamingJson(state.currentToolCall.partialJson);
891
+ captureUnicodeEscapeEvidence(state.currentToolCall, state.currentToolCall.partialJson ?? "");
892
+ delete (state.currentToolCall as any).partialJson;
893
+ delete (state.currentToolCall as any).index;
894
+ stream.push({
895
+ type: "toolcall_end",
896
+ contentIndex: idx,
897
+ toolCall: state.currentToolCall,
898
+ partial: output,
899
+ });
900
+ }
901
+
902
+ calculateCost(model, output.usage);
903
+
904
+ output.duration = Date.now() - startTime;
905
+ if (firstTokenTime) output.ttft = firstTokenTime - startTime;
906
+ stream.push({
907
+ type: "done",
908
+ reason: output.stopReason as "stop" | "length" | "toolUse",
909
+ message: output,
910
+ });
911
+ stream.end();
912
+ } catch (error) {
913
+ // Keep the completion promise terminal even for synchronous setup/write
914
+ // failures that may not emit a separate HTTP/2 error event.
915
+ const mappedError = mapH2TransportError(coordinator?.failureError() ?? error, baseUrl);
916
+ output.stopReason = options?.signal?.aborted ? "aborted" : "error";
917
+ output.errorStatus = extractHttpStatusFromError(mappedError);
918
+ output.errorMessage = formatErrorMessageWithRetryAfter(mappedError);
919
+ output.duration = Date.now() - startTime;
920
+ if (firstTokenTime) output.ttft = firstTokenTime - startTime;
921
+ stream.push({ type: "error", reason: output.stopReason, error: output });
922
+ stream.end();
923
+ } finally {
924
+ if (heartbeatTimer) {
925
+ clearInterval(heartbeatTimer);
926
+ heartbeatTimer = null;
927
+ }
928
+ if (options?.signal && onAbort) {
929
+ options.signal.removeEventListener("abort", onAbort);
930
+ }
931
+ if (h2Request && !h2Request.closed && !h2Request.destroyed) {
932
+ h2Request.end();
933
+ }
934
+ h2Client?.close();
935
+ proxiedSocket?.destroy();
936
+ }
937
+ })();
938
+
939
+ return stream;
940
+ };
941
+
942
+ type ToolCallState = ToolCall & {
943
+ index: number;
944
+ partialJson?: string;
945
+ kind: "mcp" | "todo_write" | "native" | "cursor-exec";
946
+ [kCursorExecResolved]?: true;
947
+ };
948
+
949
+ interface BlockState {
950
+ currentTextBlock: (TextContent & { index: number }) | null;
951
+ currentThinkingBlock: (ThinkingContent & { index: number }) | null;
952
+ currentToolCall: ToolCallState | null;
953
+ firstTokenTime: number | undefined;
954
+ setTextBlock: (b: (TextContent & { index: number }) | null) => void;
955
+ setThinkingBlock: (b: (ThinkingContent & { index: number }) | null) => void;
956
+ setToolCall: (t: ToolCallState | null) => void;
957
+ setFirstTokenTime: () => void;
958
+ }
959
+
960
+ interface UsageState {
961
+ sawTokenDelta: boolean;
962
+ }
963
+
964
+ async function handleServerMessage(
965
+ msg: AgentServerMessage,
966
+ output: AssistantMessage,
967
+ stream: AssistantMessageEventStream,
968
+ state: BlockState,
969
+ blobStore: Map<string, Uint8Array>,
970
+ writer: CursorRequestWriter,
971
+ execHandlers: CursorExecHandlers | undefined,
972
+ onToolResult: CursorToolResultHandler | undefined,
973
+ usageState: UsageState,
974
+ requestContextTools: McpToolDefinition[],
975
+ onConversationCheckpoint?: (checkpoint: ConversationStateStructure) => void,
976
+ requestContextRules: CursorRule[] = [],
977
+ ): Promise<void> {
978
+ const msgCase = msg.message.case;
979
+
980
+ log("serverMessage", msgCase, msg.message.value);
981
+
982
+ if (msgCase === "interactionUpdate") {
983
+ processInteractionUpdate(msg.message.value, output, stream, state, usageState);
984
+ } else if (msgCase === "kvServerMessage") {
985
+ handleKvServerMessage(msg.message.value as KvServerMessage, blobStore, writer);
986
+ } else if (msgCase === "execServerMessage") {
987
+ await handleExecServerMessage(
988
+ msg.message.value as ExecServerMessage,
989
+ writer,
990
+ execHandlers,
991
+ onToolResult,
992
+ requestContextTools,
993
+ output,
994
+ stream,
995
+ requestContextRules,
996
+ );
997
+ } else if (msgCase === "conversationCheckpointUpdate") {
998
+ handleConversationCheckpointUpdate(msg.message.value, output, usageState, onConversationCheckpoint);
999
+ }
1000
+ }
1001
+
1002
+ function handleKvServerMessage(
1003
+ kvMsg: KvServerMessage,
1004
+ blobStore: Map<string, Uint8Array>,
1005
+ writer: CursorRequestWriter,
1006
+ ): void {
1007
+ const kvCase = kvMsg.message.case;
1008
+
1009
+ if (kvCase === "getBlobArgs") {
1010
+ const blobId = kvMsg.message.value.blobId;
1011
+ const blobIdKey = Buffer.from(blobId).toString("hex");
1012
+
1013
+ const blobData = blobStore.get(blobIdKey);
1014
+
1015
+ const response = create(KvClientMessageSchema, {
1016
+ id: kvMsg.id,
1017
+ message: {
1018
+ case: "getBlobResult",
1019
+ value: create(GetBlobResultSchema, blobData ? { blobData } : {}),
1020
+ },
1021
+ });
1022
+
1023
+ const kvClientMessage = create(AgentClientMessageSchema, {
1024
+ message: { case: "kvClientMessage", value: response },
1025
+ });
1026
+
1027
+ const responseBytes = toBinary(AgentClientMessageSchema, kvClientMessage);
1028
+ writer.enqueue(frameConnectMessage(responseBytes));
1029
+
1030
+ log("kvClient", "getBlobResult", { blobId: blobIdKey.slice(0, 40) });
1031
+ } else if (kvCase === "setBlobArgs") {
1032
+ const { blobId, blobData } = kvMsg.message.value;
1033
+ const blobIdKey = Buffer.from(blobId).toString("hex");
1034
+ blobStore.set(blobIdKey, blobData);
1035
+
1036
+ const response = create(KvClientMessageSchema, {
1037
+ id: kvMsg.id,
1038
+ message: {
1039
+ case: "setBlobResult",
1040
+ value: create(SetBlobResultSchema, {}),
1041
+ },
1042
+ });
1043
+
1044
+ const kvClientMessage = create(AgentClientMessageSchema, {
1045
+ message: { case: "kvClientMessage", value: response },
1046
+ });
1047
+
1048
+ const responseBytes = toBinary(AgentClientMessageSchema, kvClientMessage);
1049
+ writer.enqueue(frameConnectMessage(responseBytes));
1050
+
1051
+ log("kvClient", "setBlobResult", { blobId: blobIdKey.slice(0, 40) });
1052
+ }
1053
+ }
1054
+
1055
+ function sendShellStreamEvent(
1056
+ h2Request: CursorRequestWriter,
1057
+ execMsg: ExecServerMessage,
1058
+ event: ShellStream["event"],
1059
+ ): void {
1060
+ sendExecClientMessage(h2Request, execMsg, "shellStream", create(ShellStreamSchema, { event }));
1061
+ }
1062
+
1063
+ function sanitizeShellExecResult(execResult: ShellResult): ShellResult {
1064
+ const result = execResult.result;
1065
+ if (!result) return execResult;
1066
+
1067
+ switch (result.case) {
1068
+ case "success":
1069
+ case "failure": {
1070
+ const value = result.value;
1071
+ return {
1072
+ ...execResult,
1073
+ result: {
1074
+ case: result.case,
1075
+ value: {
1076
+ ...value,
1077
+ stdout: value.stdout ? sanitizeText(value.stdout) : value.stdout,
1078
+ stderr: value.stderr ? sanitizeText(value.stderr) : value.stderr,
1079
+ },
1080
+ },
1081
+ } as ShellResult;
1082
+ }
1083
+ default:
1084
+ return execResult;
1085
+ }
1086
+ }
1087
+
1088
+ async function handleShellStreamArgs(
1089
+ args: ShellArgs,
1090
+ execMsg: ExecServerMessage,
1091
+ h2Request: CursorRequestWriter,
1092
+ execHandlers: CursorExecHandlers | undefined,
1093
+ onToolResult: CursorToolResultHandler | undefined,
1094
+ ): Promise<void> {
1095
+ const normalizedWorkingDirectory = args.workingDirectory || process.cwd();
1096
+ const normalizedArgs: ShellArgs = { ...args, workingDirectory: normalizedWorkingDirectory };
1097
+ const startTs = Date.now();
1098
+ log("shellStream", "start", {
1099
+ command: (args as any).command,
1100
+ workingDirectory: normalizedWorkingDirectory,
1101
+ execId: execMsg.execId,
1102
+ hasExecHandlers: !!execHandlers,
1103
+ hasShell: !!execHandlers?.shell,
1104
+ hasShellStream: !!execHandlers?.shellStream,
1105
+ });
1106
+
1107
+ sendShellStreamEvent(h2Request, execMsg, { case: "start", value: create(ShellStreamStartSchema, {}) });
1108
+
1109
+ // Buffer for incomplete ANSI sequences across chunks
1110
+ let stdoutBuffer = "";
1111
+ let stderrBuffer = "";
1112
+ let callbacksOpen = true;
1113
+ const unregisterShellGate = h2Request.registerShellGate(() => {
1114
+ callbacksOpen = false;
1115
+ if (stdoutFlushTimer) clearTimeout(stdoutFlushTimer);
1116
+ if (stderrFlushTimer) clearTimeout(stderrFlushTimer);
1117
+ });
1118
+
1119
+ const incompleteEscapeRegex = /\x1b(|\[|\[\d*|\[\?|\[\?\d*|\]\d*;?)$/;
1120
+
1121
+ const flushStdout = () => {
1122
+ if (stdoutBuffer) {
1123
+ let safeEnd = stdoutBuffer.length;
1124
+ const match = stdoutBuffer.match(incompleteEscapeRegex);
1125
+ if (match && match[0].length > 0) {
1126
+ safeEnd = stdoutBuffer.length - match[0].length;
1127
+ }
1128
+ const toSend = stdoutBuffer.slice(0, safeEnd);
1129
+ const remaining = stdoutBuffer.slice(safeEnd);
1130
+ if (toSend) {
1131
+ sendShellStreamEvent(h2Request, execMsg, {
1132
+ case: "stdout",
1133
+ value: create(ShellStreamStdoutSchema, { data: sanitizeText(toSend) }),
1134
+ });
1135
+ }
1136
+ stdoutBuffer = remaining;
1137
+ }
1138
+ };
1139
+
1140
+ const flushStderr = () => {
1141
+ if (stderrBuffer) {
1142
+ let safeEnd = stderrBuffer.length;
1143
+ const match = stderrBuffer.match(incompleteEscapeRegex);
1144
+ if (match && match[0].length > 0) {
1145
+ safeEnd = stderrBuffer.length - match[0].length;
1146
+ }
1147
+ const toSend = stderrBuffer.slice(0, safeEnd);
1148
+ const remaining = stderrBuffer.slice(safeEnd);
1149
+ if (toSend) {
1150
+ sendShellStreamEvent(h2Request, execMsg, {
1151
+ case: "stderr",
1152
+ value: create(ShellStreamStderrSchema, { data: sanitizeText(toSend) }),
1153
+ });
1154
+ }
1155
+ stderrBuffer = remaining;
1156
+ }
1157
+ };
1158
+
1159
+ let stdoutFlushTimer: NodeJS.Timeout | null = null;
1160
+ let stderrFlushTimer: NodeJS.Timeout | null = null;
1161
+
1162
+ const scheduleStdoutFlush = () => {
1163
+ if (!stdoutFlushTimer) {
1164
+ stdoutFlushTimer = setTimeout(() => {
1165
+ stdoutFlushTimer = null;
1166
+ flushStdout();
1167
+ }, 100);
1168
+ }
1169
+ };
1170
+
1171
+ const scheduleStderrFlush = () => {
1172
+ if (!stderrFlushTimer) {
1173
+ stderrFlushTimer = setTimeout(() => {
1174
+ stderrFlushTimer = null;
1175
+ flushStderr();
1176
+ }, 100);
1177
+ }
1178
+ };
1179
+
1180
+ const streamCallbacks: CursorShellStreamCallbacks = {
1181
+ onStdout(data: string) {
1182
+ if (!callbacksOpen || !h2Request.isActive()) return;
1183
+ stdoutBuffer += data;
1184
+ if (stdoutBuffer.includes("\n") || stdoutBuffer.length > 4096) {
1185
+ if (stdoutFlushTimer) {
1186
+ clearTimeout(stdoutFlushTimer);
1187
+ stdoutFlushTimer = null;
1188
+ }
1189
+ flushStdout();
1190
+ } else {
1191
+ scheduleStdoutFlush();
1192
+ }
1193
+ },
1194
+ onStderr(data: string) {
1195
+ if (!callbacksOpen || !h2Request.isActive()) return;
1196
+ stderrBuffer += data;
1197
+ if (stderrBuffer.includes("\n") || stderrBuffer.length > 4096) {
1198
+ if (stderrFlushTimer) {
1199
+ clearTimeout(stderrFlushTimer);
1200
+ stderrFlushTimer = null;
1201
+ }
1202
+ flushStderr();
1203
+ } else {
1204
+ scheduleStderrFlush();
1205
+ }
1206
+ },
1207
+ };
1208
+
1209
+ // Prefer the streaming handler — it forwards output chunks in real time.
1210
+ // Falls back to the batch shell handler otherwise.
1211
+ const streamHandler = execHandlers?.shellStream?.bind(execHandlers);
1212
+ const batchHandler = execHandlers?.shell?.bind(execHandlers);
1213
+ const handler = streamHandler ? (shellArgs: ShellArgs) => streamHandler(shellArgs, streamCallbacks) : batchHandler;
1214
+
1215
+ const { execResult } = await resolveExecHandler(
1216
+ args as any,
1217
+ handler as typeof batchHandler,
1218
+ onToolResult,
1219
+ toolResult => buildShellResultFromToolResult(normalizedArgs as any, toolResult),
1220
+ reason =>
1221
+ buildShellRejectedResult((normalizedArgs as any).command, (normalizedArgs as any).workingDirectory, reason),
1222
+ error =>
1223
+ buildShellFailureResult((normalizedArgs as any).command, (normalizedArgs as any).workingDirectory, error),
1224
+ );
1225
+
1226
+ // When using the batch handler (no shellStream), send buffered stdout/stderr
1227
+ // after execution completes. With shellStream these were already sent in real time.
1228
+ const sendBufferedOutput = !streamHandler;
1229
+ const sanitizedExecResult = sanitizeShellExecResult(execResult);
1230
+
1231
+ // Flush any remaining buffered output before sending results
1232
+ if (stdoutFlushTimer) clearTimeout(stdoutFlushTimer);
1233
+ if (stderrFlushTimer) clearTimeout(stderrFlushTimer);
1234
+ flushStdout();
1235
+ flushStderr();
1236
+
1237
+ sendShellStreamExitFromResult(h2Request, execMsg, sanitizedExecResult, sendBufferedOutput);
1238
+ // Cursor can keep the turn pending when it receives only stream deltas.
1239
+ // Send the final structured shellResult as completion acknowledgement.
1240
+ sendExecClientMessage(h2Request, execMsg, "shellResult", sanitizedExecResult);
1241
+ sendExecClientStreamClose(h2Request, execMsg);
1242
+ callbacksOpen = false;
1243
+ unregisterShellGate();
1244
+
1245
+ log("shellStream", "done", { elapsed: Date.now() - startTs });
1246
+ }
1247
+
1248
+ function sendShellStreamExitFromResult(
1249
+ h2Request: CursorRequestWriter,
1250
+ execMsg: ExecServerMessage,
1251
+ execResult: ShellResult,
1252
+ sendBufferedOutput: boolean,
1253
+ ): void {
1254
+ const result = execResult.result;
1255
+ switch (result.case) {
1256
+ case "success": {
1257
+ const value = result.value;
1258
+ if (sendBufferedOutput) {
1259
+ if (value.stdout) {
1260
+ sendShellStreamEvent(h2Request, execMsg, {
1261
+ case: "stdout",
1262
+ value: create(ShellStreamStdoutSchema, { data: sanitizeText(value.stdout) }),
1263
+ });
1264
+ }
1265
+ if (value.stderr) {
1266
+ sendShellStreamEvent(h2Request, execMsg, {
1267
+ case: "stderr",
1268
+ value: create(ShellStreamStderrSchema, { data: sanitizeText(value.stderr) }),
1269
+ });
1270
+ }
1271
+ }
1272
+ sendShellStreamEvent(h2Request, execMsg, {
1273
+ case: "exit",
1274
+ value: create(ShellStreamExitSchema, {
1275
+ code: value.exitCode,
1276
+ cwd: value.workingDirectory,
1277
+ aborted: false,
1278
+ }),
1279
+ });
1280
+ return;
1281
+ }
1282
+ case "failure": {
1283
+ const value = result.value;
1284
+ if (sendBufferedOutput) {
1285
+ if (value.stdout) {
1286
+ sendShellStreamEvent(h2Request, execMsg, {
1287
+ case: "stdout",
1288
+ value: create(ShellStreamStdoutSchema, { data: sanitizeText(value.stdout) }),
1289
+ });
1290
+ }
1291
+ if (value.stderr) {
1292
+ sendShellStreamEvent(h2Request, execMsg, {
1293
+ case: "stderr",
1294
+ value: create(ShellStreamStderrSchema, { data: sanitizeText(value.stderr) }),
1295
+ });
1296
+ }
1297
+ }
1298
+ sendShellStreamEvent(h2Request, execMsg, {
1299
+ case: "exit",
1300
+ value: create(ShellStreamExitSchema, {
1301
+ code: value.exitCode,
1302
+ cwd: value.workingDirectory,
1303
+ aborted: value.aborted,
1304
+ abortReason: value.abortReason,
1305
+ }),
1306
+ });
1307
+ return;
1308
+ }
1309
+ case "rejected": {
1310
+ sendShellStreamEvent(h2Request, execMsg, { case: "rejected", value: result.value });
1311
+ sendShellStreamEvent(h2Request, execMsg, {
1312
+ case: "exit",
1313
+ value: create(ShellStreamExitSchema, {
1314
+ code: 1,
1315
+ cwd: result.value.workingDirectory,
1316
+ aborted: false,
1317
+ }),
1318
+ });
1319
+ return;
1320
+ }
1321
+ case "timeout": {
1322
+ const value = result.value;
1323
+ sendShellStreamEvent(h2Request, execMsg, {
1324
+ case: "stderr",
1325
+ value: create(ShellStreamStderrSchema, {
1326
+ data: `Command timed out after ${value.timeoutMs}ms`,
1327
+ }),
1328
+ });
1329
+ sendShellStreamEvent(h2Request, execMsg, {
1330
+ case: "exit",
1331
+ value: create(ShellStreamExitSchema, {
1332
+ code: 1,
1333
+ cwd: value.workingDirectory,
1334
+ aborted: true,
1335
+ }),
1336
+ });
1337
+ return;
1338
+ }
1339
+ case "permissionDenied": {
1340
+ sendShellStreamEvent(h2Request, execMsg, { case: "permissionDenied", value: result.value });
1341
+ sendShellStreamEvent(h2Request, execMsg, {
1342
+ case: "exit",
1343
+ value: create(ShellStreamExitSchema, {
1344
+ code: 1,
1345
+ cwd: result.value.workingDirectory,
1346
+ aborted: false,
1347
+ }),
1348
+ });
1349
+ return;
1350
+ }
1351
+ default:
1352
+ return;
1353
+ }
1354
+ }
1355
+
1356
+ async function handleExecServerMessage(
1357
+ execMsg: ExecServerMessage,
1358
+ h2Request: CursorRequestWriter,
1359
+ execHandlers: CursorExecHandlers | undefined,
1360
+ onToolResult: CursorToolResultHandler | undefined,
1361
+ requestContextTools: McpToolDefinition[],
1362
+ output: AssistantMessage,
1363
+ stream: AssistantMessageEventStream,
1364
+ requestContextRules: CursorRule[] = [],
1365
+ ): Promise<void> {
1366
+ const execCase = execMsg.message.case;
1367
+ log("exec", "dispatch", { execCase, execId: execMsg.execId, hasHandlers: !!execHandlers });
1368
+ if (execCase === "requestContextArgs") {
1369
+ const requestContext = create(RequestContextSchema, {
1370
+ rules: requestContextRules,
1371
+ repositoryInfo: [],
1372
+ tools: requestContextTools,
1373
+ gitRepos: [],
1374
+ projectLayouts: [],
1375
+ mcpInstructions: [],
1376
+ fileContents: {},
1377
+ customSubagents: [],
1378
+ });
1379
+
1380
+ const requestContextResult = create(RequestContextResultSchema, {
1381
+ result: {
1382
+ case: "success",
1383
+ value: create(RequestContextSuccessSchema, { requestContext }),
1384
+ },
1385
+ });
1386
+
1387
+ sendExecClientMessage(h2Request, execMsg, "requestContextResult", requestContextResult);
1388
+ log("execClient", "requestContextResult");
1389
+ return;
1390
+ }
1391
+
1392
+ if (!execCase) {
1393
+ return;
1394
+ }
1395
+
1396
+ switch (execCase) {
1397
+ case "readArgs": {
1398
+ const args = execMsg.message.value;
1399
+ const { execResult } = await resolveExecHandler(
1400
+ args,
1401
+ execHandlers?.read?.bind(execHandlers),
1402
+ onToolResult,
1403
+ toolResult => buildReadResultFromToolResult(args.path, toolResult),
1404
+ reason => buildReadRejectedResult(args.path, reason),
1405
+ error => buildReadErrorResult(args.path, error),
1406
+ );
1407
+ sendExecClientMessage(h2Request, execMsg, "readResult", execResult);
1408
+ return;
1409
+ }
1410
+ case "lsArgs": {
1411
+ const args = execMsg.message.value;
1412
+ const { execResult } = await resolveExecHandler(
1413
+ args,
1414
+ execHandlers?.ls?.bind(execHandlers),
1415
+ onToolResult,
1416
+ toolResult => buildLsResultFromToolResult(args.path, toolResult),
1417
+ reason => buildLsRejectedResult(args.path, reason),
1418
+ error => buildLsErrorResult(args.path, error),
1419
+ );
1420
+ sendExecClientMessage(h2Request, execMsg, "lsResult", execResult);
1421
+ return;
1422
+ }
1423
+ case "grepArgs": {
1424
+ const args = execMsg.message.value;
1425
+ const { execResult } = await resolveExecHandler(
1426
+ args,
1427
+ execHandlers?.grep?.bind(execHandlers),
1428
+ onToolResult,
1429
+ toolResult => buildGrepResultFromToolResult(args, toolResult),
1430
+ reason => buildGrepErrorResult(reason),
1431
+ error => buildGrepErrorResult(error),
1432
+ );
1433
+ sendExecClientMessage(h2Request, execMsg, "grepResult", execResult);
1434
+ return;
1435
+ }
1436
+ case "writeArgs": {
1437
+ const args = execMsg.message.value;
1438
+ const { execResult } = await resolveExecHandler(
1439
+ args,
1440
+ execHandlers?.write?.bind(execHandlers),
1441
+ onToolResult,
1442
+ toolResult =>
1443
+ buildWriteResultFromToolResult(
1444
+ {
1445
+ path: args.path,
1446
+ fileText: args.fileText,
1447
+ fileBytes: args.fileBytes,
1448
+ returnFileContentAfterWrite: args.returnFileContentAfterWrite,
1449
+ },
1450
+ toolResult,
1451
+ ),
1452
+ reason => buildWriteRejectedResult(args.path, reason),
1453
+ error => buildWriteErrorResult(args.path, error),
1454
+ );
1455
+ sendExecClientMessage(h2Request, execMsg, "writeResult", execResult);
1456
+ return;
1457
+ }
1458
+ case "deleteArgs": {
1459
+ const args = execMsg.message.value;
1460
+ const { execResult } = await resolveExecHandler(
1461
+ args,
1462
+ execHandlers?.delete?.bind(execHandlers),
1463
+ onToolResult,
1464
+ toolResult => buildDeleteResultFromToolResult(args.path, toolResult),
1465
+ reason => buildDeleteRejectedResult(args.path, reason),
1466
+ error => buildDeleteErrorResult(args.path, error),
1467
+ );
1468
+ sendExecClientMessage(h2Request, execMsg, "deleteResult", execResult);
1469
+ return;
1470
+ }
1471
+ case "shellArgs": {
1472
+ const args = execMsg.message.value;
1473
+ const normalizedArgs: ShellArgs = { ...args, workingDirectory: args.workingDirectory || process.cwd() };
1474
+ const { execResult } = await resolveExecHandler(
1475
+ args,
1476
+ execHandlers?.shell?.bind(execHandlers),
1477
+ onToolResult,
1478
+ toolResult => buildShellResultFromToolResult(normalizedArgs, toolResult),
1479
+ reason => buildShellRejectedResult(normalizedArgs.command, normalizedArgs.workingDirectory, reason),
1480
+ error => buildShellFailureResult(normalizedArgs.command, normalizedArgs.workingDirectory, error),
1481
+ );
1482
+ const sanitizedExecResult = sanitizeShellExecResult(execResult);
1483
+ sendExecClientMessage(h2Request, execMsg, "shellResult", sanitizedExecResult);
1484
+ return;
1485
+ }
1486
+ case "shellStreamArgs": {
1487
+ const args = execMsg.message.value;
1488
+ await handleShellStreamArgs(args, execMsg, h2Request, execHandlers, onToolResult);
1489
+ return;
1490
+ }
1491
+ case "backgroundShellSpawnArgs": {
1492
+ const args = execMsg.message.value;
1493
+ const execResult = create(BackgroundShellSpawnResultSchema, {
1494
+ result: {
1495
+ case: "rejected",
1496
+ value: create(ShellRejectedSchema, {
1497
+ command: args.command,
1498
+ workingDirectory: args.workingDirectory,
1499
+ reason: "Not implemented",
1500
+ isReadonly: false,
1501
+ }),
1502
+ },
1503
+ });
1504
+ sendExecClientMessage(h2Request, execMsg, "backgroundShellSpawnResult", execResult);
1505
+ return;
1506
+ }
1507
+ case "writeShellStdinArgs": {
1508
+ const execResult = create(WriteShellStdinResultSchema, {
1509
+ result: {
1510
+ case: "error",
1511
+ value: create(WriteShellStdinErrorSchema, {
1512
+ error: "Not implemented",
1513
+ }),
1514
+ },
1515
+ });
1516
+ sendExecClientMessage(h2Request, execMsg, "writeShellStdinResult", execResult);
1517
+ return;
1518
+ }
1519
+ case "fetchArgs": {
1520
+ const args = execMsg.message.value;
1521
+ const execResult = create(FetchResultSchema, {
1522
+ result: {
1523
+ case: "error",
1524
+ value: create(FetchErrorSchema, {
1525
+ url: args.url,
1526
+ error: "Not implemented",
1527
+ }),
1528
+ },
1529
+ });
1530
+ sendExecClientMessage(h2Request, execMsg, "fetchResult", execResult);
1531
+ return;
1532
+ }
1533
+ case "diagnosticsArgs": {
1534
+ const args = execMsg.message.value;
1535
+ const { execResult } = await resolveExecHandler(
1536
+ args,
1537
+ execHandlers?.diagnostics?.bind(execHandlers),
1538
+ onToolResult,
1539
+ toolResult => buildDiagnosticsResultFromToolResult(args.path, toolResult),
1540
+ reason => buildDiagnosticsRejectedResult(args.path, reason),
1541
+ error => buildDiagnosticsErrorResult(args.path, error),
1542
+ );
1543
+ sendExecClientMessage(h2Request, execMsg, "diagnosticsResult", execResult);
1544
+ return;
1545
+ }
1546
+ case "mcpArgs": {
1547
+ const args = execMsg.message.value;
1548
+ const mcpCall = decodeMcpCall(args);
1549
+ const { execResult } = await resolveExecHandler(
1550
+ mcpCall,
1551
+ execHandlers?.mcp?.bind(execHandlers),
1552
+ onToolResult,
1553
+ toolResult => buildMcpResultFromToolResult(mcpCall, toolResult),
1554
+ _reason => buildMcpToolNotFoundResult(mcpCall),
1555
+ error => buildMcpErrorResult(error),
1556
+ );
1557
+ sendExecClientMessage(h2Request, execMsg, "mcpResult", execResult);
1558
+ return;
1559
+ }
1560
+ case "listMcpResourcesExecArgs": {
1561
+ const execResult = create(ListMcpResourcesExecResultSchema, {});
1562
+ sendExecClientMessage(h2Request, execMsg, "listMcpResourcesExecResult", execResult);
1563
+ return;
1564
+ }
1565
+ case "readMcpResourceExecArgs": {
1566
+ const execResult = create(ReadMcpResourceExecResultSchema, {});
1567
+ sendExecClientMessage(h2Request, execMsg, "readMcpResourceExecResult", execResult);
1568
+ return;
1569
+ }
1570
+ case "recordScreenArgs": {
1571
+ const execResult = create(RecordScreenResultSchema, {});
1572
+ sendExecClientMessage(h2Request, execMsg, "recordScreenResult", execResult);
1573
+ return;
1574
+ }
1575
+ case "computerUseArgs": {
1576
+ const execResult = create(ComputerUseResultSchema, {});
1577
+ sendExecClientMessage(h2Request, execMsg, "computerUseResult", execResult);
1578
+ return;
1579
+ }
1580
+ case "piReadArgs": {
1581
+ const args = execMsg.message.value;
1582
+ const toolCallId = crypto.randomUUID();
1583
+ synthesizeCursorExecToolCall(output, stream, toolCallId, "read", {
1584
+ path: piReadDisplayPath(args.path, args.offset, args.limit),
1585
+ });
1586
+ const call = { args, toolCallId };
1587
+ const { execResult } = await resolveExecHandler(
1588
+ call,
1589
+ execHandlers?.piRead?.bind(execHandlers),
1590
+ onToolResult,
1591
+ buildPiReadResult,
1592
+ buildPiReadError,
1593
+ buildPiReadError,
1594
+ );
1595
+ sendExecClientMessage(h2Request, execMsg, "piReadResult", execResult);
1596
+ return;
1597
+ }
1598
+ case "piBashArgs": {
1599
+ const args = execMsg.message.value;
1600
+ const toolCallId = crypto.randomUUID();
1601
+ synthesizeCursorExecToolCall(output, stream, toolCallId, "bash", {
1602
+ command: args.command,
1603
+ timeout: piTimeout(args.timeout),
1604
+ });
1605
+ const call = { args, toolCallId };
1606
+ const { execResult } = await resolveExecHandler(
1607
+ call,
1608
+ execHandlers?.piBash?.bind(execHandlers),
1609
+ onToolResult,
1610
+ buildPiBashResult,
1611
+ buildPiBashError,
1612
+ buildPiBashError,
1613
+ );
1614
+ sendExecClientMessage(h2Request, execMsg, "piBashResult", execResult);
1615
+ return;
1616
+ }
1617
+ case "piEditArgs": {
1618
+ const args = execMsg.message.value;
1619
+ const toolCallId = crypto.randomUUID();
1620
+ synthesizeCursorExecToolCall(output, stream, toolCallId, "edit", {
1621
+ path: args.path,
1622
+ edits: args.edits.map(edit => ({ old_text: edit.oldText, new_text: edit.newText })),
1623
+ });
1624
+ const call = { args, toolCallId };
1625
+ const { execResult } = await resolveExecHandler(
1626
+ call,
1627
+ execHandlers?.piEdit?.bind(execHandlers),
1628
+ onToolResult,
1629
+ buildPiEditResult,
1630
+ buildPiEditRejected,
1631
+ buildPiEditError,
1632
+ );
1633
+ sendExecClientMessage(h2Request, execMsg, "piEditResult", execResult);
1634
+ return;
1635
+ }
1636
+ case "piWriteArgs": {
1637
+ const args = execMsg.message.value;
1638
+ const toolCallId = crypto.randomUUID();
1639
+ synthesizeCursorExecToolCall(output, stream, toolCallId, "write", {
1640
+ path: args.path,
1641
+ content: args.content,
1642
+ });
1643
+ const call = { args, toolCallId };
1644
+ const { execResult } = await resolveExecHandler(
1645
+ call,
1646
+ execHandlers?.piWrite?.bind(execHandlers),
1647
+ onToolResult,
1648
+ buildPiWriteResult,
1649
+ buildPiWriteRejected,
1650
+ buildPiWriteError,
1651
+ );
1652
+ sendExecClientMessage(h2Request, execMsg, "piWriteResult", execResult);
1653
+ return;
1654
+ }
1655
+ case "piGrepArgs": {
1656
+ const args = execMsg.message.value;
1657
+ const toolCallId = crypto.randomUUID();
1658
+ synthesizeCursorExecToolCall(output, stream, toolCallId, "search", {
1659
+ pattern: args.literal === true ? piEscapeRegexLiteral(args.pattern) : args.pattern,
1660
+ paths: [args.glob ? piJoinPath(args.path, args.glob) : args.path || "."],
1661
+ // The model-facing search schema uses `i: true` for
1662
+ // case-insensitive matching. Keep the field absent otherwise.
1663
+ ...(args.ignoreCase === true ? { i: true } : {}),
1664
+ context: args.context,
1665
+ limit: piLimit(args.limit),
1666
+ });
1667
+ const call = { args, toolCallId };
1668
+ const { execResult } = await resolveExecHandler(
1669
+ call,
1670
+ execHandlers?.piGrep?.bind(execHandlers),
1671
+ onToolResult,
1672
+ buildPiGrepResult,
1673
+ buildPiGrepError,
1674
+ buildPiGrepError,
1675
+ );
1676
+ sendExecClientMessage(h2Request, execMsg, "piGrepResult", execResult);
1677
+ return;
1678
+ }
1679
+ case "piFindArgs": {
1680
+ const args = execMsg.message.value;
1681
+ const toolCallId = crypto.randomUUID();
1682
+ synthesizeCursorExecToolCall(output, stream, toolCallId, "find", {
1683
+ paths: [piJoinPath(args.path, args.pattern)],
1684
+ limit: piLimit(args.limit),
1685
+ });
1686
+ const call = { args, toolCallId };
1687
+ const { execResult } = await resolveExecHandler(
1688
+ call,
1689
+ execHandlers?.piFind?.bind(execHandlers),
1690
+ onToolResult,
1691
+ buildPiFindResult,
1692
+ buildPiFindError,
1693
+ buildPiFindError,
1694
+ );
1695
+ sendExecClientMessage(h2Request, execMsg, "piFindResult", execResult);
1696
+ return;
1697
+ }
1698
+ case "piLsArgs": {
1699
+ const args = execMsg.message.value;
1700
+ const toolCallId = crypto.randomUUID();
1701
+ synthesizeCursorExecToolCall(output, stream, toolCallId, "read", { path: piLsPath(args.path) });
1702
+ const call = { args, toolCallId };
1703
+ const { execResult } = await resolveExecHandler(
1704
+ call,
1705
+ execHandlers?.piLs?.bind(execHandlers),
1706
+ onToolResult,
1707
+ buildPiLsResult,
1708
+ buildPiLsError,
1709
+ buildPiLsError,
1710
+ );
1711
+ sendExecClientMessage(h2Request, execMsg, "piLsResult", execResult);
1712
+ return;
1713
+ }
1714
+ case "mcpStateExecArgs": {
1715
+ sendExecClientMessage(
1716
+ h2Request,
1717
+ execMsg,
1718
+ "mcpStateExecResult",
1719
+ buildMcpStateResult(requestContextTools, execMsg.message.value.serverIdentifiers),
1720
+ );
1721
+ return;
1722
+ }
1723
+ case "executeHookArgs": {
1724
+ const execResult = buildNeutralHookResult(execMsg.message.value.request);
1725
+ if (!execResult) {
1726
+ sendExecClientThrow(
1727
+ h2Request,
1728
+ execMsg,
1729
+ `Unsupported hook request: ${execMsg.message.value.request?.request.case ?? "unset"}`,
1730
+ "unknown_hook_request",
1731
+ );
1732
+ return;
1733
+ }
1734
+ sendExecClientMessage(h2Request, execMsg, "executeHookResult", execResult);
1735
+ return;
1736
+ }
1737
+ case "shellAllowlistPrecheckArgs": {
1738
+ sendExecClientMessage(
1739
+ h2Request,
1740
+ execMsg,
1741
+ "shellAllowlistPrecheckResult",
1742
+ create(ShellAllowlistPrecheckResultSchema, { allowlisted: false }),
1743
+ );
1744
+ return;
1745
+ }
1746
+ case "mcpAllowlistPrecheckArgs": {
1747
+ sendExecClientMessage(
1748
+ h2Request,
1749
+ execMsg,
1750
+ "mcpAllowlistPrecheckResult",
1751
+ create(McpAllowlistPrecheckResultSchema, { allowlisted: false }),
1752
+ );
1753
+ return;
1754
+ }
1755
+ case "webFetchAllowlistPrecheckArgs": {
1756
+ sendExecClientMessage(
1757
+ h2Request,
1758
+ execMsg,
1759
+ "webFetchAllowlistPrecheckResult",
1760
+ create(WebFetchAllowlistPrecheckResultSchema, { allowlisted: false }),
1761
+ );
1762
+ return;
1763
+ }
1764
+ default: {
1765
+ log("warn", "unhandledExecMessage", { execCase });
1766
+ sendExecClientThrow(
1767
+ h2Request,
1768
+ execMsg,
1769
+ `No handler for exec message of type ${execCase}`,
1770
+ "exec_variant_unsupported",
1771
+ );
1772
+ }
1773
+ }
1774
+ }
1775
+
1776
+ function sendExecClientMessage<TCase extends NonNullable<ExecClientMessage["message"]["case"]>>(
1777
+ h2Request: CursorRequestWriter,
1778
+ execMsg: ExecServerMessage,
1779
+ messageCase: TCase,
1780
+ value: Extract<ExecClientMessage["message"], { case: TCase }>["value"],
1781
+ ): void {
1782
+ const execClientMessage = create(ExecClientMessageSchema, {
1783
+ id: execMsg.id,
1784
+ execId: execMsg.execId,
1785
+ message: { case: messageCase, value } as ExecClientMessage["message"],
1786
+ });
1787
+
1788
+ const clientMessage = create(AgentClientMessageSchema, {
1789
+ message: { case: "execClientMessage", value: execClientMessage },
1790
+ });
1791
+
1792
+ const responseBytes = toBinary(AgentClientMessageSchema, clientMessage);
1793
+ h2Request.enqueue(frameConnectMessage(responseBytes));
1794
+
1795
+ log("execClientMessage", messageCase, value);
1796
+ }
1797
+
1798
+ function sendExecClientThrow(
1799
+ h2Request: CursorRequestWriter,
1800
+ execMsg: ExecServerMessage,
1801
+ error: string,
1802
+ errorCode: string,
1803
+ ): void {
1804
+ const controlMessage = create(ExecClientControlMessageSchema, {
1805
+ message: {
1806
+ case: "throw",
1807
+ value: create(ExecClientThrowSchema, { id: execMsg.id, error, errorCode }),
1808
+ },
1809
+ });
1810
+ const clientMessage = create(AgentClientMessageSchema, {
1811
+ message: { case: "execClientControlMessage", value: controlMessage },
1812
+ });
1813
+ h2Request.enqueue(frameConnectMessage(toBinary(AgentClientMessageSchema, clientMessage)));
1814
+ sendExecClientStreamClose(h2Request, execMsg);
1815
+ }
1816
+
1817
+ function sendExecClientStreamClose(h2Request: CursorRequestWriter, execMsg: ExecServerMessage): void {
1818
+ const closeMessage = create(ExecClientControlMessageSchema, {
1819
+ message: {
1820
+ case: "streamClose",
1821
+ value: create(ExecClientStreamCloseSchema, {
1822
+ id: execMsg.id,
1823
+ }),
1824
+ },
1825
+ });
1826
+ const clientMessage = create(AgentClientMessageSchema, {
1827
+ message: { case: "execClientControlMessage", value: closeMessage },
1828
+ });
1829
+ const responseBytes = toBinary(AgentClientMessageSchema, clientMessage);
1830
+ h2Request.enqueue(frameConnectMessage(responseBytes));
1831
+ log("execClientControl", "streamClose", { id: execMsg.id, execId: execMsg.execId });
1832
+ }
1833
+
1834
+ /** Exported for tests: verifies handler is invoked with correct `this` when passed as bound. */
1835
+ export async function resolveExecHandler<TArgs, TResult>(
1836
+ args: TArgs,
1837
+ handler: ((args: TArgs) => Promise<CursorExecHandlerResult<TResult>>) | undefined,
1838
+ onToolResult: CursorToolResultHandler | undefined,
1839
+ buildFromToolResult: (toolResult: ToolResultMessage) => TResult,
1840
+ buildRejected: (reason: string) => TResult,
1841
+ buildError: (error: string) => TResult,
1842
+ ): Promise<{ execResult: TResult; toolResult?: ToolResultMessage }> {
1843
+ if (!handler) {
1844
+ return { execResult: buildRejected("Tool not available") };
1845
+ }
1846
+
1847
+ try {
1848
+ const handlerResult = await handler(args);
1849
+ const { execResult, toolResult } = splitExecHandlerResult(handlerResult);
1850
+ const finalToolResult = await applyToolResultHandler(toolResult, onToolResult);
1851
+
1852
+ if (execResult) {
1853
+ return { execResult, toolResult: finalToolResult };
1854
+ }
1855
+ if (finalToolResult) {
1856
+ return { execResult: buildFromToolResult(finalToolResult), toolResult: finalToolResult };
1857
+ }
1858
+ return { execResult: buildRejected("Tool returned no result") };
1859
+ } catch (error) {
1860
+ const message = error instanceof Error ? error.message : String(error);
1861
+ return { execResult: buildError(message) };
1862
+ }
1863
+ }
1864
+
1865
+ /** Exported for deterministic coverage of ordered server-message handling. */
1866
+ export function createCursorMessageQueueForTest(onError?: (error: unknown) => void): {
1867
+ enqueue(handler: () => void | Promise<void>): Promise<void>;
1868
+ drain(): Promise<void>;
1869
+ } {
1870
+ let chain = Promise.resolve();
1871
+ return {
1872
+ enqueue(handler) {
1873
+ const result = chain.then(handler);
1874
+ chain = result.catch(error => {
1875
+ onError?.(error);
1876
+ });
1877
+ return result;
1878
+ },
1879
+ drain() {
1880
+ return chain;
1881
+ },
1882
+ };
1883
+ }
1884
+
1885
+ function splitExecHandlerResult<TResult>(result: CursorExecHandlerResult<TResult>): {
1886
+ execResult?: TResult;
1887
+ toolResult?: ToolResultMessage;
1888
+ } {
1889
+ if (isToolResultMessage(result)) {
1890
+ return { toolResult: result };
1891
+ }
1892
+ if (result && typeof result === "object") {
1893
+ const record = result as Record<string, unknown>;
1894
+ if ("execResult" in record) {
1895
+ const { execResult, toolResult } = record as {
1896
+ execResult: TResult;
1897
+ toolResult?: ToolResultMessage;
1898
+ };
1899
+ return { execResult, toolResult };
1900
+ }
1901
+ if ("toolResult" in record && !isToolResultMessage(record)) {
1902
+ const { result: execResult, toolResult } = record as {
1903
+ result?: TResult;
1904
+ toolResult?: ToolResultMessage;
1905
+ };
1906
+ return { execResult, toolResult };
1907
+ }
1908
+ if ("result" in record && !("$typeName" in record)) {
1909
+ const { result: execResult, toolResult } = record as {
1910
+ result: TResult;
1911
+ toolResult?: ToolResultMessage;
1912
+ };
1913
+ return { execResult, toolResult };
1914
+ }
1915
+ }
1916
+ return { execResult: result as TResult };
1917
+ }
1918
+
1919
+ function isToolResultMessage(value: unknown): value is ToolResultMessage {
1920
+ return !!value && typeof value === "object" && (value as ToolResultMessage).role === "toolResult";
1921
+ }
1922
+
1923
+ async function applyToolResultHandler(
1924
+ toolResult: ToolResultMessage | undefined,
1925
+ onToolResult: CursorToolResultHandler | undefined,
1926
+ ): Promise<ToolResultMessage | undefined> {
1927
+ if (!toolResult || !onToolResult) {
1928
+ return toolResult;
1929
+ }
1930
+ const updated = await onToolResult(toolResult);
1931
+ return updated ?? toolResult;
1932
+ }
1933
+
1934
+ function toolResultToText(toolResult: ToolResultMessage): string {
1935
+ return toolResult.content.map(item => (item.type === "text" ? item.text : `[${item.mimeType} image]`)).join("\n");
1936
+ }
1937
+
1938
+ function toolResultWasTruncated(toolResult: ToolResultMessage): boolean {
1939
+ if (!toolResult.details || typeof toolResult.details !== "object") {
1940
+ return false;
1941
+ }
1942
+ const truncation = (toolResult.details as { truncation?: { truncated?: boolean } }).truncation;
1943
+ return !!truncation?.truncated;
1944
+ }
1945
+
1946
+ function toolResultDetailBoolean(toolResult: ToolResultMessage, key: string): boolean {
1947
+ if (!toolResult.details || typeof toolResult.details !== "object") {
1948
+ return false;
1949
+ }
1950
+ const value = (toolResult.details as Record<string, unknown>)[key];
1951
+ return typeof value === "boolean" ? value : false;
1952
+ }
1953
+
1954
+ function buildReadResultFromToolResult(path: string, toolResult: ToolResultMessage) {
1955
+ const text = toolResultToText(toolResult);
1956
+ if (toolResult.isError) {
1957
+ return buildReadErrorResult(path, text || "Read failed");
1958
+ }
1959
+ const totalLines = text ? text.split("\n").length : 0;
1960
+ return create(ReadResultSchema, {
1961
+ result: {
1962
+ case: "success",
1963
+ value: create(ReadSuccessSchema, {
1964
+ path,
1965
+ totalLines,
1966
+ fileSize: BigInt(Buffer.byteLength(text, "utf-8")),
1967
+ truncated: toolResultWasTruncated(toolResult),
1968
+ output: { case: "content", value: text },
1969
+ }),
1970
+ },
1971
+ });
1972
+ }
1973
+
1974
+ function buildReadErrorResult(path: string, error: string) {
1975
+ return create(ReadResultSchema, {
1976
+ result: {
1977
+ case: "error",
1978
+ value: create(ReadErrorSchema, { path, error }),
1979
+ },
1980
+ });
1981
+ }
1982
+
1983
+ function buildReadRejectedResult(path: string, reason: string) {
1984
+ return create(ReadResultSchema, {
1985
+ result: {
1986
+ case: "rejected",
1987
+ value: create(ReadRejectedSchema, { path, reason }),
1988
+ },
1989
+ });
1990
+ }
1991
+
1992
+ function buildWriteResultFromToolResult(
1993
+ args: { path: string; fileText?: string; fileBytes?: Uint8Array; returnFileContentAfterWrite?: boolean },
1994
+ toolResult: ToolResultMessage,
1995
+ ) {
1996
+ const text = toolResultToText(toolResult);
1997
+ if (toolResult.isError) {
1998
+ return buildWriteErrorResult(args.path, text || "Write failed");
1999
+ }
2000
+ const fileText = args.fileText ?? "";
2001
+ const fileSize = args.fileBytes?.length ?? Buffer.byteLength(fileText, "utf-8");
2002
+ const linesCreated = fileText ? fileText.split("\n").length : 0;
2003
+ return create(WriteResultSchema, {
2004
+ result: {
2005
+ case: "success",
2006
+ value: create(WriteSuccessSchema, {
2007
+ path: args.path,
2008
+ linesCreated,
2009
+ fileSize,
2010
+ fileContentAfterWrite: args.returnFileContentAfterWrite ? fileText : undefined,
2011
+ }),
2012
+ },
2013
+ });
2014
+ }
2015
+
2016
+ function buildWriteErrorResult(path: string, error: string) {
2017
+ return create(WriteResultSchema, {
2018
+ result: {
2019
+ case: "error",
2020
+ value: create(WriteErrorSchema, { path, error }),
2021
+ },
2022
+ });
2023
+ }
2024
+
2025
+ function buildWriteRejectedResult(path: string, reason: string) {
2026
+ return create(WriteResultSchema, {
2027
+ result: {
2028
+ case: "rejected",
2029
+ value: create(WriteRejectedSchema, { path, reason }),
2030
+ },
2031
+ });
2032
+ }
2033
+
2034
+ function buildDeleteResultFromToolResult(path: string, toolResult: ToolResultMessage) {
2035
+ const text = toolResultToText(toolResult);
2036
+ if (toolResult.isError) {
2037
+ return buildDeleteErrorResult(path, text || "Delete failed");
2038
+ }
2039
+ return create(DeleteResultSchema, {
2040
+ result: {
2041
+ case: "success",
2042
+ value: create(DeleteSuccessSchema, {
2043
+ path,
2044
+ deletedFile: path,
2045
+ fileSize: BigInt(0),
2046
+ prevContent: "",
2047
+ }),
2048
+ },
2049
+ });
2050
+ }
2051
+
2052
+ function buildDeleteErrorResult(path: string, error: string) {
2053
+ return create(DeleteResultSchema, {
2054
+ result: {
2055
+ case: "error",
2056
+ value: create(DeleteErrorSchema, { path, error }),
2057
+ },
2058
+ });
2059
+ }
2060
+
2061
+ function buildDeleteRejectedResult(path: string, reason: string) {
2062
+ return create(DeleteResultSchema, {
2063
+ result: {
2064
+ case: "rejected",
2065
+ value: create(DeleteRejectedSchema, { path, reason }),
2066
+ },
2067
+ });
2068
+ }
2069
+
2070
+ function buildShellResultFromToolResult(
2071
+ args: { command: string; workingDirectory: string },
2072
+ toolResult: ToolResultMessage,
2073
+ ) {
2074
+ const output = toolResultToText(toolResult);
2075
+ if (toolResult.isError) {
2076
+ return buildShellFailureResult(args.command, args.workingDirectory, output || "Shell failed");
2077
+ }
2078
+ return create(ShellResultSchema, {
2079
+ result: {
2080
+ case: "success",
2081
+ value: create(ShellSuccessSchema, {
2082
+ command: args.command,
2083
+ workingDirectory: args.workingDirectory,
2084
+ exitCode: 0,
2085
+ signal: "",
2086
+ stdout: output,
2087
+ stderr: "",
2088
+ executionTime: 0,
2089
+ }),
2090
+ },
2091
+ });
2092
+ }
2093
+
2094
+ function buildShellFailureResult(command: string, workingDirectory: string, error: string) {
2095
+ return create(ShellResultSchema, {
2096
+ result: {
2097
+ case: "failure",
2098
+ value: create(ShellFailureSchema, {
2099
+ command,
2100
+ workingDirectory,
2101
+ exitCode: 1,
2102
+ signal: "",
2103
+ stdout: "",
2104
+ stderr: error,
2105
+ executionTime: 0,
2106
+ aborted: false,
2107
+ }),
2108
+ },
2109
+ });
2110
+ }
2111
+
2112
+ function buildShellRejectedResult(command: string, workingDirectory: string, reason: string) {
2113
+ return create(ShellResultSchema, {
2114
+ result: {
2115
+ case: "rejected",
2116
+ value: create(ShellRejectedSchema, {
2117
+ command,
2118
+ workingDirectory,
2119
+ reason,
2120
+ isReadonly: false,
2121
+ }),
2122
+ },
2123
+ });
2124
+ }
2125
+
2126
+ function buildLsResultFromToolResult(path: string, toolResult: ToolResultMessage) {
2127
+ const text = toolResultToText(toolResult);
2128
+ if (toolResult.isError) {
2129
+ return buildLsErrorResult(path, text || "Ls failed");
2130
+ }
2131
+ const rootPath = path || ".";
2132
+ const entries = text
2133
+ .split("\n")
2134
+ .map(line => line.trim())
2135
+ .filter(line => line.length > 0 && !line.startsWith("["));
2136
+ const childrenDirs: LsDirectoryTreeNode[] = [];
2137
+ const childrenFiles: LsDirectoryTreeNode_File[] = [];
2138
+
2139
+ for (const entry of entries) {
2140
+ const name = entry.split(" (")[0];
2141
+ if (name.endsWith("/")) {
2142
+ const dirName = name.slice(0, -1);
2143
+ childrenDirs.push(
2144
+ create(LsDirectoryTreeNodeSchema, {
2145
+ absPath: `${rootPath.replace(/\/$/, "")}/${dirName}`,
2146
+ childrenDirs: [],
2147
+ childrenFiles: [],
2148
+ childrenWereProcessed: false,
2149
+ fullSubtreeExtensionCounts: {},
2150
+ numFiles: 0,
2151
+ }),
2152
+ );
2153
+ } else {
2154
+ childrenFiles.push(create(LsDirectoryTreeNode_FileSchema, { name }));
2155
+ }
2156
+ }
2157
+
2158
+ const root = create(LsDirectoryTreeNodeSchema, {
2159
+ absPath: rootPath,
2160
+ childrenDirs,
2161
+ childrenFiles,
2162
+ childrenWereProcessed: true,
2163
+ fullSubtreeExtensionCounts: {},
2164
+ numFiles: childrenFiles.length,
2165
+ });
2166
+
2167
+ return create(LsResultSchema, {
2168
+ result: {
2169
+ case: "success",
2170
+ value: create(LsSuccessSchema, { directoryTreeRoot: root }),
2171
+ },
2172
+ });
2173
+ }
2174
+
2175
+ function buildLsErrorResult(path: string, error: string) {
2176
+ return create(LsResultSchema, {
2177
+ result: {
2178
+ case: "error",
2179
+ value: create(LsErrorSchema, { path, error }),
2180
+ },
2181
+ });
2182
+ }
2183
+
2184
+ function buildLsRejectedResult(path: string, reason: string) {
2185
+ return create(LsResultSchema, {
2186
+ result: {
2187
+ case: "rejected",
2188
+ value: create(LsRejectedSchema, { path, reason }),
2189
+ },
2190
+ });
2191
+ }
2192
+
2193
+ function buildGrepResultFromToolResult(
2194
+ args: { pattern: string; path?: string; outputMode?: string },
2195
+ toolResult: ToolResultMessage,
2196
+ ) {
2197
+ const text = toolResultToText(toolResult);
2198
+ if (toolResult.isError) {
2199
+ return buildGrepErrorResult(text || "Grep failed");
2200
+ }
2201
+
2202
+ const outputMode = args.outputMode || "content";
2203
+ const clientTruncated = toolResultDetailBoolean(toolResult, "truncated");
2204
+ const lines = text
2205
+ .split("\n")
2206
+ .map(line => line.trimEnd())
2207
+ .filter(line => line.length > 0 && !line.startsWith("[") && !line.toLowerCase().startsWith("no matches"));
2208
+
2209
+ const workspaceKey = args.path || ".";
2210
+ let unionResult: GrepUnionResult;
2211
+
2212
+ if (outputMode === "files_with_matches") {
2213
+ const files = lines;
2214
+ unionResult = create(GrepUnionResultSchema, {
2215
+ result: {
2216
+ case: "files",
2217
+ value: create(GrepFilesResultSchema, {
2218
+ files,
2219
+ totalFiles: files.length,
2220
+ clientTruncated,
2221
+ ripgrepTruncated: false,
2222
+ }),
2223
+ },
2224
+ });
2225
+ } else if (outputMode === "count") {
2226
+ const counts = lines
2227
+ .map(line => {
2228
+ const separatorIndex = line.lastIndexOf(":");
2229
+ if (separatorIndex === -1) {
2230
+ return null;
2231
+ }
2232
+ const file = line.slice(0, separatorIndex);
2233
+ const count = Number.parseInt(line.slice(separatorIndex + 1), 10);
2234
+ if (!file || Number.isNaN(count)) {
2235
+ return null;
2236
+ }
2237
+ return create(GrepFileCountSchema, { file, count });
2238
+ })
2239
+ .filter((entry): entry is GrepFileCount => entry !== null);
2240
+ const totalMatches = counts.reduce((sum, entry) => sum + entry.count, 0);
2241
+ unionResult = create(GrepUnionResultSchema, {
2242
+ result: {
2243
+ case: "count",
2244
+ value: create(GrepCountResultSchema, {
2245
+ counts,
2246
+ totalFiles: counts.length,
2247
+ totalMatches,
2248
+ clientTruncated,
2249
+ ripgrepTruncated: false,
2250
+ }),
2251
+ },
2252
+ });
2253
+ } else {
2254
+ const matchMap = new Map<string, Array<{ line: number; content: string; isContextLine: boolean }>>();
2255
+ let totalMatchedLines = 0;
2256
+
2257
+ for (const line of lines) {
2258
+ const matchLine = line.match(/^(.+?):(\d+):\s?(.*)$/);
2259
+ const contextLine = line.match(/^(.+?)-(\d+)-\s?(.*)$/);
2260
+ const match = matchLine ?? contextLine;
2261
+ if (!match) {
2262
+ continue;
2263
+ }
2264
+ const [, file, lineNumber, content] = match;
2265
+ const isContextLine = Boolean(contextLine);
2266
+ const list = matchMap.get(file) ?? [];
2267
+ list.push({ line: Number(lineNumber), content, isContextLine });
2268
+ matchMap.set(file, list);
2269
+ if (!isContextLine) {
2270
+ totalMatchedLines += 1;
2271
+ }
2272
+ }
2273
+
2274
+ const matches = Array.from(matchMap.entries()).map(([file, matches]) =>
2275
+ create(GrepFileMatchSchema, {
2276
+ file,
2277
+ matches: matches.map(entry =>
2278
+ create(GrepContentMatchSchema, {
2279
+ lineNumber: entry.line,
2280
+ content: entry.content,
2281
+ contentTruncated: false,
2282
+ isContextLine: entry.isContextLine,
2283
+ }),
2284
+ ),
2285
+ }),
2286
+ );
2287
+ const totalLines = matches.reduce((sum, entry) => sum + entry.matches.length, 0);
2288
+ unionResult = create(GrepUnionResultSchema, {
2289
+ result: {
2290
+ case: "content",
2291
+ value: create(GrepContentResultSchema, {
2292
+ matches,
2293
+ totalLines,
2294
+ totalMatchedLines,
2295
+ clientTruncated,
2296
+ ripgrepTruncated: false,
2297
+ }),
2298
+ },
2299
+ });
2300
+ }
2301
+
2302
+ return create(GrepResultSchema, {
2303
+ result: {
2304
+ case: "success",
2305
+ value: create(GrepSuccessSchema, {
2306
+ pattern: args.pattern,
2307
+ path: args.path || "",
2308
+ outputMode,
2309
+ workspaceResults: { [workspaceKey]: unionResult },
2310
+ }),
2311
+ },
2312
+ });
2313
+ }
2314
+
2315
+ function buildGrepErrorResult(error: string) {
2316
+ return create(GrepResultSchema, {
2317
+ result: {
2318
+ case: "error",
2319
+ value: create(GrepErrorSchema, { error }),
2320
+ },
2321
+ });
2322
+ }
2323
+
2324
+ function buildDiagnosticsResultFromToolResult(path: string, toolResult: ToolResultMessage) {
2325
+ const text = toolResultToText(toolResult);
2326
+ if (toolResult.isError) {
2327
+ return buildDiagnosticsErrorResult(path, text || "Diagnostics failed");
2328
+ }
2329
+ return create(DiagnosticsResultSchema, {
2330
+ result: {
2331
+ case: "success",
2332
+ value: create(DiagnosticsSuccessSchema, {
2333
+ path,
2334
+ diagnostics: [],
2335
+ totalDiagnostics: 0,
2336
+ }),
2337
+ },
2338
+ });
2339
+ }
2340
+
2341
+ function buildDiagnosticsErrorResult(_path: string, error: string) {
2342
+ return create(DiagnosticsResultSchema, {
2343
+ result: {
2344
+ case: "error",
2345
+ value: create(DiagnosticsErrorSchema, { error }),
2346
+ },
2347
+ });
2348
+ }
2349
+
2350
+ function buildDiagnosticsRejectedResult(path: string, reason: string) {
2351
+ return create(DiagnosticsResultSchema, {
2352
+ result: {
2353
+ case: "rejected",
2354
+ value: create(DiagnosticsRejectedSchema, { path, reason }),
2355
+ },
2356
+ });
2357
+ }
2358
+
2359
+ function parseToolArgsJson(text: string): unknown {
2360
+ const trimmed = text.trim();
2361
+ if (!trimmed) {
2362
+ return text;
2363
+ }
2364
+ try {
2365
+ const normalized = trimmed
2366
+ .replace(/\bNone\b/g, "null")
2367
+ .replace(/\bTrue\b/g, "true")
2368
+ .replace(/\bFalse\b/g, "false");
2369
+ return Bun.JSON5.parse(normalized);
2370
+ } catch {}
2371
+ return text;
2372
+ }
2373
+
2374
+ function decodeMcpArgValue(value: Uint8Array): unknown {
2375
+ try {
2376
+ const parsedValue = fromBinary(ValueSchema, value);
2377
+ const jsonValue = toJson(ValueSchema, parsedValue) as JsonValue;
2378
+ if (typeof jsonValue === "string") {
2379
+ return parseToolArgsJson(jsonValue);
2380
+ }
2381
+ return jsonValue;
2382
+ } catch {}
2383
+ const text = new TextDecoder().decode(value);
2384
+ return parseToolArgsJson(text);
2385
+ }
2386
+
2387
+ function decodeMcpArgsMap(args?: Record<string, Uint8Array>): Record<string, unknown> | undefined {
2388
+ if (!args) {
2389
+ return undefined;
2390
+ }
2391
+ const decoded: Record<string, unknown> = {};
2392
+ for (const [key, value] of Object.entries(args)) {
2393
+ decoded[key] = decodeMcpArgValue(value);
2394
+ }
2395
+ return decoded;
2396
+ }
2397
+
2398
+ function decodeMcpCall(args: {
2399
+ name: string;
2400
+ args: Record<string, Uint8Array>;
2401
+ toolCallId: string;
2402
+ providerIdentifier: string;
2403
+ toolName: string;
2404
+ }): CursorMcpCall {
2405
+ const decodedArgs: Record<string, unknown> = {};
2406
+ for (const [key, value] of Object.entries(args.args ?? {})) {
2407
+ decodedArgs[key] = decodeMcpArgValue(value);
2408
+ }
2409
+ return {
2410
+ name: args.name,
2411
+ providerIdentifier: args.providerIdentifier,
2412
+ toolName: args.toolName || args.name,
2413
+ toolCallId: args.toolCallId,
2414
+ args: decodedArgs,
2415
+ rawArgs: args.args ?? {},
2416
+ };
2417
+ }
2418
+
2419
+ function mapTodoStatusValue(status?: number): "pending" | "in_progress" | "completed" {
2420
+ switch (status) {
2421
+ case 2:
2422
+ return "in_progress";
2423
+ case 3:
2424
+ return "completed";
2425
+ default:
2426
+ return "pending";
2427
+ }
2428
+ }
2429
+
2430
+ interface CursorTodoItem {
2431
+ id?: string;
2432
+ content?: string;
2433
+ status?: number;
2434
+ }
2435
+
2436
+ interface CursorUpdateTodosToolCall {
2437
+ updateTodosToolCall?: { args?: { todos?: CursorTodoItem[] } };
2438
+ tool?: { case?: string; value?: { args?: { todos?: CursorTodoItem[] } } };
2439
+ }
2440
+
2441
+ function buildTodoWriteArgs(toolCall: CursorUpdateTodosToolCall): {
2442
+ todos: Array<{ id?: string; content: string; activeForm: string; status: "pending" | "in_progress" | "completed" }>;
2443
+ } | null {
2444
+ const updateCall =
2445
+ toolCall.tool?.case === "updateTodosToolCall" ? toolCall.tool.value : toolCall.updateTodosToolCall;
2446
+ const todos = updateCall?.args?.todos;
2447
+ if (!todos) return null;
2448
+ return {
2449
+ todos: todos.map(todo => ({
2450
+ id: typeof todo.id === "string" && todo.id.length > 0 ? todo.id : undefined,
2451
+ content: typeof todo.content === "string" ? todo.content : "",
2452
+ activeForm: typeof todo.content === "string" ? todo.content : "",
2453
+ status: mapTodoStatusValue(typeof todo.status === "number" ? todo.status : undefined),
2454
+ })),
2455
+ };
2456
+ }
2457
+
2458
+ // Map a cursor ToolCall oneof field name (e.g. "shellToolCall") to a display tool
2459
+ // name. Mirrors cli-jaw's cursorToolKindLabel (src/agent/events/cursor.ts) so the
2460
+ // two surfaces label cursor-native tools identically.
2461
+ const CURSOR_NATIVE_KIND_ALIASES: Record<string, string> = {
2462
+ shell: "bash",
2463
+ read: "read",
2464
+ write: "write",
2465
+ delete: "delete",
2466
+ edit: "edit",
2467
+ grep: "grep",
2468
+ glob: "glob",
2469
+ ls: "ls",
2470
+ semSearch: "codebase_search",
2471
+ webSearch: "web_search",
2472
+ fetch: "fetch",
2473
+ task: "task",
2474
+ createPlan: "create_plan",
2475
+ askQuestion: "ask_question",
2476
+ readLints: "read_lints",
2477
+ applyAgentDiff: "apply_diff",
2478
+ };
2479
+
2480
+ function cursorNativeToolName(kindKey: string): string {
2481
+ const base = kindKey.replace(/ToolCall$/i, "");
2482
+ if (!base) return "tool";
2483
+ return CURSOR_NATIVE_KIND_ALIASES[base] ?? base;
2484
+ }
2485
+
2486
+ // Cursor's model sometimes calls its own native IDE tools (shell/glob/grep/…)
2487
+ // instead of the advertised MCP tools. Those arrive as ToolCall oneof variants we
2488
+ // do not otherwise handle (everything except mcpToolCall / updateTodosToolCall), so
2489
+ // without this they are silently dropped and never render. Build a generic toolCall
2490
+ // block from whichever *ToolCall field is set so the call (and its result) is shown.
2491
+
2492
+ /** Hard node budget for one native-payload conversion; bounds hostile or cyclic graphs. */
2493
+ const CURSOR_JSON_SAFE_MAX_NODES = 10_000;
2494
+ const CURSOR_JSON_SAFE_MAX_DEPTH = 100;
2495
+
2496
+ /**
2497
+ * Total conversion of a Cursor protobuf payload into plain JSON-safe data.
2498
+ *
2499
+ * protobuf-es v2 messages are plain objects, but they carry `$typeName`
2500
+ * markers, `bigint` fields (e.g. `fileSize`, `durationMs`, `timestampMs`,
2501
+ * `fileOutputThresholdBytes`), and `Uint8Array` blobs. None of those may leak
2502
+ * into assistant message content: toolCall `arguments` are staged into managed
2503
+ * snapshots, persisted to the JSONL transcript, and replayed to providers —
2504
+ * all of which require `JSON.stringify`-safe values. Attaching the raw payload
2505
+ * is exactly the local-snapshot producer defect class behind issue #4578.
2506
+ *
2507
+ * Rules: `$typeName` is stripped, safe-range bigints become numbers (decimal
2508
+ * strings beyond `Number.MAX_SAFE_INTEGER`), byte arrays become base64
2509
+ * strings, dates become ISO strings, functions/symbols are dropped, cycles
2510
+ * and over-depth values collapse to null, and containers stop accepting
2511
+ * entries once the shared node budget is exhausted.
2512
+ */
2513
+ function cursorJsonSafeValue(value: unknown, path?: Set<object>, budget?: { remaining: number }, depth = 0): unknown {
2514
+ const seen = path ?? new Set<object>();
2515
+ const nodes = budget ?? { remaining: CURSOR_JSON_SAFE_MAX_NODES };
2516
+ if (nodes.remaining-- <= 0) return null;
2517
+ if (depth >= CURSOR_JSON_SAFE_MAX_DEPTH) return null;
2518
+ if (typeof value === "bigint") {
2519
+ return value <= BigInt(Number.MAX_SAFE_INTEGER) && value >= BigInt(-Number.MAX_SAFE_INTEGER)
2520
+ ? Number(value)
2521
+ : value.toString();
2522
+ }
2523
+ if (typeof value === "function" || typeof value === "symbol" || value === undefined) return null;
2524
+ if (typeof value === "number" && !Number.isFinite(value)) return null;
2525
+ if (value === null || typeof value !== "object") return value;
2526
+ if (seen.has(value)) return null;
2527
+ if (value instanceof Uint8Array)
2528
+ return Buffer.from(value.buffer, value.byteOffset, value.byteLength).toString("base64");
2529
+ if (value instanceof Date) return Number.isFinite(value.getTime()) ? value.toISOString() : null;
2530
+ seen.add(value);
2531
+ try {
2532
+ if (Array.isArray(value)) {
2533
+ const array: unknown[] = [];
2534
+ for (const entry of value) {
2535
+ if (nodes.remaining <= 0) break;
2536
+ array.push(cursorJsonSafeValue(entry, seen, nodes, depth + 1));
2537
+ }
2538
+ return array;
2539
+ }
2540
+ const record: Record<string, unknown> = {};
2541
+ for (const [key, entry] of Object.entries(value)) {
2542
+ if (key === "$typeName") continue;
2543
+ if (nodes.remaining <= 0) break;
2544
+ record[key] = cursorJsonSafeValue(entry, seen, nodes, depth + 1);
2545
+ }
2546
+ return record;
2547
+ } catch {
2548
+ return null;
2549
+ } finally {
2550
+ seen.delete(value);
2551
+ }
2552
+ }
2553
+
2554
+ /** Exported for direct regression coverage of the JSON-safety boundary. */
2555
+ export function cursorJsonSafeValueForTest(value: unknown): unknown {
2556
+ return cursorJsonSafeValue(value);
2557
+ }
2558
+
2559
+ function selectMcpToolCall(toolCall: any): any {
2560
+ return toolCall?.tool?.case === "mcpToolCall" ? toolCall.tool.value : toolCall?.mcpToolCall;
2561
+ }
2562
+
2563
+ const CURSOR_EXEC_OWNED_TOOL_CASES = new Set([
2564
+ "piReadToolCall",
2565
+ "piBashToolCall",
2566
+ "piEditToolCall",
2567
+ "piWriteToolCall",
2568
+ "piGrepToolCall",
2569
+ "piFindToolCall",
2570
+ "piLsToolCall",
2571
+ ]);
2572
+
2573
+ function isExecOwnedToolCall(toolCall: any): boolean {
2574
+ return CURSOR_EXEC_OWNED_TOOL_CASES.has(toolCall?.tool?.case);
2575
+ }
2576
+
2577
+ export function buildNativeToolCallBlock(
2578
+ toolCall: Record<string, unknown>,
2579
+ callId: string,
2580
+ index: number,
2581
+ ): ToolCallState | null {
2582
+ const oneof = toolCall.tool as { case?: string; value?: unknown } | undefined;
2583
+ if (oneof?.case && oneof.value && typeof oneof.value === "object") {
2584
+ const args = (oneof.value as { args?: unknown }).args;
2585
+ const convertedArgs = cursorJsonSafeValue(args ?? oneof.value);
2586
+ return {
2587
+ type: "toolCall",
2588
+ id: callId,
2589
+ name: cursorNativeToolName(oneof.case),
2590
+ arguments:
2591
+ convertedArgs && typeof convertedArgs === "object" && !Array.isArray(convertedArgs)
2592
+ ? (convertedArgs as Record<string, unknown>)
2593
+ : { raw: convertedArgs },
2594
+ index,
2595
+ kind: "native",
2596
+ };
2597
+ }
2598
+ for (const [key, payload] of Object.entries(toolCall)) {
2599
+ if (!/ToolCall$/.test(key) || !payload || typeof payload !== "object") continue;
2600
+ if (key === "mcpToolCall" || key === "updateTodosToolCall") continue;
2601
+ const args = (payload as { args?: unknown }).args;
2602
+ const hasObjectArgs = args !== null && typeof args === "object";
2603
+ const convertedArgs = hasObjectArgs ? cursorJsonSafeValue(args) : undefined;
2604
+ const safeArguments =
2605
+ convertedArgs !== undefined &&
2606
+ convertedArgs !== null &&
2607
+ typeof convertedArgs === "object" &&
2608
+ !Array.isArray(convertedArgs)
2609
+ ? (convertedArgs as Record<string, unknown>)
2610
+ : { raw: hasObjectArgs ? convertedArgs : cursorJsonSafeValue(payload) };
2611
+ return {
2612
+ type: "toolCall",
2613
+ id: callId,
2614
+ name: cursorNativeToolName(key),
2615
+ arguments: safeArguments,
2616
+ index,
2617
+ kind: "native",
2618
+ };
2619
+ }
2620
+ return null;
2621
+ }
2622
+
2623
+ function buildMcpResultFromToolResult(_mcpCall: CursorMcpCall, toolResult: ToolResultMessage) {
2624
+ if (toolResult.isError) {
2625
+ return buildMcpErrorResult(toolResultToText(toolResult) || "MCP tool failed");
2626
+ }
2627
+ const content = toolResult.content.map(item => {
2628
+ if (item.type === "image") {
2629
+ return create(McpToolResultContentItemSchema, {
2630
+ content: {
2631
+ case: "image",
2632
+ value: create(McpImageContentSchema, {
2633
+ data: Uint8Array.from(Buffer.from(item.data, "base64")),
2634
+ mimeType: item.mimeType,
2635
+ }),
2636
+ },
2637
+ });
2638
+ }
2639
+ return create(McpToolResultContentItemSchema, {
2640
+ content: {
2641
+ case: "text",
2642
+ value: create(McpTextContentSchema, { text: item.text }),
2643
+ },
2644
+ });
2645
+ });
2646
+
2647
+ return create(McpResultSchema, {
2648
+ result: {
2649
+ case: "success",
2650
+ value: create(McpSuccessSchema, {
2651
+ content,
2652
+ isError: false,
2653
+ }),
2654
+ },
2655
+ });
2656
+ }
2657
+
2658
+ function buildMcpToolNotFoundResult(mcpCall: CursorMcpCall) {
2659
+ return create(McpResultSchema, {
2660
+ result: {
2661
+ case: "toolNotFound",
2662
+ value: create(McpToolNotFoundSchema, { name: mcpCall.toolName, availableTools: [] }),
2663
+ },
2664
+ });
2665
+ }
2666
+
2667
+ function buildMcpErrorResult(error: string) {
2668
+ return create(McpResultSchema, {
2669
+ result: {
2670
+ case: "error",
2671
+ value: create(McpErrorSchema, { error }),
2672
+ },
2673
+ });
2674
+ }
2675
+
2676
+ function synthesizeCursorExecToolCall(
2677
+ output: AssistantMessage,
2678
+ stream: AssistantMessageEventStream,
2679
+ toolCallId: string,
2680
+ name: string,
2681
+ args: Record<string, unknown>,
2682
+ ): void {
2683
+ const block: ToolCallState = {
2684
+ type: "toolCall",
2685
+ id: toolCallId,
2686
+ name,
2687
+ arguments: cursorJsonSafeValue(args) as Record<string, unknown>,
2688
+ index: output.content.length,
2689
+ kind: "cursor-exec",
2690
+ [kCursorExecResolved]: true,
2691
+ };
2692
+ output.content.push(block);
2693
+ const contentIndex = output.content.length - 1;
2694
+ stream.push({ type: "toolcall_start", contentIndex, partial: output });
2695
+ delete (block as Partial<ToolCallState>).index;
2696
+ delete (block as Partial<ToolCallState>).kind;
2697
+ stream.push({ type: "toolcall_end", contentIndex, toolCall: block, partial: output });
2698
+ }
2699
+
2700
+ function processInteractionUpdate(
2701
+ update: any,
2702
+ output: AssistantMessage,
2703
+ stream: AssistantMessageEventStream,
2704
+ state: BlockState,
2705
+ usageState: UsageState,
2706
+ ): void {
2707
+ const updateCase = update.message?.case;
2708
+
2709
+ log("interactionUpdate", updateCase, update.message?.value);
2710
+
2711
+ if (updateCase === "textDelta") {
2712
+ state.setFirstTokenTime();
2713
+ const delta = update.message.value.text || "";
2714
+ if (!state.currentTextBlock) {
2715
+ const block: TextContent & { index: number } = {
2716
+ type: "text",
2717
+ text: "",
2718
+ index: output.content.length,
2719
+ };
2720
+ output.content.push(block);
2721
+ state.setTextBlock(block);
2722
+ stream.push({ type: "text_start", contentIndex: output.content.length - 1, partial: output });
2723
+ }
2724
+ state.currentTextBlock!.text += delta;
2725
+ const idx = output.content.indexOf(state.currentTextBlock!);
2726
+ stream.push({ type: "text_delta", contentIndex: idx, delta, partial: output });
2727
+ } else if (updateCase === "thinkingDelta") {
2728
+ state.setFirstTokenTime();
2729
+ const delta = update.message.value.text || "";
2730
+ if (!state.currentThinkingBlock) {
2731
+ const block: ThinkingContent & { index: number } = {
2732
+ type: "thinking",
2733
+ thinking: "",
2734
+ index: output.content.length,
2735
+ };
2736
+ output.content.push(block);
2737
+ state.setThinkingBlock(block);
2738
+ stream.push({ type: "thinking_start", contentIndex: output.content.length - 1, partial: output });
2739
+ }
2740
+ state.currentThinkingBlock!.thinking += delta;
2741
+ const idx = output.content.indexOf(state.currentThinkingBlock!);
2742
+ stream.push({ type: "thinking_delta", contentIndex: idx, delta, partial: output });
2743
+ } else if (updateCase === "thinkingCompleted") {
2744
+ if (state.currentThinkingBlock) {
2745
+ const idx = output.content.indexOf(state.currentThinkingBlock);
2746
+ delete (state.currentThinkingBlock as any).index;
2747
+ stream.push({
2748
+ type: "thinking_end",
2749
+ contentIndex: idx,
2750
+ content: state.currentThinkingBlock.thinking,
2751
+ partial: output,
2752
+ });
2753
+ state.setThinkingBlock(null);
2754
+ }
2755
+ } else if (updateCase === "toolCallStarted" && isExecOwnedToolCall(update.message.value.toolCall)) {
2756
+ // Pi stream call IDs and exec IDs are distinct namespaces; without a shared
2757
+ // correlation field, suppress the streamed variant and synthesize from exec.
2758
+ log("exec", "streamedToolCallOwnedByExec", { case: update.message.value.toolCall?.tool?.case });
2759
+ } else if (updateCase === "toolCallStarted") {
2760
+ const toolCall = update.message.value.toolCall;
2761
+ if (toolCall) {
2762
+ const mcpCall = selectMcpToolCall(toolCall);
2763
+ if (mcpCall) {
2764
+ const args = mcpCall.args || {};
2765
+ const block: ToolCallState = {
2766
+ type: "toolCall",
2767
+ id: args.toolCallId || crypto.randomUUID(),
2768
+ name: args.name || args.toolName || "",
2769
+ arguments: {},
2770
+ index: output.content.length,
2771
+ partialJson: "",
2772
+ kind: "mcp",
2773
+ };
2774
+ output.content.push(block);
2775
+ state.setToolCall(block);
2776
+ stream.push({ type: "toolcall_start", contentIndex: output.content.length - 1, partial: output });
2777
+ return;
2778
+ }
2779
+
2780
+ const todoArgs = buildTodoWriteArgs(toolCall);
2781
+ if (todoArgs) {
2782
+ const callId = update.message.value.callId || crypto.randomUUID();
2783
+ const block: ToolCallState = {
2784
+ type: "toolCall",
2785
+ id: callId,
2786
+ name: "todo_write",
2787
+ arguments: todoArgs,
2788
+ index: output.content.length,
2789
+ kind: "todo_write",
2790
+ };
2791
+ output.content.push(block);
2792
+ state.setToolCall(block);
2793
+ stream.push({ type: "toolcall_start", contentIndex: output.content.length - 1, partial: output });
2794
+ return;
2795
+ }
2796
+
2797
+ // Fallback: cursor-native tool variants (shell/glob/grep/…) we don't model
2798
+ // explicitly. Render them so the call and its result are visible instead of
2799
+ // vanishing.
2800
+ const nativeBlock = buildNativeToolCallBlock(
2801
+ toolCall,
2802
+ update.message.value.callId || crypto.randomUUID(),
2803
+ output.content.length,
2804
+ );
2805
+ if (nativeBlock) {
2806
+ output.content.push(nativeBlock);
2807
+ state.setToolCall(nativeBlock);
2808
+ stream.push({ type: "toolcall_start", contentIndex: output.content.length - 1, partial: output });
2809
+ }
2810
+ }
2811
+ } else if (updateCase === "toolCallDelta" || updateCase === "partialToolCall") {
2812
+ if (state.currentToolCall?.kind === "mcp") {
2813
+ const delta = update.message.value.argsTextDelta || "";
2814
+ state.currentToolCall.partialJson = `${state.currentToolCall.partialJson ?? ""}${delta}`;
2815
+ state.currentToolCall.arguments = parseStreamingJson(state.currentToolCall.partialJson ?? "");
2816
+ const idx = output.content.indexOf(state.currentToolCall);
2817
+ stream.push({ type: "toolcall_delta", contentIndex: idx, delta, partial: output });
2818
+ }
2819
+ } else if (updateCase === "toolCallCompleted") {
2820
+ if (state.currentToolCall) {
2821
+ const toolCall = update.message.value.toolCall;
2822
+ if (state.currentToolCall.kind === "mcp") {
2823
+ captureUnicodeEscapeEvidence(state.currentToolCall, state.currentToolCall.partialJson ?? "");
2824
+ const decodedArgs = decodeMcpArgsMap(selectMcpToolCall(toolCall)?.args?.args);
2825
+ if (decodedArgs) {
2826
+ state.currentToolCall.arguments = decodedArgs;
2827
+ }
2828
+ } else if (state.currentToolCall.kind === "todo_write" && toolCall) {
2829
+ const todoArgs = buildTodoWriteArgs(toolCall);
2830
+ if (todoArgs) {
2831
+ state.currentToolCall.arguments = todoArgs;
2832
+ }
2833
+ }
2834
+ const idx = output.content.indexOf(state.currentToolCall);
2835
+ delete (state.currentToolCall as any).partialJson;
2836
+ delete (state.currentToolCall as any).index;
2837
+ delete (state.currentToolCall as any).kind;
2838
+ stream.push({ type: "toolcall_end", contentIndex: idx, toolCall: state.currentToolCall, partial: output });
2839
+ state.setToolCall(null);
2840
+ }
2841
+ } else if (updateCase === "turnEnded") {
2842
+ output.stopReason = "stop";
2843
+ } else if (updateCase === "tokenDelta") {
2844
+ const tokenDelta = update.message.value;
2845
+ usageState.sawTokenDelta = true;
2846
+ output.usage.output += tokenDelta.tokens || 0;
2847
+ output.usage.totalTokens = output.usage.input + output.usage.output;
2848
+ }
2849
+ }
2850
+
2851
+ function handleConversationCheckpointUpdate(
2852
+ checkpoint: ConversationStateStructure,
2853
+ output: AssistantMessage,
2854
+ usageState: UsageState,
2855
+ onConversationCheckpoint?: (checkpoint: ConversationStateStructure) => void,
2856
+ ): void {
2857
+ onConversationCheckpoint?.(checkpoint);
2858
+ if (usageState.sawTokenDelta) {
2859
+ return;
2860
+ }
2861
+ const usedTokens = checkpoint.tokenDetails?.usedTokens ?? 0;
2862
+ if (usedTokens <= 0) {
2863
+ return;
2864
+ }
2865
+ if (output.usage.output !== usedTokens) {
2866
+ output.usage.output = usedTokens;
2867
+ output.usage.totalTokens = output.usage.input + output.usage.output;
2868
+ }
2869
+ }
2870
+
2871
+ function createBlobId(data: Uint8Array): Uint8Array {
2872
+ return new Uint8Array(createHash("sha256").update(data).digest());
2873
+ }
2874
+
2875
+ function storeCursorBlob(blobStore: Map<string, Uint8Array>, data: Uint8Array): Uint8Array {
2876
+ const blobId = createBlobId(data);
2877
+ blobStore.set(Buffer.from(blobId).toString("hex"), data);
2878
+ return blobId;
2879
+ }
2880
+
2881
+ function readCursorBlob(blobStore: Map<string, Uint8Array>, blobId: Uint8Array): Uint8Array {
2882
+ const data = blobStore.get(Buffer.from(blobId).toString("hex"));
2883
+ if (!data) {
2884
+ throw new Error("Cursor blob not found");
2885
+ }
2886
+ return data;
2887
+ }
2888
+
2889
+ const CURSOR_NATIVE_TOOL_NAMES = new Set(["bash", "read", "write", "delete", "ls", "grep", "lsp", "todo_write"]);
2890
+
2891
+ function buildMcpToolDefinitions(tools: Tool[] | undefined): McpToolDefinition[] {
2892
+ if (!tools || tools.length === 0) {
2893
+ return [];
2894
+ }
2895
+
2896
+ const advertisedTools = tools.filter(tool => !CURSOR_NATIVE_TOOL_NAMES.has(tool.name));
2897
+ if (advertisedTools.length === 0) {
2898
+ return [];
2899
+ }
2900
+
2901
+ return advertisedTools.map(tool => {
2902
+ const jsonSchema = flattenToolRootCombinators(toolWireSchema(tool));
2903
+ const schemaValue: JsonValue =
2904
+ jsonSchema && typeof jsonSchema === "object"
2905
+ ? (jsonSchema as JsonValue)
2906
+ : { type: "object", properties: {}, required: [] };
2907
+ const inputSchema = toBinary(ValueSchema, fromJson(ValueSchema, schemaValue));
2908
+ return create(McpToolDefinitionSchema, {
2909
+ name: tool.name,
2910
+ description: tool.description || "",
2911
+ providerIdentifier: "pi-agent",
2912
+ toolName: tool.name,
2913
+ inputSchema,
2914
+ });
2915
+ });
2916
+ }
2917
+
2918
+ /**
2919
+ * Extract text content from a user or developer message.
2920
+ */
2921
+ function extractUserMessageText(msg: Message): string {
2922
+ if (msg.role !== "user" && msg.role !== "developer") return "";
2923
+ const content = msg.content;
2924
+ if (typeof content === "string") return content.trim();
2925
+ const text = content
2926
+ .filter((c): c is TextContent => c.type === "text")
2927
+ .map(c => c.text)
2928
+ .join("\n");
2929
+ return text.trim();
2930
+ }
2931
+
2932
+ function hasUserMessageImages(msg: Message): boolean {
2933
+ return (
2934
+ (msg.role === "user" || msg.role === "developer") &&
2935
+ Array.isArray(msg.content) &&
2936
+ msg.content.some(item => item.type === "image")
2937
+ );
2938
+ }
2939
+
2940
+ type CursorRootPromptContentPart = { type: "text"; text: string } | { type: "image"; image: string; mediaType: string };
2941
+
2942
+ function buildCursorRootPromptContent(content: string | (TextContent | ImageContent)[]): CursorRootPromptContentPart[] {
2943
+ if (typeof content === "string") {
2944
+ const text = content.trim();
2945
+ return text ? [{ type: "text", text }] : [];
2946
+ }
2947
+ const parts: CursorRootPromptContentPart[] = [];
2948
+ for (const item of content) {
2949
+ if (item.type === "text") {
2950
+ const text = item.text.trim();
2951
+ if (text) {
2952
+ parts.push({ type: "text", text });
2953
+ }
2954
+ } else {
2955
+ parts.push({ type: "image", image: item.data, mediaType: item.mimeType });
2956
+ }
2957
+ }
2958
+ return parts;
2959
+ }
2960
+
2961
+ function cursorUserContentKey(content: string | (TextContent | ImageContent)[]): string {
2962
+ if (typeof content === "string") {
2963
+ return content.trim();
2964
+ }
2965
+ const hash = createHash("sha256");
2966
+ for (const item of content) {
2967
+ hash.update(item.type);
2968
+ if (item.type === "text") {
2969
+ hash.update(item.text);
2970
+ } else {
2971
+ hash.update(item.mimeType);
2972
+ hash.update(item.data);
2973
+ }
2974
+ }
2975
+ return hash.digest("hex");
2976
+ }
2977
+
2978
+ /**
2979
+ * Extract text content from an assistant message.
2980
+ */
2981
+ function extractAssistantMessageText(msg: Message): string {
2982
+ if (msg.role !== "assistant") return "";
2983
+ if (!Array.isArray(msg.content)) return "";
2984
+ return msg.content
2985
+ .filter((c): c is TextContent => c.type === "text")
2986
+ .map(c => c.text)
2987
+ .join("\n");
2988
+ }
2989
+
2990
+ /**
2991
+ * Derive a stable, UUID-formatted `message_id` from a content key.
2992
+ * Ensures identical historical messages hash to the same blob IDs across
2993
+ * requests, so `conversationBlobStores` does not grow unboundedly and
2994
+ * unchanged history reuses existing blob IDs.
2995
+ */
2996
+ type CursorMessageId = `${string}-${string}-${string}-${string}-${string}`;
2997
+
2998
+ function deterministicMessageId(key: string): CursorMessageId {
2999
+ const hex = createHash("sha256").update(key).digest("hex");
3000
+ return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20, 32)}`;
3001
+ }
3002
+
3003
+ /**
3004
+ * Index of the last user/developer message in `messages`, or -1 if none.
3005
+ * Used to exclude the current user turn from history builders — it goes in
3006
+ * `ConversationActionSchema.userMessageAction`, not in history structures.
3007
+ */
3008
+ function findLastUserMessageIndex(messages: Message[]): number {
3009
+ for (let i = messages.length - 1; i >= 0; i--) {
3010
+ const role = messages[i].role;
3011
+ if (role === "user" || role === "developer") {
3012
+ return i;
3013
+ }
3014
+ }
3015
+ return -1;
3016
+ }
3017
+
3018
+ /**
3019
+ * Build `ConversationStateStructure.rootPromptMessagesJson` blob IDs for the
3020
+ * system prompt plus prior conversation history, as JSON blobs matching
3021
+ * Cursor's internal Vercel-AI-SDK-shaped message format.
3022
+ *
3023
+ * Cursor's server uses `rootPromptMessagesJson` (not `turns[]`) to build the
3024
+ * actual model prompt. `turns[]` is UI/display metadata. Without populating
3025
+ * this field, multi-turn conversations lose prior context — the model sees
3026
+ * only an empty placeholder where historical user turns should be.
3027
+ * The last user message is excluded because it is sent in the action.
3028
+ */
3029
+ /**
3030
+ * Build one Cursor system-message JSON blob per ordered system prompt. Emitting separate blobs
3031
+ * (rather than a single `\n\n`-joined string) lets Cursor's blob cache hit independently per
3032
+ * entry: changing only the last prompt does not invalidate earlier blob ids, so the prefix
3033
+ * up to the changed prompt remains cached on the server side.
3034
+ *
3035
+ * When no system prompts are provided, returns a single default greeting so we never emit
3036
+ * an empty `rootPromptMessagesJson` head.
3037
+ */
3038
+ export function buildCursorSystemPromptJsons(systemPrompt: readonly string[] | undefined, modelId?: string): string[] {
3039
+ const systemPrompts = normalizeSystemPrompts(systemPrompt);
3040
+ const jsons =
3041
+ systemPrompts.length === 0
3042
+ ? [JSON.stringify({ role: "system", content: "You are a helpful assistant." })]
3043
+ : systemPrompts.map(content => JSON.stringify({ role: "system", content }));
3044
+ // Composer-harness models need anchor/edit discipline pinned ahead of any
3045
+ // host/default prompt (see composer-discipline.ts for the observed failure modes).
3046
+ if (modelId !== undefined && isComposerHarnessModel(modelId)) {
3047
+ jsons.unshift(JSON.stringify({ role: "system", content: CURSOR_COMPOSER_EDIT_DISCIPLINE_PROMPT }));
3048
+ }
3049
+ return jsons;
3050
+ }
3051
+
3052
+ function buildRootPromptMessagesJson(
3053
+ messages: Message[],
3054
+ systemPromptIds: Uint8Array[],
3055
+ blobStore: Map<string, Uint8Array>,
3056
+ ): Uint8Array[] {
3057
+ const entries: Uint8Array[] = [...systemPromptIds];
3058
+ const lastUserIdx = findLastUserMessageIndex(messages);
3059
+
3060
+ const pushJson = (obj: unknown) => {
3061
+ const bytes = new TextEncoder().encode(JSON.stringify(obj));
3062
+ entries.push(storeCursorBlob(blobStore, bytes));
3063
+ };
3064
+
3065
+ for (let i = 0; i < messages.length; i++) {
3066
+ if (i === lastUserIdx) break;
3067
+ const msg = messages[i];
3068
+ if (msg.role === "user" || msg.role === "developer") {
3069
+ const content = buildCursorRootPromptContent(msg.content);
3070
+ if (content.length === 0) continue;
3071
+ pushJson({ role: "user", content });
3072
+ } else if (msg.role === "assistant") {
3073
+ const text = extractAssistantMessageText(msg);
3074
+ if (!text) continue;
3075
+ pushJson({ role: "assistant", content: [{ type: "text", text }] });
3076
+ } else if (msg.role === "toolResult") {
3077
+ const text = toolResultToText(msg);
3078
+ if (!text) continue;
3079
+ pushJson({
3080
+ role: "user",
3081
+ content: [{ type: "text", text: `[Tool Result]\n${text}` }],
3082
+ });
3083
+ }
3084
+ }
3085
+
3086
+ return entries;
3087
+ }
3088
+
3089
+ /**
3090
+ * Convert context.messages to Cursor's ConversationTurnStructure blob IDs.
3091
+ * Groups messages into turns: each turn is a user message followed by the assistant's response.
3092
+ * Excludes the last user message (which goes in the action).
3093
+ *
3094
+ * Each `AgentConversationTurnStructure.user_message`, `steps[]`, and the outer
3095
+ * `ConversationStateStructure.turns[]` entry is a blob ID into `blobStore`.
3096
+ */
3097
+ function buildConversationTurns(messages: Message[], blobStore: Map<string, Uint8Array>): Uint8Array[] {
3098
+ const turns: Uint8Array[] = [];
3099
+
3100
+ // Find turn boundaries - each turn starts with a user message
3101
+ let i = 0;
3102
+ while (i < messages.length) {
3103
+ const msg = messages[i];
3104
+
3105
+ // Skip non-user messages at the start
3106
+ if (msg.role !== "user" && msg.role !== "developer") {
3107
+ i++;
3108
+ continue;
3109
+ }
3110
+
3111
+ // Check if this is the last user message (which goes in the action, not turns)
3112
+ let isLastUserMessage = true;
3113
+ for (let j = i + 1; j < messages.length; j++) {
3114
+ if (messages[j].role === "user" || messages[j].role === "developer") {
3115
+ isLastUserMessage = false;
3116
+ break;
3117
+ }
3118
+ }
3119
+ if (isLastUserMessage) {
3120
+ break;
3121
+ }
3122
+
3123
+ // Create and serialize user message
3124
+ const userText = extractUserMessageText(msg);
3125
+ if (userText.length === 0 && !hasUserMessageImages(msg)) {
3126
+ i++;
3127
+ continue;
3128
+ }
3129
+
3130
+ const userMessage = createCursorUserMessage(
3131
+ msg.content,
3132
+ userText,
3133
+ deterministicMessageId(`u:${turns.length}:${cursorUserContentKey(msg.content)}`),
3134
+ );
3135
+ const userMessageBytes = toBinary(UserMessageSchema, userMessage);
3136
+ const userMessageBlobId = storeCursorBlob(blobStore, userMessageBytes);
3137
+
3138
+ // Collect and serialize steps until next user message
3139
+ const stepBlobIds: Uint8Array[] = [];
3140
+ i++;
3141
+
3142
+ while (i < messages.length && messages[i].role !== "user" && messages[i].role !== "developer") {
3143
+ const stepMsg = messages[i];
3144
+
3145
+ if (stepMsg.role === "assistant") {
3146
+ const text = extractAssistantMessageText(stepMsg);
3147
+ if (text) {
3148
+ const step = create(ConversationStepSchema, {
3149
+ message: {
3150
+ case: "assistantMessage",
3151
+ value: create(AssistantMessageSchema, { text }),
3152
+ },
3153
+ });
3154
+ stepBlobIds.push(storeCursorBlob(blobStore, toBinary(ConversationStepSchema, step)));
3155
+ }
3156
+ } else if (stepMsg.role === "toolResult") {
3157
+ // Include tool results as assistant text for context
3158
+ const text = toolResultToText(stepMsg);
3159
+ if (text) {
3160
+ const step = create(ConversationStepSchema, {
3161
+ message: {
3162
+ case: "assistantMessage",
3163
+ value: create(AssistantMessageSchema, { text: `[Tool Result]\n${text}` }),
3164
+ },
3165
+ });
3166
+ stepBlobIds.push(storeCursorBlob(blobStore, toBinary(ConversationStepSchema, step)));
3167
+ }
3168
+ }
3169
+
3170
+ i++;
3171
+ }
3172
+
3173
+ // Create the serialized turn using Structure types. The bytes fields
3174
+ // (user_message, steps) are blob IDs resolved through the KV store.
3175
+ const agentTurn = create(AgentConversationTurnStructureSchema, {
3176
+ userMessage: userMessageBlobId,
3177
+ steps: stepBlobIds,
3178
+ });
3179
+ const turn = create(ConversationTurnStructureSchema, {
3180
+ turn: {
3181
+ case: "agentConversationTurn",
3182
+ value: agentTurn,
3183
+ },
3184
+ });
3185
+ turns.push(storeCursorBlob(blobStore, toBinary(ConversationTurnStructureSchema, turn)));
3186
+ }
3187
+
3188
+ return turns;
3189
+ }
3190
+
3191
+ /** Exported for tests: decodes Cursor history blobs built from conversation messages. */
3192
+ export function buildCursorHistoryForTest(messages: Message[]): {
3193
+ rootPromptMessagesJson: unknown[];
3194
+ turnUserMessagesJson: JsonValue[];
3195
+ } {
3196
+ const blobStore = new Map<string, Uint8Array>();
3197
+ const rootPromptMessagesJson = buildRootPromptMessagesJson(messages, [], blobStore).map(blobId =>
3198
+ JSON.parse(new TextDecoder().decode(readCursorBlob(blobStore, blobId))),
3199
+ );
3200
+ const turnUserMessagesJson: JsonValue[] = [];
3201
+ for (const turnBlobId of buildConversationTurns(messages, blobStore)) {
3202
+ const turn = fromBinary(ConversationTurnStructureSchema, readCursorBlob(blobStore, turnBlobId));
3203
+ if (turn.turn.case !== "agentConversationTurn") {
3204
+ continue;
3205
+ }
3206
+ const userMessage = fromBinary(UserMessageSchema, readCursorBlob(blobStore, turn.turn.value.userMessage));
3207
+ turnUserMessagesJson.push(toJson(UserMessageSchema, userMessage));
3208
+ }
3209
+ return { rootPromptMessagesJson, turnUserMessagesJson };
3210
+ }
3211
+ function createCursorUserMessage(
3212
+ content: string | (TextContent | ImageContent)[],
3213
+ text: string,
3214
+ messageId = crypto.randomUUID(),
3215
+ ) {
3216
+ const images = typeof content === "string" ? [] : extractImages(content);
3217
+ return create(UserMessageSchema, {
3218
+ text,
3219
+ messageId,
3220
+ ...(images.length > 0
3221
+ ? {
3222
+ selectedContext: create(SelectedContextSchema, {
3223
+ selectedImages: images,
3224
+ }),
3225
+ }
3226
+ : {}),
3227
+ });
3228
+ }
3229
+
3230
+ function extractImages(content: (TextContent | ImageContent)[]) {
3231
+ return content
3232
+ .filter((item): item is ImageContent => item.type === "image")
3233
+ .map(image =>
3234
+ create(SelectedImageSchema, {
3235
+ uuid: crypto.randomUUID(),
3236
+ mimeType: image.mimeType,
3237
+ dataOrBlobId: {
3238
+ case: "data",
3239
+ value: Uint8Array.from(Buffer.from(image.data, "base64")),
3240
+ },
3241
+ }),
3242
+ );
3243
+ }
3244
+
3245
+ function buildGrpcRequest(
3246
+ model: Model<"cursor-agent">,
3247
+ context: Context,
3248
+ options: CursorOptions | undefined,
3249
+ state: {
3250
+ conversationId: string;
3251
+ blobStore: Map<string, Uint8Array>;
3252
+ conversationState?: ConversationStateStructure;
3253
+ },
3254
+ ): {
3255
+ requestBytes: Uint8Array;
3256
+ blobStore: Map<string, Uint8Array>;
3257
+ conversationState: ConversationStateStructure;
3258
+ } {
3259
+ const blobStore = state.blobStore;
3260
+
3261
+ const systemPromptIds = buildCursorSystemPromptJsons(context.systemPrompt, model.id).map(json =>
3262
+ storeCursorBlob(blobStore, new TextEncoder().encode(json)),
3263
+ );
3264
+
3265
+ const lastMessage = context.messages[context.messages.length - 1];
3266
+ let userContent: string | (TextContent | ImageContent)[] | undefined;
3267
+ let userText = "";
3268
+ let hasUserImages = false;
3269
+ if (lastMessage?.role === "user" || lastMessage?.role === "developer") {
3270
+ userContent = lastMessage.content;
3271
+ if (typeof userContent === "string") {
3272
+ userText = userContent.trim();
3273
+ } else {
3274
+ userText = extractText(userContent);
3275
+ hasUserImages = hasImages(userContent);
3276
+ }
3277
+ }
3278
+
3279
+ const action = create(ConversationActionSchema, {
3280
+ action:
3281
+ userContent && (userText.trim().length > 0 || hasUserImages)
3282
+ ? {
3283
+ case: "userMessageAction",
3284
+ value: create(UserMessageActionSchema, {
3285
+ userMessage: createCursorUserMessage(userContent, userText),
3286
+ }),
3287
+ }
3288
+ : {
3289
+ case: "resumeAction",
3290
+ value: create(ResumeActionSchema, {}),
3291
+ },
3292
+ });
3293
+
3294
+ // Build conversation turns from prior messages (excluding the last user message).
3295
+ // This populates the UI-side history view (`turns[]`).
3296
+ const turns = buildConversationTurns(context.messages, blobStore);
3297
+
3298
+ // Build `rootPromptMessagesJson` from prior messages. Cursor's server uses this
3299
+ // field (not `turns[]`) to construct the actual model prompt; if we only send the
3300
+ // system prompt here, multi-turn conversations lose prior context and the model
3301
+ // sees only the current user message.
3302
+ const rootPromptMessagesJson = buildRootPromptMessagesJson(context.messages, systemPromptIds, blobStore);
3303
+
3304
+ // Preserve cached non-history state fields (todos, file states, summaries, etc.)
3305
+ // when the system prompt is unchanged; otherwise start fresh.
3306
+ const cachedPromptHead = state.conversationState?.rootPromptMessagesJson?.slice(0, systemPromptIds.length) ?? [];
3307
+ const hasMatchingPrompt =
3308
+ cachedPromptHead.length === systemPromptIds.length &&
3309
+ systemPromptIds.every((id, idx) => Buffer.from(cachedPromptHead[idx]).equals(id));
3310
+ const baseState =
3311
+ state.conversationState && hasMatchingPrompt
3312
+ ? state.conversationState
3313
+ : create(ConversationStateStructureSchema, {
3314
+ rootPromptMessagesJson: systemPromptIds,
3315
+ turns: [],
3316
+ todos: [],
3317
+ pendingToolCalls: [],
3318
+ previousWorkspaceUris: [],
3319
+ fileStates: {},
3320
+ fileStatesV2: {},
3321
+ summaryArchives: [],
3322
+ turnTimings: [],
3323
+ subagentStates: {},
3324
+ selfSummaryCount: 0,
3325
+ readPaths: [],
3326
+ });
3327
+
3328
+ // Always override `rootPromptMessagesJson` and `turns` with content freshly built from
3329
+ // `context.messages`. The server-echoed checkpoint replaces historical user entries
3330
+ // with empty placeholders, so we cannot rely on the cached `rootPromptMessagesJson`.
3331
+ const conversationState = create(ConversationStateStructureSchema, {
3332
+ ...baseState,
3333
+ rootPromptMessagesJson,
3334
+ turns,
3335
+ });
3336
+
3337
+ const resolvedModel = resolveCursorWireModelForTest(model);
3338
+ const modelDetails = create(ModelDetailsSchema, {
3339
+ modelId: resolvedModel.modelId,
3340
+ displayModelId: model.id,
3341
+ displayName: model.name,
3342
+ });
3343
+
3344
+ const runRequest = create(AgentRunRequestSchema, {
3345
+ conversationState,
3346
+ action,
3347
+ modelDetails,
3348
+ conversationId: state.conversationId,
3349
+ ...(resolvedModel.translated
3350
+ ? {
3351
+ requestedModel: create(RequestedModelSchema, {
3352
+ modelId: resolvedModel.modelId,
3353
+ parameters: resolvedModel.parameters,
3354
+ }),
3355
+ }
3356
+ : {}),
3357
+ });
3358
+
3359
+ options?.onPayload?.(runRequest, model, options?.attemptScope);
3360
+
3361
+ // Tools are sent later via requestContext (exec handshake)
3362
+
3363
+ if (options?.customSystemPrompt) {
3364
+ runRequest.customSystemPrompt = options.customSystemPrompt;
3365
+ }
3366
+
3367
+ const clientMessage = create(AgentClientMessageSchema, {
3368
+ message: { case: "runRequest", value: runRequest },
3369
+ });
3370
+
3371
+ const requestBytes = toBinary(AgentClientMessageSchema, clientMessage);
3372
+
3373
+ const toolNames = context.tools?.map(tool => tool.name) ?? [];
3374
+ const detail =
3375
+ $env.DEBUG_CURSOR === "2"
3376
+ ? ` ${JSON.stringify(clientMessage.message.value, debugReplacer, 2)?.slice(0, 2000)}`
3377
+ : "";
3378
+ log("info", "builtRunRequest", {
3379
+ bytes: requestBytes.length,
3380
+ tools: toolNames.length,
3381
+ toolNames: toolNames.slice(0, 20),
3382
+ detail: detail || undefined,
3383
+ });
3384
+
3385
+ return { requestBytes, blobStore, conversationState };
3386
+ }
3387
+
3388
+ function hasImages(content: (TextContent | ImageContent)[]): boolean {
3389
+ return content.some(item => item.type === "image");
3390
+ }
3391
+ function extractText(content: (TextContent | ImageContent)[]): string {
3392
+ return content
3393
+ .filter((c): c is TextContent => c.type === "text")
3394
+ .map(c => c.text)
3395
+ .join("\n");
3396
+ }