@code-yeongyu/senpi-ai 2026.8.18 → 2026.8.20

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 (153) hide show
  1. package/README.md +5 -3
  2. package/dist/api/anthropic-messages.d.ts +7 -1
  3. package/dist/api/anthropic-messages.d.ts.map +1 -1
  4. package/dist/api/anthropic-messages.js +21 -4
  5. package/dist/api/anthropic-messages.js.map +1 -1
  6. package/dist/api/azure-openai-responses.d.ts +2 -0
  7. package/dist/api/azure-openai-responses.d.ts.map +1 -1
  8. package/dist/api/azure-openai-responses.js +7 -1
  9. package/dist/api/azure-openai-responses.js.map +1 -1
  10. package/dist/api/bedrock-converse-stream.d.ts.map +1 -1
  11. package/dist/api/bedrock-converse-stream.js +39 -2
  12. package/dist/api/bedrock-converse-stream.js.map +1 -1
  13. package/dist/api/cursor-agent/gen/agent_pb.d.ts +22 -1
  14. package/dist/api/cursor-agent/gen/agent_pb.d.ts.map +1 -1
  15. package/dist/api/cursor-agent/gen/agent_pb.js +1 -1
  16. package/dist/api/cursor-agent/gen/agent_pb.js.map +1 -1
  17. package/dist/api/cursor-agent/reasoning-params.d.ts +20 -0
  18. package/dist/api/cursor-agent/reasoning-params.d.ts.map +1 -0
  19. package/dist/api/cursor-agent/reasoning-params.js +26 -0
  20. package/dist/api/cursor-agent/reasoning-params.js.map +1 -0
  21. package/dist/api/cursor-agent/types.d.ts +7 -1
  22. package/dist/api/cursor-agent/types.d.ts.map +1 -1
  23. package/dist/api/cursor-agent/types.js.map +1 -1
  24. package/dist/api/cursor-agent.d.ts +7 -3
  25. package/dist/api/cursor-agent.d.ts.map +1 -1
  26. package/dist/api/cursor-agent.js +363 -218
  27. package/dist/api/cursor-agent.js.map +1 -1
  28. package/dist/api/cursor-conversation-rotation.d.ts +37 -0
  29. package/dist/api/cursor-conversation-rotation.d.ts.map +1 -0
  30. package/dist/api/cursor-conversation-rotation.js +110 -0
  31. package/dist/api/cursor-conversation-rotation.js.map +1 -0
  32. package/dist/api/cursor-task-args.d.ts +3 -0
  33. package/dist/api/cursor-task-args.d.ts.map +1 -0
  34. package/dist/api/cursor-task-args.js +17 -0
  35. package/dist/api/cursor-task-args.js.map +1 -0
  36. package/dist/api/google-generative-ai.d.ts +2 -2
  37. package/dist/api/google-generative-ai.d.ts.map +1 -1
  38. package/dist/api/google-generative-ai.js +14 -11
  39. package/dist/api/google-generative-ai.js.map +1 -1
  40. package/dist/api/google-shared.d.ts +5 -2
  41. package/dist/api/google-shared.d.ts.map +1 -1
  42. package/dist/api/google-shared.js +16 -0
  43. package/dist/api/google-shared.js.map +1 -1
  44. package/dist/api/google-vertex.d.ts +2 -2
  45. package/dist/api/google-vertex.d.ts.map +1 -1
  46. package/dist/api/google-vertex.js +13 -10
  47. package/dist/api/google-vertex.js.map +1 -1
  48. package/dist/api/mistral-conversations.d.ts.map +1 -1
  49. package/dist/api/mistral-conversations.js +4 -1
  50. package/dist/api/mistral-conversations.js.map +1 -1
  51. package/dist/api/openai-codex-responses.d.ts.map +1 -1
  52. package/dist/api/openai-codex-responses.js +4 -1
  53. package/dist/api/openai-codex-responses.js.map +1 -1
  54. package/dist/api/openai-completions.d.ts +1 -1
  55. package/dist/api/openai-completions.d.ts.map +1 -1
  56. package/dist/api/openai-completions.js +45 -29
  57. package/dist/api/openai-completions.js.map +1 -1
  58. package/dist/api/openai-responses.d.ts.map +1 -1
  59. package/dist/api/openai-responses.js +8 -1
  60. package/dist/api/openai-responses.js.map +1 -1
  61. package/dist/api/pi-messages.js +1 -1
  62. package/dist/api/pi-messages.js.map +1 -1
  63. package/dist/api/simple-options.d.ts +4 -0
  64. package/dist/api/simple-options.d.ts.map +1 -1
  65. package/dist/api/simple-options.js +18 -9
  66. package/dist/api/simple-options.js.map +1 -1
  67. package/dist/auth/oauth/device-code.d.ts +1 -0
  68. package/dist/auth/oauth/device-code.d.ts.map +1 -1
  69. package/dist/auth/oauth/device-code.js +1 -1
  70. package/dist/auth/oauth/device-code.js.map +1 -1
  71. package/dist/auth/oauth/github-copilot.d.ts.map +1 -1
  72. package/dist/auth/oauth/github-copilot.js +22 -9
  73. package/dist/auth/oauth/github-copilot.js.map +1 -1
  74. package/dist/cursor/catalog-grouping.d.ts +25 -0
  75. package/dist/cursor/catalog-grouping.d.ts.map +1 -0
  76. package/dist/cursor/catalog-grouping.js +134 -0
  77. package/dist/cursor/catalog-grouping.js.map +1 -0
  78. package/dist/cursor/cursor-variant-aliases.json +1893 -0
  79. package/dist/cursor/model-capabilities.d.ts +47 -0
  80. package/dist/cursor/model-capabilities.d.ts.map +1 -0
  81. package/dist/cursor/model-capabilities.js +208 -0
  82. package/dist/cursor/model-capabilities.js.map +1 -0
  83. package/dist/cursor/selection-descriptor.d.ts +21 -0
  84. package/dist/cursor/selection-descriptor.d.ts.map +1 -0
  85. package/dist/cursor/selection-descriptor.js +99 -0
  86. package/dist/cursor/selection-descriptor.js.map +1 -0
  87. package/dist/cursor/store-migration.d.ts +8 -0
  88. package/dist/cursor/store-migration.d.ts.map +1 -0
  89. package/dist/cursor/store-migration.js +76 -0
  90. package/dist/cursor/store-migration.js.map +1 -0
  91. package/dist/index.d.ts +5 -1
  92. package/dist/index.d.ts.map +1 -1
  93. package/dist/index.js +4 -0
  94. package/dist/index.js.map +1 -1
  95. package/dist/model.d.ts +13 -0
  96. package/dist/model.d.ts.map +1 -1
  97. package/dist/model.js.map +1 -1
  98. package/dist/models.d.ts +6 -0
  99. package/dist/models.d.ts.map +1 -1
  100. package/dist/models.js +9 -1
  101. package/dist/models.js.map +1 -1
  102. package/dist/providers/cursor.d.ts.map +1 -1
  103. package/dist/providers/cursor.js +40 -11
  104. package/dist/providers/cursor.js.map +1 -1
  105. package/dist/providers/data/.manifest.json +1 -1
  106. package/dist/providers/data/amazon-bedrock.json +1 -1
  107. package/dist/providers/data/anthropic.json +1 -1
  108. package/dist/providers/data/baseten.json +1 -1
  109. package/dist/providers/data/github-copilot.json +1 -1
  110. package/dist/providers/data/huggingface.json +1 -1
  111. package/dist/providers/data/openai-codex.json +1 -1
  112. package/dist/providers/data/openai.json +1 -1
  113. package/dist/providers/data/opencode-go.json +1 -1
  114. package/dist/providers/data/opencode.json +1 -1
  115. package/dist/providers/data/openrouter.json +1 -1
  116. package/dist/providers/data/qwen-token-plan-individual.json +1 -1
  117. package/dist/providers/data/vercel-ai-gateway.json +1 -1
  118. package/dist/providers/data/xai.json +1 -1
  119. package/dist/providers/data/xiaomi-token-plan-ams.json +1 -1
  120. package/dist/providers/data/xiaomi-token-plan-cn.json +1 -1
  121. package/dist/providers/data/xiaomi-token-plan-sgp.json +1 -1
  122. package/dist/providers/data/xiaomi.json +1 -1
  123. package/dist/providers/data/zai-coding-cn.json +1 -1
  124. package/dist/providers/data/zai.json +1 -1
  125. package/dist/providers/xai.d.ts +1 -1
  126. package/dist/providers/xai.d.ts.map +1 -1
  127. package/dist/providers/xai.js +1 -5
  128. package/dist/providers/xai.js.map +1 -1
  129. package/dist/tool-call-middleware/index.d.ts.map +1 -1
  130. package/dist/tool-call-middleware/index.js +2 -0
  131. package/dist/tool-call-middleware/index.js.map +1 -1
  132. package/dist/tool-call-middleware/protocols/anthropic-xml/tool-resolver.d.ts.map +1 -1
  133. package/dist/tool-call-middleware/protocols/anthropic-xml/tool-resolver.js +21 -1
  134. package/dist/tool-call-middleware/protocols/anthropic-xml/tool-resolver.js.map +1 -1
  135. package/dist/tool-call-middleware/recovery-message-snapshot.d.ts.map +1 -1
  136. package/dist/tool-call-middleware/recovery-message-snapshot.js +2 -1
  137. package/dist/tool-call-middleware/recovery-message-snapshot.js.map +1 -1
  138. package/dist/types.d.ts +45 -4
  139. package/dist/types.d.ts.map +1 -1
  140. package/dist/types.js.map +1 -1
  141. package/dist/utils/overflow.d.ts +30 -0
  142. package/dist/utils/overflow.d.ts.map +1 -1
  143. package/dist/utils/overflow.js +48 -0
  144. package/dist/utils/overflow.js.map +1 -1
  145. package/dist/utils/pi-user-agent.d.ts +2 -0
  146. package/dist/utils/pi-user-agent.d.ts.map +1 -1
  147. package/dist/utils/pi-user-agent.js +7 -0
  148. package/dist/utils/pi-user-agent.js.map +1 -1
  149. package/dist/utils/prompt-cache-ttl.d.ts +2 -1
  150. package/dist/utils/prompt-cache-ttl.d.ts.map +1 -1
  151. package/dist/utils/prompt-cache-ttl.js +2 -0
  152. package/dist/utils/prompt-cache-ttl.js.map +1 -1
  153. package/package.json +2 -3
@@ -28,8 +28,11 @@ import { sanitizeSurrogates } from "../utils/sanitize-unicode.js";
28
28
  import { deterministicUuid } from "./cursor-agent/deterministic-id.js";
29
29
  import { armExecHeartbeat } from "./cursor-agent/exec-lifecycle.js";
30
30
  import { buildMcpStateResult, buildNeutralHookResult, buildPiBashError, buildPiBashResult, buildPiEditError, buildPiEditRejected, buildPiEditResult, buildPiFindError, buildPiFindResult, buildPiGrepError, buildPiGrepResult, buildPiLsError, buildPiLsResult, buildPiReadError, buildPiReadResult, buildPiWriteError, buildPiWriteRejected, buildPiWriteResult, } from "./cursor-agent/exec-modern.js";
31
- import { AgentClientMessageSchema, AgentConversationTurnStructureSchema, AgentRunRequestSchema, AgentServerMessageSchema, AgentStoreConflictErrorSchema, AgentStoreConflictResultSchema, AssistantMessageSchema, BackgroundShellSpawnResultSchema, CanvasDiagnosticsErrorSchema, CanvasDiagnosticsResultSchema, ClientHeartbeatSchema, ComputerUseErrorSchema, ComputerUseResultSchema, ConversationActionSchema, ConversationSearchErrorSchema, ConversationSearchResultSchema, ConversationStateStructureSchema, ConversationStepSchema, ConversationTurnStructureSchema, DeleteErrorSchema, DeleteRejectedSchema, DeleteResultSchema, DeleteSuccessSchema, DiagnosticsErrorSchema, DiagnosticsRejectedSchema, DiagnosticsResultSchema, DiagnosticsSuccessSchema, ExecClientControlMessageSchema, ExecClientHeartbeatSchema, ExecClientMessageSchema, ExecClientStreamCloseSchema, ExecClientThrowSchema, FetchErrorSchema, FetchResultSchema, ForceBackgroundShellResultSchema, ForceBackgroundShellStatus, ForceBackgroundSubagentResultSchema, ForceBackgroundSubagentStatus, GetBlobResultSchema, GetUsableModelsRequestSchema, GetUsableModelsResponseSchema, GrepContentMatchSchema, GrepContentResultSchema, GrepCountResultSchema, GrepErrorSchema, GrepFileCountSchema, GrepFileMatchSchema, GrepFilesResultSchema, GrepResultSchema, GrepSuccessSchema, GrepUnionResultSchema, KvClientMessageSchema, ListMcpResourcesExecResultSchema, ListMcpResourcesSuccessSchema, LsDirectoryTreeNode_FileSchema, LsDirectoryTreeNodeSchema, LsErrorSchema, LsRejectedSchema, LsResultSchema, LsSuccessSchema, McpAllowlistPrecheckResultSchema, McpApprovedSchema, McpArgsSchema, McpErrorSchema, McpImageContentSchema, McpRejectedSchema, McpResultSchema, McpSuccessSchema, McpTextContentSchema, McpToolCallSchema, McpToolDefinitionSchema, McpToolErrorSchema, McpToolNotFoundSchema, McpToolResultContentItemSchema, McpToolResultSchema, ModelDetailsSchema, ReadErrorSchema, ReadMcpResourceExecResultSchema, ReadMcpResourceNotFoundSchema, ReadRejectedSchema, ReadResultSchema, ReadSuccessSchema, RecordScreenFailureSchema, RecordScreenResultSchema, RequestContextResultSchema, RequestContextSchema, RequestContextSuccessSchema, RequestedModelSchema, ResumeActionSchema, SelectedContextSchema, SelectedImageSchema, SetBlobResultSchema, ShellAllowlistPrecheckResultSchema, ShellFailureSchema, ShellRejectedSchema, ShellResultSchema, ShellStreamExitSchema, ShellStreamSchema, ShellStreamStartSchema, ShellStreamStderrSchema, ShellStreamStdoutSchema, ShellSuccessSchema, SmartModeClassifierErrorSchema, SmartModeClassifierResultSchema, SubagentAwaitNotFoundSchema, SubagentAwaitResultSchema, SubagentErrorSchema, SubagentResultSchema, ToolCallSchema, UserMessageActionSchema, UserMessageSchema, WebFetchAllowlistPrecheckResultSchema, WriteErrorSchema, WriteRejectedSchema, WriteResultSchema, WriteShellStdinErrorSchema, WriteShellStdinResultSchema, WriteSuccessSchema, } from "./cursor-agent/gen/agent_pb.js";
31
+ import { AgentClientMessageSchema, AgentConversationTurnStructureSchema, AgentRunRequestSchema, AgentServerMessageSchema, AgentStoreConflictErrorSchema, AgentStoreConflictResultSchema, AssistantMessageSchema, BackgroundShellSpawnResultSchema, CanvasDiagnosticsErrorSchema, CanvasDiagnosticsResultSchema, ClientHeartbeatSchema, ComputerUseErrorSchema, ComputerUseResultSchema, ConversationActionSchema, ConversationSearchErrorSchema, ConversationSearchResultSchema, ConversationStateStructureSchema, ConversationStepSchema, ConversationTurnStructureSchema, DeleteErrorSchema, DeleteRejectedSchema, DeleteResultSchema, DeleteSuccessSchema, DiagnosticsErrorSchema, DiagnosticsRejectedSchema, DiagnosticsResultSchema, DiagnosticsSuccessSchema, ExecClientControlMessageSchema, ExecClientHeartbeatSchema, ExecClientMessageSchema, ExecClientStreamCloseSchema, ExecClientThrowSchema, FetchErrorSchema, FetchResultSchema, ForceBackgroundShellResultSchema, ForceBackgroundShellStatus, ForceBackgroundSubagentResultSchema, ForceBackgroundSubagentStatus, GetBlobResultSchema, GetUsableModelsRequestSchema, GetUsableModelsResponseSchema, GrepContentMatchSchema, GrepContentResultSchema, GrepCountResultSchema, GrepErrorSchema, GrepFileCountSchema, GrepFileMatchSchema, GrepFilesResultSchema, GrepResultSchema, GrepSuccessSchema, GrepUnionResultSchema, KvClientMessageSchema, ListMcpResourcesExecResultSchema, ListMcpResourcesSuccessSchema, LsDirectoryTreeNode_FileSchema, LsDirectoryTreeNodeSchema, LsErrorSchema, LsRejectedSchema, LsResultSchema, LsSuccessSchema, McpAllowlistPrecheckResultSchema, McpApprovedSchema, McpArgsSchema, McpErrorSchema, McpImageContentSchema, McpRejectedSchema, McpResultSchema, McpSuccessSchema, McpTextContentSchema, McpToolCallSchema, McpToolDefinitionSchema, McpToolErrorSchema, McpToolNotFoundSchema, McpToolResultContentItemSchema, McpToolResultSchema, ModelDetailsSchema, ReadErrorSchema, ReadMcpResourceExecResultSchema, ReadMcpResourceNotFoundSchema, ReadRejectedSchema, ReadResultSchema, ReadSuccessSchema, RecordScreenFailureSchema, RecordScreenResultSchema, RequestContextResultSchema, RequestContextSchema, RequestContextSuccessSchema, ResumeActionSchema, SelectedContextSchema, SelectedImageSchema, SetBlobResultSchema, ShellAllowlistPrecheckResultSchema, ShellFailureSchema, ShellRejectedSchema, ShellResultSchema, ShellStreamExitSchema, ShellStreamSchema, ShellStreamStartSchema, ShellStreamStderrSchema, ShellStreamStdoutSchema, ShellSuccessSchema, SmartModeClassifierErrorSchema, SmartModeClassifierResultSchema, SubagentAwaitNotFoundSchema, SubagentAwaitResultSchema, SubagentErrorSchema, SubagentResultSchema, ToolCallSchema, UserMessageActionSchema, UserMessageSchema, WebFetchAllowlistPrecheckResultSchema, WriteErrorSchema, WriteRejectedSchema, WriteResultSchema, WriteShellStdinErrorSchema, WriteShellStdinResultSchema, WriteSuccessSchema, } from "./cursor-agent/gen/agent_pb.js";
32
32
  import { composeShellCommand, omitUndefinedArgs, piLimit, piLsPath, piReadArgs, piTimeout, } from "./cursor-agent/pi-args.js";
33
+ import { buildRequestedModel } from "./cursor-agent/reasoning-params.js";
34
+ import { CURSOR_CONVERSATION_POISONED_MESSAGE, createConversationRotationStore, isZeroTokenResourceExhausted, resolveConversationRotationPersistPath, } from "./cursor-conversation-rotation.js";
35
+ import { keepUsableCursorTaskArgs } from "./cursor-task-args.js";
33
36
  export const CURSOR_API_URL = "https://api2.cursor.sh";
34
37
  export const CURSOR_CLIENT_VERSION = "cli-2026.07.23-e383d2b";
35
38
  const EXEC_HEARTBEAT_INTERVAL_MS = 3000;
@@ -90,7 +93,6 @@ export function sanitizeCursorCallerHeaders(headers) {
90
93
  const NOT_IMPLEMENTED_SUFFIX = "not implemented by this client";
91
94
  const NOT_IMPLEMENTED = "Not implemented by this client";
92
95
  /** Bare gRPC `resource_exhausted` end-streams (also inside a Connect error message). */
93
- const RESOURCE_EXHAUSTED_PATTERN = /resource.?exhausted/i;
94
96
  const conversationStateCache = new Map();
95
97
  const conversationBlobStores = new Map();
96
98
  /**
@@ -100,7 +102,18 @@ const conversationBlobStores = new Map();
100
102
  * and the cached state migrates, so the retry loop's next attempt starts a
101
103
  * fresh conversation. Keyed by the base id so a failed rotation never repeats.
102
104
  */
103
- const rotatedConversationIds = new Map();
105
+ let conversationRotationStore = createConversationRotationStore({
106
+ persistPath: resolveConversationRotationPersistPath(),
107
+ });
108
+ let conversationRotationPersistPath = resolveConversationRotationPersistPath();
109
+ function rotationStore() {
110
+ const persistPath = resolveConversationRotationPersistPath();
111
+ if (persistPath !== conversationRotationPersistPath) {
112
+ conversationRotationPersistPath = persistPath;
113
+ conversationRotationStore = createConversationRotationStore({ persistPath });
114
+ }
115
+ return conversationRotationStore;
116
+ }
104
117
  const CONNECT_END_STREAM_FLAG = 0b00000010;
105
118
  function log(type, subtype, data) {
106
119
  if (!process.env.DEBUG_CURSOR)
@@ -216,10 +229,6 @@ export const stream = (model, context, options) => {
216
229
  let openBlockState;
217
230
  let resolveH2 = () => { };
218
231
  let rejectH2 = () => { };
219
- const h2Completion = new Promise((resolve, reject) => {
220
- resolveH2 = resolve;
221
- rejectH2 = reject;
222
- });
223
232
  const settleH2 = (error) => {
224
233
  if (h2Settled)
225
234
  return;
@@ -243,225 +252,271 @@ export const stream = (model, context, options) => {
243
252
  let baseConversationId;
244
253
  let conversationId;
245
254
  let usageState;
246
- try {
247
- const apiKey = options?.apiKey;
248
- if (!apiKey) {
249
- throw new Error("Cursor access token is required; run /login cursor");
250
- }
251
- baseConversationId = options?.conversationId ?? options?.sessionId ?? randomUUID();
252
- conversationId = rotatedConversationIds.get(baseConversationId) ?? baseConversationId;
253
- const blobStore = conversationBlobStores.get(conversationId) ?? new Map();
254
- conversationBlobStores.set(conversationId, blobStore);
255
- const cachedState = conversationStateCache.get(conversationId);
256
- const { requestBytes, conversationState } = await buildGrpcRequest(model, context, options, {
257
- conversationId,
258
- blobStore,
259
- conversationState: cachedState,
255
+ let retryPoisonedConversation = false;
256
+ let attempt = 0;
257
+ do {
258
+ retryPoisonedConversation = false;
259
+ attempt += 1;
260
+ h2Settled = false;
261
+ sawTurnEnded = false;
262
+ endStreamError = null;
263
+ openBlockState = undefined;
264
+ const h2Completion = new Promise((resolve, reject) => {
265
+ resolveH2 = resolve;
266
+ rejectH2 = reject;
260
267
  });
261
- conversationStateCache.set(conversationId, conversationState);
262
- const requestContextTools = buildMcpToolDefinitions(context.tools);
263
- const baseUrl = model.baseUrl || CURSOR_API_URL;
264
- const requestPath = "/agent.v1.AgentService/Run";
265
- // Caller headers are additive, and are spread FIRST so the protocol
266
- // framing, auth, and request id below always win.
267
- const callerHeaders = sanitizeCursorCallerHeaders(providerHeadersToRecord(options?.headers));
268
- const requestHeaders = {
269
- ...callerHeaders,
270
- ":method": "POST",
271
- ":path": requestPath,
272
- "content-type": "application/connect+proto",
273
- "connect-protocol-version": "1",
274
- te: "trailers",
275
- authorization: `Bearer ${apiKey}`,
276
- "x-ghost-mode": "true",
277
- "x-cursor-client-version": CURSOR_CLIENT_VERSION,
278
- "x-cursor-client-type": "cli",
279
- "x-request-id": randomUUID(),
280
- };
281
- h2Client = http2.connect(baseUrl);
282
- h2Client.on("error", (error) => settleH2(mapH2TransportError(error, baseUrl)));
283
- h2Request = h2Client.request(requestHeaders);
284
- stream.push({ type: "start", partial: output });
285
- let pendingBuffer = Buffer.alloc(0);
286
- let currentTextBlock = null;
287
- let currentThinkingBlock = null;
288
- let currentToolCall = null;
289
- const resolvedMcpToolCallIds = new Set();
290
- usageState = { sawTokenDelta: false };
291
- const state = {
292
- get currentTextBlock() {
293
- return currentTextBlock;
294
- },
295
- get currentThinkingBlock() {
296
- return currentThinkingBlock;
297
- },
298
- get currentToolCall() {
299
- return currentToolCall;
300
- },
301
- openToolCalls: new Map(),
302
- resolvedMcpToolCallIds,
303
- setTextBlock: (b) => {
304
- currentTextBlock = b;
305
- },
306
- setThinkingBlock: (b) => {
307
- currentThinkingBlock = b;
308
- },
309
- setToolCall: (t) => {
310
- currentToolCall = t;
311
- },
312
- onToolResult: options?.onToolResult ?? options?.execHandlers?.onToolResult,
313
- };
314
- openBlockState = state;
315
- const onConversationCheckpoint = (checkpoint) => {
316
- conversationStateCache.set(conversationId, checkpoint);
317
- };
318
- h2Request.on("data", (chunk) => {
319
- // Steady state drains fully per chunk; alias the fresh h2 chunk
320
- // instead of copying it through Buffer.concat.
321
- pendingBuffer = pendingBuffer.length === 0 ? chunk : Buffer.concat([pendingBuffer, chunk]);
322
- while (pendingBuffer.length >= 5) {
323
- const flags = pendingBuffer[0];
324
- const msgLen = pendingBuffer.readUInt32BE(1);
325
- if (pendingBuffer.length < 5 + msgLen)
326
- break;
327
- const messageBytes = pendingBuffer.subarray(5, 5 + msgLen);
328
- pendingBuffer = pendingBuffer.subarray(5 + msgLen);
329
- if (flags & CONNECT_END_STREAM_FLAG) {
330
- const endError = parseConnectEndStream(messageBytes);
331
- if (endError) {
332
- endStreamError = endError;
333
- h2Request?.close();
268
+ try {
269
+ const apiKey = options?.apiKey;
270
+ if (!apiKey) {
271
+ throw new Error("Cursor access token is required; run /login cursor");
272
+ }
273
+ baseConversationId = options?.conversationId ?? options?.sessionId ?? randomUUID();
274
+ conversationId = rotationStore().getWireId(baseConversationId);
275
+ const blobStore = conversationBlobStores.get(conversationId) ?? new Map();
276
+ conversationBlobStores.set(conversationId, blobStore);
277
+ const cachedState = conversationStateCache.get(conversationId);
278
+ const { requestBytes, conversationState } = await buildGrpcRequest(model, context, options, {
279
+ conversationId,
280
+ blobStore,
281
+ conversationState: cachedState,
282
+ });
283
+ conversationStateCache.set(conversationId, conversationState);
284
+ const requestContextTools = buildMcpToolDefinitions(context.tools);
285
+ const baseUrl = model.baseUrl || CURSOR_API_URL;
286
+ const requestPath = "/agent.v1.AgentService/Run";
287
+ // Caller headers are additive, and are spread FIRST so the protocol
288
+ // framing, auth, and request id below always win.
289
+ const callerHeaders = sanitizeCursorCallerHeaders(providerHeadersToRecord(options?.headers));
290
+ const requestHeaders = {
291
+ ...callerHeaders,
292
+ ":method": "POST",
293
+ ":path": requestPath,
294
+ "content-type": "application/connect+proto",
295
+ "connect-protocol-version": "1",
296
+ te: "trailers",
297
+ authorization: `Bearer ${apiKey}`,
298
+ "x-ghost-mode": "true",
299
+ "x-cursor-client-version": CURSOR_CLIENT_VERSION,
300
+ "x-cursor-client-type": "cli",
301
+ "x-request-id": randomUUID(),
302
+ };
303
+ h2Client = http2.connect(baseUrl);
304
+ h2Client.on("error", (error) => settleH2(mapH2TransportError(error, baseUrl)));
305
+ h2Request = h2Client.request(requestHeaders);
306
+ if (attempt === 1) {
307
+ stream.push({ type: "start", partial: output });
308
+ }
309
+ let pendingBuffer = Buffer.alloc(0);
310
+ let currentTextBlock = null;
311
+ let currentThinkingBlock = null;
312
+ let currentToolCall = null;
313
+ const resolvedMcpToolCallIds = new Set();
314
+ usageState = { sawTokenDelta: false, sawTurnEndedUsage: false };
315
+ const state = {
316
+ get currentTextBlock() {
317
+ return currentTextBlock;
318
+ },
319
+ get currentThinkingBlock() {
320
+ return currentThinkingBlock;
321
+ },
322
+ get currentToolCall() {
323
+ return currentToolCall;
324
+ },
325
+ openToolCalls: new Map(),
326
+ resolvedMcpToolCallIds,
327
+ setTextBlock: (b) => {
328
+ currentTextBlock = b;
329
+ },
330
+ setThinkingBlock: (b) => {
331
+ currentThinkingBlock = b;
332
+ },
333
+ setToolCall: (t) => {
334
+ currentToolCall = t;
335
+ },
336
+ onToolResult: options?.onToolResult ?? options?.execHandlers?.onToolResult,
337
+ };
338
+ openBlockState = state;
339
+ const onConversationCheckpoint = (checkpoint) => {
340
+ conversationStateCache.set(conversationId, checkpoint);
341
+ };
342
+ h2Request.on("data", (chunk) => {
343
+ // Steady state drains fully per chunk; alias the fresh h2 chunk
344
+ // instead of copying it through Buffer.concat.
345
+ pendingBuffer = pendingBuffer.length === 0 ? chunk : Buffer.concat([pendingBuffer, chunk]);
346
+ while (pendingBuffer.length >= 5) {
347
+ const flags = pendingBuffer[0];
348
+ const msgLen = pendingBuffer.readUInt32BE(1);
349
+ if (pendingBuffer.length < 5 + msgLen)
350
+ break;
351
+ const messageBytes = pendingBuffer.subarray(5, 5 + msgLen);
352
+ pendingBuffer = pendingBuffer.subarray(5 + msgLen);
353
+ if (flags & CONNECT_END_STREAM_FLAG) {
354
+ const endError = parseConnectEndStream(messageBytes);
355
+ if (endError) {
356
+ endStreamError = endError;
357
+ h2Request?.close();
358
+ }
359
+ continue;
334
360
  }
335
- continue;
336
- }
337
- try {
338
- const serverMessage = fromBinary(AgentServerMessageSchema, messageBytes);
339
- const isTurnEnded = serverMessage.message.case === "interactionUpdate" &&
340
- serverMessage.message.value.message?.case === "turnEnded";
341
- // Dispatch is fire-and-forget so the socket keeps draining
342
- // while a handler runs, but the promise is tracked: `done`
343
- // must not be pushed while an exec handler is still resolving,
344
- // or the buffered tool result is delivered after the turn
345
- // already finalized and the call is left unpaired.
346
- const dispatch = handleServerMessage(serverMessage, output, stream, state, blobStore, h2Request, options?.execHandlers, state.onToolResult, usageState, requestContextTools, onConversationCheckpoint).catch((error) => {
347
- log("error", "handleServerMessage", { error: String(error) });
348
- });
349
- inFlightDispatches.add(dispatch);
350
- void dispatch.finally(() => inFlightDispatches.delete(dispatch));
351
- // Application completion is not protocol success; wait for a
352
- // clean HTTP/2 end.
353
- if (isTurnEnded) {
354
- sawTurnEnded = true;
361
+ try {
362
+ const serverMessage = fromBinary(AgentServerMessageSchema, messageBytes);
363
+ const isTurnEnded = serverMessage.message.case === "interactionUpdate" &&
364
+ serverMessage.message.value.message?.case === "turnEnded";
365
+ // Dispatch is fire-and-forget so the socket keeps draining
366
+ // while a handler runs, but the promise is tracked: `done`
367
+ // must not be pushed while an exec handler is still resolving,
368
+ // or the buffered tool result is delivered after the turn
369
+ // already finalized and the call is left unpaired.
370
+ const dispatch = handleServerMessage(serverMessage, output, stream, state, blobStore, h2Request, options?.execHandlers, state.onToolResult, usageState, requestContextTools, onConversationCheckpoint).catch((error) => {
371
+ log("error", "handleServerMessage", { error: String(error) });
372
+ });
373
+ inFlightDispatches.add(dispatch);
374
+ void dispatch.finally(() => inFlightDispatches.delete(dispatch));
375
+ // Application completion is not protocol success; wait for a
376
+ // clean HTTP/2 end.
377
+ if (isTurnEnded) {
378
+ sawTurnEnded = true;
379
+ }
380
+ }
381
+ catch (e) {
382
+ log("error", "parseServerMessage", { error: String(e) });
355
383
  }
356
384
  }
357
- catch (e) {
358
- log("error", "parseServerMessage", { error: String(e) });
385
+ });
386
+ const sendHeartbeat = () => {
387
+ if (!h2Request || h2Request.closed) {
388
+ return;
389
+ }
390
+ const heartbeatMessage = create(AgentClientMessageSchema, {
391
+ message: { case: "clientHeartbeat", value: create(ClientHeartbeatSchema, {}) },
392
+ });
393
+ const heartbeatBytes = toBinary(AgentClientMessageSchema, heartbeatMessage);
394
+ h2Request.write(frameConnectMessage(heartbeatBytes));
395
+ };
396
+ h2Request.on("trailers", (trailers) => {
397
+ const status = trailers["grpc-status"];
398
+ const msg = trailers["grpc-message"];
399
+ if (status && status !== "0" && !endStreamError) {
400
+ endStreamError = new Error(`gRPC error ${status}: ${decodeURIComponent(String(msg || ""))}`);
359
401
  }
360
- }
361
- });
362
- const sendHeartbeat = () => {
363
- if (!h2Request || h2Request.closed) {
364
- return;
365
- }
366
- const heartbeatMessage = create(AgentClientMessageSchema, {
367
- message: { case: "clientHeartbeat", value: create(ClientHeartbeatSchema, {}) },
368
402
  });
369
- const heartbeatBytes = toBinary(AgentClientMessageSchema, heartbeatMessage);
370
- h2Request.write(frameConnectMessage(heartbeatBytes));
371
- };
372
- h2Request.on("trailers", (trailers) => {
373
- const status = trailers["grpc-status"];
374
- const msg = trailers["grpc-message"];
375
- if (status && status !== "0" && !endStreamError) {
376
- endStreamError = new Error(`gRPC error ${status}: ${decodeURIComponent(String(msg || ""))}`);
403
+ h2Request.on("end", () => {
404
+ settleH2();
405
+ });
406
+ h2Request.on("error", (error) => {
407
+ settleH2(mapH2TransportError(error, baseUrl));
408
+ });
409
+ if (options?.signal) {
410
+ options.signal.addEventListener("abort", () => {
411
+ h2Request?.close();
412
+ settleH2(new Error("Request was aborted"));
413
+ });
377
414
  }
378
- });
379
- h2Request.on("end", () => {
380
- settleH2();
381
- });
382
- h2Request.on("error", (error) => {
383
- settleH2(mapH2TransportError(error, baseUrl));
384
- });
385
- if (options?.signal) {
386
- options.signal.addEventListener("abort", () => {
387
- h2Request?.close();
388
- settleH2(new Error("Request was aborted"));
415
+ h2Request.write(frameConnectMessage(requestBytes));
416
+ heartbeatTimer = setInterval(sendHeartbeat, 5000);
417
+ await h2Completion;
418
+ // The transport is done, but a handler decoded from the last chunk
419
+ // may still be running. Pushing `done` now would let the host drain
420
+ // its buffered tool results before such a handler reserved its entry,
421
+ // leaving the call unpaired and stripped from rebuilt transcripts.
422
+ await drainInFlightDispatches();
423
+ endCurrentTextBlock(output, stream, state);
424
+ endCurrentThinkingBlock(output, stream, state);
425
+ flushOpenToolCalls(output, stream, state);
426
+ calculateCost(model, output.usage);
427
+ stream.push({
428
+ type: "done",
429
+ reason: output.stopReason,
430
+ message: output,
389
431
  });
432
+ stream.end();
390
433
  }
391
- h2Request.write(frameConnectMessage(requestBytes));
392
- heartbeatTimer = setInterval(sendHeartbeat, 5000);
393
- await h2Completion;
394
- // The transport is done, but a handler decoded from the last chunk
395
- // may still be running. Pushing `done` now would let the host drain
396
- // its buffered tool results before such a handler reserved its entry,
397
- // leaving the call unpaired and stripped from rebuilt transcripts.
398
- await drainInFlightDispatches();
399
- endCurrentTextBlock(output, stream, state);
400
- endCurrentThinkingBlock(output, stream, state);
401
- flushOpenToolCalls(output, stream, state);
402
- calculateCost(model, output.usage);
403
- stream.push({
404
- type: "done",
405
- reason: output.stopReason,
406
- message: output,
407
- });
408
- stream.end();
409
- }
410
- catch (error) {
411
- // Same reason as the success path: a handler still running would land
412
- // its real result after the turn finalized and be discarded — even
413
- // though the tool may already have run side effects. On abort the
414
- // drain returns immediately.
415
- await drainInFlightDispatches();
416
- // A stream that dies mid-turn leaves blocks open. Closing them here
417
- // settles their live cards and pairs the server-owned calls that
418
- // nothing else answers an unpaired call is stripped from every
419
- // rebuilt transcript.
420
- if (openBlockState) {
421
- endCurrentTextBlock(output, stream, openBlockState);
422
- endCurrentThinkingBlock(output, stream, openBlockState);
423
- flushOpenToolCalls(output, stream, openBlockState);
424
- }
425
- const message = error instanceof Error ? error.message : JSON.stringify(error);
426
- // A server-side per-conversation rejection surfaces as a bare
427
- // resource_exhausted with zero tokens the conversation is poisoned,
428
- // not the account. Rotate the wire id once and migrate cached state
429
- // so the caller's retry loop starts a fresh conversation.
430
- if (conversationId !== undefined &&
431
- baseConversationId !== undefined &&
432
- usageState !== undefined &&
433
- !usageState.sawTokenDelta &&
434
- RESOURCE_EXHAUSTED_PATTERN.test(message) &&
435
- !rotatedConversationIds.has(baseConversationId)) {
436
- const rotated = randomUUID();
437
- rotatedConversationIds.set(baseConversationId, rotated);
438
- const cached = conversationStateCache.get(conversationId);
439
- if (cached)
440
- conversationStateCache.set(rotated, cached);
441
- const blobs = conversationBlobStores.get(conversationId);
442
- if (blobs)
443
- conversationBlobStores.set(rotated, blobs);
434
+ catch (error) {
435
+ // Same reason as the success path: a handler still running would land
436
+ // its real result after the turn finalized and be discarded — even
437
+ // though the tool may already have run side effects. On abort the
438
+ // drain returns immediately.
439
+ await drainInFlightDispatches();
440
+ // A stream that dies mid-turn leaves blocks open. Closing them here
441
+ // settles their live cards and pairs the server-owned calls that
442
+ // nothing else answers — an unpaired call is stripped from every
443
+ // rebuilt transcript.
444
+ if (openBlockState) {
445
+ endCurrentTextBlock(output, stream, openBlockState);
446
+ endCurrentThinkingBlock(output, stream, openBlockState);
447
+ flushOpenToolCalls(output, stream, openBlockState);
448
+ }
449
+ let message = error instanceof Error ? error.message : JSON.stringify(error);
450
+ // A server-side per-conversation rejection surfaces as a bare
451
+ // resource_exhausted with zero tokens. That has two distinct causes:
452
+ // an oversized payload, and a genuinely poisoned conversationId.
453
+ // Only the second is fixed by rotating the wire id.
454
+ //
455
+ // The FIRST 0-token RE for a base conversation always surfaces without
456
+ // rotating, so the session layer gets first refusal: agent-session
457
+ // classifies a surfaced 0-token RE as overflow and compacts before
458
+ // retrying. Rotating here instead would swallow the error, make that
459
+ // compaction dead code, and burn the 3-rotation budget replaying the
460
+ // same oversized payload. Once that surface has happened (the flag is
461
+ // persisted with the wire id), compaction has had its turn and further
462
+ // 0-token REs rotate and retry in-call, up to the cap.
463
+ if (conversationId !== undefined &&
464
+ baseConversationId !== undefined &&
465
+ usageState !== undefined &&
466
+ isZeroTokenResourceExhausted(message, usageState.sawTokenDelta)) {
467
+ if (rotationStore().shouldSkip(baseConversationId)) {
468
+ // The base conversation burned its rotation cap; another wire id
469
+ // will not help, so surface the poisoned-conversation error and
470
+ // let the session move to a different provider.
471
+ message = CURSOR_CONVERSATION_POISONED_MESSAGE;
472
+ }
473
+ else if (rotationStore().shouldSurfaceBeforeRotating(baseConversationId)) {
474
+ // First 0-token RE for this conversation: surface it so the
475
+ // session layer can compact. If the payload really was oversized,
476
+ // the compacted retry succeeds and no rotation is ever spent.
477
+ rotationStore().markSurfaced(baseConversationId, conversationId);
478
+ }
479
+ else {
480
+ const decision = rotationStore().recordZeroTokenPoison(baseConversationId, conversationId);
481
+ if (decision.kind === "rotated") {
482
+ const cached = conversationStateCache.get(conversationId);
483
+ if (cached)
484
+ conversationStateCache.set(decision.wireId, cached);
485
+ const blobs = conversationBlobStores.get(conversationId);
486
+ if (blobs)
487
+ conversationBlobStores.set(decision.wireId, blobs);
488
+ retryPoisonedConversation = true;
489
+ }
490
+ else {
491
+ message = CURSOR_CONVERSATION_POISONED_MESSAGE;
492
+ }
493
+ }
494
+ }
495
+ if (!retryPoisonedConversation) {
496
+ output.stopReason = options?.signal?.aborted ? "aborted" : "error";
497
+ output.errorMessage = message;
498
+ stream.push({ type: "error", reason: output.stopReason, error: output });
499
+ stream.end();
500
+ }
444
501
  }
445
- output.stopReason = options?.signal?.aborted ? "aborted" : "error";
446
- output.errorMessage = message;
447
- stream.push({ type: "error", reason: output.stopReason, error: output });
448
- stream.end();
449
- }
450
- finally {
451
- if (heartbeatTimer) {
452
- clearInterval(heartbeatTimer);
453
- heartbeatTimer = null;
502
+ finally {
503
+ if (heartbeatTimer) {
504
+ clearInterval(heartbeatTimer);
505
+ heartbeatTimer = null;
506
+ }
507
+ h2Request?.close();
508
+ h2Client?.close();
509
+ h2Request = null;
510
+ h2Client = null;
454
511
  }
455
- h2Request?.close();
456
- h2Client?.close();
457
- }
512
+ } while (retryPoisonedConversation);
458
513
  })();
459
514
  return stream;
460
515
  };
461
516
  /**
462
- * `streamSimple` for Cursor: reasoning/thinking is managed server-side per
463
- * model (there is no client thinking knob on the Run request), so the simple
464
- * options map through unchanged.
517
+ * `streamSimple` for Cursor: an explicit thinking selection (`options.thinkingSelection`)
518
+ * is rendered into `RequestedModel.parameters`; reasoning output itself streams back as
519
+ * `ThinkingContent` regardless of the selection.
465
520
  */
466
521
  export const streamSimple = (model, context, options) => {
467
522
  return stream(model, context, options);
@@ -487,6 +542,7 @@ export async function handleServerMessage(msg, output, stream, state, blobStore,
487
542
  await stream.trackLocalWork(handleExecServerMessage(msg.message.value, h2Request, execHandlers, onToolResult, requestContextTools, output, stream, state));
488
543
  }
489
544
  else if (msgCase === "conversationCheckpointUpdate") {
545
+ applyCheckpointTokenDetails(msg.message.value, output, usageState);
490
546
  onConversationCheckpoint?.(msg.message.value);
491
547
  }
492
548
  }
@@ -2542,12 +2598,16 @@ export function processInteractionUpdate(update, output, stream, state, usageSta
2542
2598
  const toolCall = update.message.value.toolCall;
2543
2599
  if (settled[kStreamingBlockKind] === "mcp") {
2544
2600
  // Authoritative full parse of the accumulated argument buffer.
2601
+ const previousArgs = settled.arguments;
2545
2602
  const partial = settled[kStreamingPartialJson];
2546
2603
  if (partial !== undefined) {
2547
2604
  settled.arguments = parseStreamingJson(partial);
2548
2605
  }
2549
2606
  const decodedArgs = decodeMcpArgsMap(selectMcpCall(toolCall)?.args?.args);
2550
2607
  settled.arguments = mergeCursorMcpToolCallArgs(settled.arguments, decodedArgs);
2608
+ if (settled.name === "task") {
2609
+ settled.arguments = keepUsableCursorTaskArgs(previousArgs, settled.arguments);
2610
+ }
2551
2611
  }
2552
2612
  else if (settled[kStreamingBlockKind] === "connect-scm") {
2553
2613
  // The authoritative outcome arrives only here. Late args are merged
@@ -2587,14 +2647,79 @@ export function processInteractionUpdate(update, output, stream, state, usageSta
2587
2647
  }
2588
2648
  else if (updateCase === "turnEnded") {
2589
2649
  output.stopReason = "stop";
2650
+ applyBilledTurnEndedUsage(update.message.value, output, usageState);
2590
2651
  }
2591
2652
  else if (updateCase === "tokenDelta") {
2592
2653
  const tokenDelta = update.message.value;
2593
2654
  usageState.sawTokenDelta = true;
2594
2655
  output.usage.output += tokenDelta.tokens || 0;
2595
- output.usage.totalTokens = output.usage.input + output.usage.output;
2656
+ output.usage.totalTokens =
2657
+ output.usage.input + output.usage.output + output.usage.cacheRead + output.usage.cacheWrite;
2596
2658
  }
2597
2659
  }
2660
+ /**
2661
+ * Cursor's production schema (cursor-agent 2026.08.11) carries the billed
2662
+ * token split on `turnEnded`: 1 input, 2 output, 3 cache read, 4 cache write,
2663
+ * 5 reasoning (optional int64). Live probes against api2.cursor.sh show
2664
+ * input_tokens is cache-INCLUSIVE (turn 1: input 21357 ≈ cacheWrite 21354;
2665
+ * turn 2: input 17989 ≈ cacheRead 17575 + cacheWrite 411), so the uncached
2666
+ * remainder is backed out for senpi's exclusive `usage.input`. The billed
2667
+ * split is authoritative for context accounting; the tokenDelta-accumulated
2668
+ * output is kept only when the server omits the billed output field.
2669
+ * Reasoning tokens are deliberately not folded into output: no other field of
2670
+ * `Usage` represents them and double counting against the billed output must
2671
+ * be avoided.
2672
+ */
2673
+ function applyBilledTurnEndedUsage(update, output, usageState) {
2674
+ const { inputTokens, outputTokens, cacheReadTokens, cacheWriteTokens } = update;
2675
+ if (inputTokens === undefined &&
2676
+ outputTokens === undefined &&
2677
+ cacheReadTokens === undefined &&
2678
+ cacheWriteTokens === undefined) {
2679
+ return;
2680
+ }
2681
+ usageState.sawTurnEndedUsage = true;
2682
+ const usage = output.usage;
2683
+ const cacheRead = Number(cacheReadTokens ?? 0n);
2684
+ const cacheWrite = Number(cacheWriteTokens ?? 0n);
2685
+ const liveUsed = usageState.liveUsedTokens ?? 0;
2686
+ // Cursor sometimes reports dashboard-cumulative cache_read (millions) while
2687
+ // usedTokens stays at the real window (~150k). Folding that into totalTokens
2688
+ // forces a useless compact and then a 0-token resource_exhausted.
2689
+ if (liveUsed > 0 && cacheRead > liveUsed * 3) {
2690
+ if (outputTokens !== undefined) {
2691
+ usage.output = Number(outputTokens);
2692
+ }
2693
+ usage.cacheRead = 0;
2694
+ usage.cacheWrite = cacheWrite <= liveUsed ? cacheWrite : 0;
2695
+ usage.input = Math.max(0, liveUsed - usage.output - usage.cacheWrite);
2696
+ usage.totalTokens = liveUsed;
2697
+ return;
2698
+ }
2699
+ usage.cacheRead = cacheRead;
2700
+ usage.cacheWrite = cacheWrite;
2701
+ usage.input = Math.max(0, Number(inputTokens ?? 0n) - usage.cacheRead - usage.cacheWrite);
2702
+ if (outputTokens !== undefined) {
2703
+ usage.output = Number(outputTokens);
2704
+ }
2705
+ usage.totalTokens = usage.input + usage.output + usage.cacheRead + usage.cacheWrite;
2706
+ }
2707
+ /**
2708
+ * A checkpoint's `tokenDetails.usedTokens` is the server's live conversation
2709
+ * size, sent mid-turn. It feeds context accounting while the turn streams,
2710
+ * but never overrides the billed turnEnded split once that arrived.
2711
+ */
2712
+ function applyCheckpointTokenDetails(checkpoint, output, usageState) {
2713
+ if (usageState.sawTurnEndedUsage)
2714
+ return;
2715
+ const usedTokens = checkpoint.tokenDetails?.usedTokens ?? 0;
2716
+ if (usedTokens <= 0)
2717
+ return;
2718
+ usageState.liveUsedTokens = usedTokens;
2719
+ const usage = output.usage;
2720
+ usage.input = Math.max(0, usedTokens - usage.output - usage.cacheRead - usage.cacheWrite);
2721
+ usage.totalTokens = usage.input + usage.output + usage.cacheRead + usage.cacheWrite;
2722
+ }
2598
2723
  function createBlobId(data) {
2599
2724
  return new Uint8Array(createHash("sha256").update(data).digest());
2600
2725
  }
@@ -2625,6 +2750,29 @@ function toolParametersToJsonSchema(tool) {
2625
2750
  return { type: "object", properties: {}, required: [] };
2626
2751
  }
2627
2752
  }
2753
+ /**
2754
+ * JSON-Schema composition keywords Cursor's gateway cannot carry: an
2755
+ * advertised tool whose inputSchema contains `oneOf`, `anyOf`, or `allOf` is
2756
+ * rejected upstream with a wrapped provider 400 for the WHOLE request
2757
+ * (zero tokens, `resource_exhausted` end-stream). MCP tools imported from
2758
+ * external servers routinely ship such schemas (e.g. ast-grep's `scan`).
2759
+ * `not` is tolerated upstream and kept. Returns a new structure; the input is
2760
+ * never mutated.
2761
+ */
2762
+ const CURSOR_UNSUPPORTED_SCHEMA_KEYS = new Set(["oneOf", "anyOf", "allOf"]);
2763
+ export function sanitizeCursorToolSchema(schema) {
2764
+ if (Array.isArray(schema))
2765
+ return schema.map(sanitizeCursorToolSchema);
2766
+ if (schema === null || typeof schema !== "object")
2767
+ return schema;
2768
+ const sanitized = {};
2769
+ for (const [key, value] of Object.entries(schema)) {
2770
+ if (CURSOR_UNSUPPORTED_SCHEMA_KEYS.has(key))
2771
+ continue;
2772
+ sanitized[key] = sanitizeCursorToolSchema(value);
2773
+ }
2774
+ return sanitized;
2775
+ }
2628
2776
  export function buildMcpToolDefinitions(tools) {
2629
2777
  if (!tools || tools.length === 0) {
2630
2778
  return [];
@@ -2634,7 +2782,7 @@ export function buildMcpToolDefinitions(tools) {
2634
2782
  return [];
2635
2783
  }
2636
2784
  return advertisedTools.map((tool) => {
2637
- const jsonSchema = toolParametersToJsonSchema(tool);
2785
+ const jsonSchema = sanitizeCursorToolSchema(toolParametersToJsonSchema(tool));
2638
2786
  const schemaValue = jsonSchema && typeof jsonSchema === "object"
2639
2787
  ? jsonSchema
2640
2788
  : { type: "object", properties: {}, required: [] };
@@ -3096,7 +3244,8 @@ async function buildGrpcRequest(model, context, options, state) {
3096
3244
  rootPromptMessagesJson,
3097
3245
  turns,
3098
3246
  });
3099
- const wireModelId = model.upstreamModelId ?? model.id;
3247
+ const requestedModel = buildRequestedModel(model, options?.thinkingSelection);
3248
+ const wireModelId = requestedModel.modelId;
3100
3249
  const cursorMaxMode = model.compat?.cursorMaxMode === true;
3101
3250
  const modelDetails = create(ModelDetailsSchema, {
3102
3251
  modelId: wireModelId,
@@ -3104,10 +3253,6 @@ async function buildGrpcRequest(model, context, options, state) {
3104
3253
  displayName: model.name,
3105
3254
  ...(cursorMaxMode ? { maxMode: true } : undefined),
3106
3255
  });
3107
- const requestedModel = create(RequestedModelSchema, {
3108
- modelId: wireModelId,
3109
- maxMode: cursorMaxMode,
3110
- });
3111
3256
  const runRequest = create(AgentRunRequestSchema, {
3112
3257
  conversationState,
3113
3258
  action,