cursor-opencode-provider 0.8.0 → 0.8.1

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 (67) hide show
  1. package/CHANGELOG.md +79 -0
  2. package/README.md +28 -9
  3. package/SECURITY.md +43 -7
  4. package/dist/context/build.d.ts +22 -4
  5. package/dist/context/build.js +33 -5
  6. package/dist/context/dynamic-catalog.js +6 -1
  7. package/dist/context/frozen.js +6 -2
  8. package/dist/context/index.d.ts +2 -0
  9. package/dist/context/index.js +2 -0
  10. package/dist/context/paths.d.ts +44 -17
  11. package/dist/context/paths.js +65 -18
  12. package/dist/context/skills-bridge.d.ts +22 -0
  13. package/dist/context/skills-bridge.js +20 -0
  14. package/dist/context/skills.d.ts +34 -0
  15. package/dist/context/skills.js +211 -0
  16. package/dist/host-agent-mode.d.ts +53 -4
  17. package/dist/host-agent-mode.js +117 -13
  18. package/dist/host-plan-file.d.ts +28 -0
  19. package/dist/host-plan-file.js +76 -0
  20. package/dist/image-input.d.ts +18 -0
  21. package/dist/image-input.js +119 -28
  22. package/dist/image-save.d.ts +10 -2
  23. package/dist/image-save.js +33 -21
  24. package/dist/language-model.d.ts +45 -17
  25. package/dist/language-model.js +925 -292
  26. package/dist/model-config.d.ts +9 -0
  27. package/dist/model-config.js +28 -1
  28. package/dist/opencode2/catalog.d.ts +1 -0
  29. package/dist/opencode2/catalog.js +2 -0
  30. package/dist/opencode2/image-save-tool.d.ts +8 -0
  31. package/dist/opencode2/image-save-tool.js +84 -0
  32. package/dist/opencode2/types.d.ts +15 -0
  33. package/dist/parallel-step.d.ts +65 -0
  34. package/dist/parallel-step.js +102 -0
  35. package/dist/plugin-opencode2.js +48 -40
  36. package/dist/plugin.js +69 -18
  37. package/dist/pricing-data.d.ts +14 -1
  38. package/dist/pricing-data.js +14 -1
  39. package/dist/protocol/ask-question.d.ts +12 -1
  40. package/dist/protocol/ask-question.js +77 -3
  41. package/dist/protocol/create-plan.d.ts +47 -25
  42. package/dist/protocol/create-plan.js +33 -23
  43. package/dist/protocol/interactions.d.ts +26 -17
  44. package/dist/protocol/interactions.js +80 -31
  45. package/dist/protocol/messages.js +74 -14
  46. package/dist/protocol/request.d.ts +5 -0
  47. package/dist/protocol/request.js +2 -0
  48. package/dist/protocol/switch-mode.d.ts +66 -27
  49. package/dist/protocol/switch-mode.js +147 -73
  50. package/dist/protocol/token-details.d.ts +2 -0
  51. package/dist/protocol/token-details.js +11 -1
  52. package/dist/protocol/tool-call-bridge.d.ts +4 -0
  53. package/dist/protocol/tool-call-bridge.js +14 -0
  54. package/dist/protocol/tools.d.ts +16 -1
  55. package/dist/protocol/tools.js +113 -11
  56. package/dist/replay-safety.d.ts +6 -0
  57. package/dist/replay-safety.js +56 -6
  58. package/dist/session.d.ts +35 -1
  59. package/dist/session.js +45 -1
  60. package/dist/todo-store.d.ts +2 -0
  61. package/dist/todo-store.js +19 -0
  62. package/dist/transport/connect.d.ts +2 -0
  63. package/dist/transport/connect.js +23 -6
  64. package/dist/usage.js +23 -6
  65. package/package.json +3 -2
  66. package/dist/plan-execution-kickoff.d.ts +0 -57
  67. package/dist/plan-execution-kickoff.js +0 -186
@@ -2,7 +2,7 @@ import { encodeMessage } from "./messages.js";
2
2
  import { readAllFields } from "./struct.js";
3
3
  import { decodeAskQuestionQuery, MISSING_ARGS_REASON, MISSING_QUERY_REASON, } from "./ask-question.js";
4
4
  import { decodeGenerateImageQuery, } from "./generate-image.js";
5
- import { CURSOR_PLAN_STAGE_TOOL, createPlanApprovalQuestionInput, decodeCreatePlanQuery, renderPlanReviewMessage, resolveCreatePlanBridge, writeOpencodePlanFile, } from "./create-plan.js";
5
+ import { CURSOR_PLAN_STAGE_TOOL, decodeCreatePlanQuery, createPlanApprovalQuestionInput, renderPlanReviewMessage, resolveCreatePlanBridge, writeOpencodePlanFile, } from "./create-plan.js";
6
6
  import { decodeSwitchModeQuery, MISSING_ARGS_REASON as SWITCH_MODE_MISSING_ARGS_REASON, MISSING_QUERY_REASON as SWITCH_MODE_MISSING_QUERY_REASON, resolveSwitchModeBridge, switchModeApprovedResult, } from "./switch-mode.js";
7
7
  const HEADLESS_REASON = "This Cursor interaction requires UI approval and is not available through the OpenCode provider.";
8
8
  /**
@@ -76,9 +76,9 @@ export function inspectInteractionQueryWire(agentServerPayload) {
76
76
  * Bridged / persisted exceptions:
77
77
  * - AskQuestion (#3) → OpenCode `question` tool
78
78
  * - SwitchMode (#4) → OpenCode `plan_enter` / `plan_exit` when advertised
79
- * - CreatePlan (#7) → host-calculated plan file via hostPlansDir (project-config
80
- * `plans/` in a git worktree, else host global data/plans); empty args still
81
- * get the CLI empty-`plan_uri` success ack
79
+ * - CreatePlan (#7) → the host's plan file (session `Session.plan` file when
80
+ * known, else a new file under hostPlansDir); empty args still get the CLI
81
+ * empty-`plan_uri` success ack
82
82
  * - GenerateImage (#12) → approve when `cursor_image_save` is advertised
83
83
  */
84
84
  export function handleInteractionQuery(query, agentServerPayload, options = {}) {
@@ -160,7 +160,7 @@ export function handleInteractionQuery(query, agentServerPayload, options = {})
160
160
  * non-blocking prompt, and otherwise hold the query open until the user picks.
161
161
  */
162
162
  function handleAskQuestionQuery(id, variantBytes, options) {
163
- const base = { id, variantField: 3, variantName: "ask_question_interaction_query" };
163
+ const base = { id, variantField: 3, variantName: "ask_question_interaction_query", toolCallId: undefined };
164
164
  const reject = (reason) => ({
165
165
  ...base,
166
166
  outcome: "rejected",
@@ -171,6 +171,7 @@ function handleAskQuestionQuery(id, variantBytes, options) {
171
171
  const decoded = decodeAskQuestionQuery(variantBytes);
172
172
  if (!decoded)
173
173
  return reject(MISSING_ARGS_REASON);
174
+ base.toolCallId = decoded.toolCallId;
174
175
  if (!options.canBridgeAskQuestion)
175
176
  return reject(ASK_QUESTION_UNAVAILABLE_REASON);
176
177
  return {
@@ -200,7 +201,7 @@ export function buildAskQuestionInteractionReply(id, result) {
200
201
  * until the host tool returns, then reply approved{} or rejected{reason}.
201
202
  */
202
203
  function handleSwitchModeQuery(id, variantBytes, options) {
203
- const base = { id, variantField: 4, variantName: "switch_mode_request_query" };
204
+ const base = { id, variantField: 4, variantName: "switch_mode_request_query", toolCallId: undefined };
204
205
  const reject = (reason) => ({
205
206
  ...base,
206
207
  outcome: "rejected",
@@ -211,10 +212,12 @@ function handleSwitchModeQuery(id, variantBytes, options) {
211
212
  const decoded = decodeSwitchModeQuery(variantBytes);
212
213
  if (!decoded)
213
214
  return reject(SWITCH_MODE_MISSING_ARGS_REASON);
215
+ base.toolCallId = decoded.toolCallId;
214
216
  const bridge = resolveSwitchModeBridge(decoded.args.targetModeId, {
215
217
  allowTools: options.allowTools === true,
216
218
  advertised: options.advertisedTools ?? [],
217
219
  ...(options.activeCursorModeId ? { activeModeId: options.activeCursorModeId } : {}),
220
+ ...(options.hostAgent ? { hostAgent: options.hostAgent } : {}),
218
221
  });
219
222
  if (bridge.kind === "reject")
220
223
  return reject(bridge.reason);
@@ -243,10 +246,10 @@ function handleSwitchModeQuery(id, variantBytes, options) {
243
246
  switchMode: {
244
247
  ...decoded,
245
248
  bridge,
246
- toolName: bridge.kind === "native" ? bridge.toolName : "question",
249
+ toolName: bridge.toolName,
247
250
  },
248
- // Keep Cursor waiting until the host tool returns, matching CLI blocking on
249
- // the mode-switch approval prompt.
251
+ // Keep Cursor waiting until the host plan tool returns: the host owns the
252
+ // mode-switch approval.
250
253
  reply: undefined,
251
254
  };
252
255
  }
@@ -298,7 +301,7 @@ export function buildAsyncAskQuestionCompletion(originalToolCallId, originalArgs
298
301
  * written but not accepted, so the model keeps planning.
299
302
  */
300
303
  function handleCreatePlanQuery(id, variantBytes, options) {
301
- const base = { id, variantField: 7, variantName: "create_plan_request_query" };
304
+ const base = { id, variantField: 7, variantName: "create_plan_request_query", toolCallId: undefined };
302
305
  const reply = (result) => ({
303
306
  ...base,
304
307
  outcome: result.error ? "failed" : "acknowledged",
@@ -315,13 +318,46 @@ function handleCreatePlanQuery(id, variantBytes, options) {
315
318
  const decoded = decodeCreatePlanQuery(variantBytes);
316
319
  if (!decoded)
317
320
  return reply({ success: {}, plan_uri: "" });
321
+ base.toolCallId = decoded.toolCallId;
318
322
  const bridge = resolveCreatePlanBridge({
319
323
  allowTools: options.allowTools === true,
320
324
  canStage: options.canBridgeCreatePlan === true,
321
- planModeActive: options.planModeActive === true,
322
325
  advertised: options.advertisedTools ?? [],
326
+ hostPlanEntryPending: options.hostPlanEntryPending === true,
327
+ ...(options.hostAgent ? { hostAgent: options.hostAgent } : {}),
328
+ planModeActive: options.planModeActive === true,
329
+ ...(options.hostPlanFile ? { hostPlanFile: options.hostPlanFile } : {}),
323
330
  });
324
- // The host plan-stage tool owns the write and the approval prompt together.
331
+ // The host's plan agent records and reviews the plan; write nothing here.
332
+ if (bridge.kind === "defer") {
333
+ return {
334
+ ...reply({ error: { error: bridge.reason }, plan_uri: "" }),
335
+ ...(decoded.toolCallId ? { deferredCreatePlanToolCallId: decoded.toolCallId } : {}),
336
+ };
337
+ }
338
+ // The host plan agent reviews its own plan file: record the plan there and
339
+ // hand it to the host plan_exit, which asks the user and, on approval,
340
+ // moves the session to its build agent.
341
+ if (bridge.kind === "exit") {
342
+ const written = writeOpencodePlanFile(decoded.args, options.workspaceRoot ?? "", Date.now(), bridge.planPath);
343
+ if (!written.ok) {
344
+ return reply({ error: { error: written.error }, plan_uri: "" });
345
+ }
346
+ return {
347
+ ...base,
348
+ outcome: "bridged",
349
+ createPlan: {
350
+ ...decoded,
351
+ bridge,
352
+ toolName: "plan_exit",
353
+ planUri: written.planUri,
354
+ planPath: written.planPath,
355
+ planReview: renderPlanReviewMessage(written.markdown, written.planPath),
356
+ },
357
+ reply: undefined,
358
+ };
359
+ }
360
+ // The host plan-stage tool owns the write and the review together.
325
361
  if (bridge.kind === "stage") {
326
362
  return {
327
363
  ...base,
@@ -330,25 +366,18 @@ function handleCreatePlanQuery(id, variantBytes, options) {
330
366
  reply: undefined,
331
367
  };
332
368
  }
333
- // A lifecycle turn (title generation, compaction) runs alongside the real one
334
- // and would otherwise write a second, throwaway plan file for the same
335
- // request. Ack it the CLI way instead of persisting anything.
336
- if (options.allowTools !== true)
337
- return reply({ success: {}, plan_uri: "" });
338
- const workspaceRoot = options.workspaceRoot?.trim();
339
- if (!workspaceRoot) {
340
- return reply({
341
- error: { error: "CreatePlan requires a workspace root to write the plan file" },
342
- plan_uri: "",
343
- });
344
- }
345
- const written = writeOpencodePlanFile(decoded.args, workspaceRoot);
346
- if (!written.ok) {
347
- return reply({ error: { error: written.error }, plan_uri: "" });
348
- }
349
- // The plan is on disk either way; only execution needs the user. Hold Cursor's
350
- // query open while the host asks, exactly as the CLI blocks on its own prompt.
351
369
  if (bridge.kind === "approve") {
370
+ const workspaceRoot = options.workspaceRoot?.trim();
371
+ if (!workspaceRoot) {
372
+ return reply({
373
+ error: { error: "CreatePlan requires a workspace root to write the plan file" },
374
+ plan_uri: "",
375
+ });
376
+ }
377
+ const written = writeOpencodePlanFile(decoded.args, workspaceRoot, Date.now(), options.hostPlanFile);
378
+ if (!written.ok) {
379
+ return reply({ error: { error: written.error }, plan_uri: "" });
380
+ }
352
381
  return {
353
382
  ...base,
354
383
  outcome: "bridged",
@@ -364,6 +393,25 @@ function handleCreatePlanQuery(id, variantBytes, options) {
364
393
  reply: undefined,
365
394
  };
366
395
  }
396
+ // A lifecycle turn (title generation, compaction) runs alongside the real one
397
+ // and would otherwise write a second, throwaway plan file for the same
398
+ // request. Ack it the CLI way instead of persisting anything.
399
+ if (options.allowTools !== true)
400
+ return reply({ success: {}, plan_uri: "" });
401
+ const workspaceRoot = options.workspaceRoot?.trim();
402
+ if (!workspaceRoot) {
403
+ return reply({
404
+ error: { error: "CreatePlan requires a workspace root to write the plan file" },
405
+ plan_uri: "",
406
+ });
407
+ }
408
+ // The session's own plan file when known, as the host's plan agent uses it.
409
+ const written = writeOpencodePlanFile(decoded.args, workspaceRoot, Date.now(), options.hostPlanFile);
410
+ if (!written.ok) {
411
+ return reply({ error: { error: written.error }, plan_uri: "" });
412
+ }
413
+ // Nothing advertised can ask: the plan is recorded where the host keeps
414
+ // plans. Execution starts only if the user later switches agents.
367
415
  return reply({ success: {}, plan_uri: written.planUri });
368
416
  }
369
417
  /**
@@ -375,7 +423,7 @@ function handleCreatePlanQuery(id, variantBytes, options) {
375
423
  * nowhere to go.
376
424
  */
377
425
  function handleGenerateImageQuery(id, variantBytes, options) {
378
- const base = { id, variantField: 12, variantName: "generate_image_request_query" };
426
+ const base = { id, variantField: 12, variantName: "generate_image_request_query", toolCallId: undefined };
379
427
  const reject = (reason) => ({
380
428
  ...base,
381
429
  outcome: "rejected",
@@ -388,6 +436,7 @@ function handleGenerateImageQuery(id, variantBytes, options) {
388
436
  const decoded = decodeGenerateImageQuery(variantBytes);
389
437
  if (!decoded)
390
438
  return reject("Missing generate image arguments");
439
+ base.toolCallId = decoded.toolCallId;
391
440
  if (!options.canSaveGeneratedImage) {
392
441
  return reject("This OpenCode agent cannot save a generated image, so generating one would "
393
442
  + "produce nothing. Describe the image in your reply instead.");
@@ -28,8 +28,8 @@ export function createMessageTypes() {
28
28
  ]);
29
29
  addType(root, "Heartbeat", []);
30
30
  // Display ToolCall (interaction_update.tool_call_*) — agent.v1 oneof, not
31
- // {tool_name,args} strings. Args-only wrappers are enough to bridge into
32
- // OpenCode; result payloads are ignored on decode.
31
+ // {tool_name,args} strings. Decode finalized todo state for mirroring and
32
+ // MCP errors for diagnostics; other completion results are not replayed.
33
33
  root.add(new protobuf.Enum("TodoStatus", {
34
34
  TODO_STATUS_UNSPECIFIED: 0,
35
35
  TODO_STATUS_PENDING: 1,
@@ -79,7 +79,10 @@ export function createMessageTypes() {
79
79
  ]);
80
80
  addType(root, "EditToolCall", [{ id: 1, name: "args", type: "EditToolArgs" }]);
81
81
  addType(root, "LsToolCall", [{ id: 1, name: "args", type: "LsArgs" }]);
82
- addType(root, "McpToolCall", [{ id: 1, name: "args", type: "McpArgs" }]);
82
+ addType(root, "McpToolCall", [
83
+ { id: 1, name: "args", type: "McpArgs" },
84
+ { id: 2, name: "result", type: "McpResult" },
85
+ ]);
83
86
  addType(root, "CreatePlanArgs", [
84
87
  { id: 1, name: "plan", type: "string" },
85
88
  { id: 2, name: "todos", type: "TodoItem", repeated: true },
@@ -389,6 +392,13 @@ export function createMessageTypes() {
389
392
  { id: 3, name: "args_text_delta", type: "string" },
390
393
  { id: 4, name: "model_call_id", type: "string" },
391
394
  ]);
395
+ // agent.v1 ToolCallDeltaUpdate: keep the nested delta opaque; the pump
396
+ // needs its presence as generation progress, not its display-only content.
397
+ addType(root, "ToolCallDeltaUpdate", [
398
+ { id: 1, name: "call_id", type: "string" },
399
+ { id: 2, name: "tool_call_delta", type: "bytes" },
400
+ { id: 3, name: "model_call_id", type: "string" },
401
+ ]);
392
402
  // agent.v1 StepStartedUpdate / StepCompletedUpdate: step_id is uint64 (T:4),
393
403
  // not string — wrong type made every step_completed frame throw
394
404
  // "index out of range" in decodeMessage.
@@ -399,6 +409,12 @@ export function createMessageTypes() {
399
409
  { id: 1, name: "step_id", type: "uint64" },
400
410
  { id: 2, name: "step_duration_ms", type: "int64" },
401
411
  ]);
412
+ // agent.v1 ToolRequestsListedUpdate — Cursor lists how many tool calls this
413
+ // generation will send so the client can place its step boundary (CLI does
414
+ // not need this; OpenCode's AI SDK step does). Field 27 on InteractionUpdate.
415
+ addType(root, "ToolRequestsListedUpdate", [
416
+ { id: 1, name: "call_count", type: "uint32" },
417
+ ]);
402
418
  // InteractionUpdate — the core streaming update message
403
419
  addType(root, "InteractionUpdate", [
404
420
  { id: 1, name: "text_delta", type: "TextDeltaUpdate" },
@@ -408,9 +424,11 @@ export function createMessageTypes() {
408
424
  { id: 7, name: "partial_tool_call", type: "PartialToolCall" },
409
425
  { id: 13, name: "heartbeat", type: "Heartbeat" },
410
426
  { id: 14, name: "turn_ended", type: "TurnEnded" },
427
+ { id: 15, name: "tool_call_delta", type: "ToolCallDeltaUpdate" },
411
428
  { id: 16, name: "step_started", type: "StepStarted" },
412
429
  { id: 17, name: "step_completed", type: "StepCompleted" },
413
- ], [{ name: "update", fields: ["text_delta", "tool_call_started", "tool_call_completed", "thinking_delta", "partial_tool_call", "heartbeat", "turn_ended", "step_started", "step_completed"] }]);
430
+ { id: 27, name: "tool_requests_listed", type: "ToolRequestsListedUpdate" },
431
+ ], [{ name: "update", fields: ["text_delta", "tool_call_started", "tool_call_completed", "thinking_delta", "partial_tool_call", "heartbeat", "turn_ended", "tool_call_delta", "step_started", "step_completed", "tool_requests_listed"] }]);
414
432
  // ── Exec channel ──
415
433
  // Field numbers match agent.v1. Extra fields we don't use are still declared
416
434
  // so protobufjs doesn't drop them on decode.
@@ -934,12 +952,25 @@ export function createMessageTypes() {
934
952
  { id: 5, name: "tool_name", type: "string" },
935
953
  ]);
936
954
  addType(root, "McpTextContent", [{ id: 1, name: "text", type: "string" }]);
937
- addType(root, "McpToolResultContentItem", [{ id: 1, name: "text", type: "McpTextContent" }], [{ name: "content", fields: ["text"] }]);
955
+ // Cursor CLI 2026.10.01 `agent/v1/mcp_exec_pb.js`: "McpImageContent|1 data 12|2 mime_type 9",
956
+ // "McpToolResultContentItem|1 text #0 content|2 image #1 content".
957
+ addType(root, "McpImageContent", [
958
+ { id: 1, name: "data", type: "bytes" },
959
+ { id: 2, name: "mime_type", type: "string" },
960
+ ]);
961
+ addType(root, "McpToolResultContentItem", [
962
+ { id: 1, name: "text", type: "McpTextContent" },
963
+ { id: 2, name: "image", type: "McpImageContent" },
964
+ ], [{ name: "content", fields: ["text", "image"] }]);
938
965
  addType(root, "McpSuccess", [
939
966
  { id: 1, name: "content", type: "McpToolResultContentItem", repeated: true },
940
967
  { id: 2, name: "is_error", type: "bool" },
941
968
  ]);
942
- addType(root, "McpError", [{ id: 1, name: "error", type: "string" }]);
969
+ addType(root, "McpError", [
970
+ { id: 1, name: "error", type: "string" },
971
+ // Server-side CallDynamicTool validation uses a title (#1) and detail (#2).
972
+ { id: 2, name: "detail", type: "string" },
973
+ ]);
943
974
  addType(root, "McpResult", [
944
975
  { id: 1, name: "success", type: "McpSuccess" },
945
976
  { id: 2, name: "error", type: "McpError" },
@@ -1347,10 +1378,23 @@ export function createMessageTypes() {
1347
1378
  addType(root, "SelectedContext", [
1348
1379
  { id: 1, name: "selected_images", type: "SelectedImage", repeated: true },
1349
1380
  ]);
1381
+ // agent.v1.AgentMode — the client's current mode, sent on every user message.
1382
+ root.add(new protobuf.Enum("AgentMode", {
1383
+ AGENT_MODE_UNSPECIFIED: 0,
1384
+ AGENT_MODE_AGENT: 1,
1385
+ AGENT_MODE_ASK: 2,
1386
+ AGENT_MODE_PLAN: 3,
1387
+ AGENT_MODE_DEBUG: 4,
1388
+ AGENT_MODE_TRIAGE: 5,
1389
+ AGENT_MODE_PROJECT: 6,
1390
+ AGENT_MODE_MULTITASK: 7,
1391
+ AGENT_MODE_CUSTOM: 8,
1392
+ }));
1350
1393
  addType(root, "UserMessage", [
1351
1394
  { id: 1, name: "text", type: "string" },
1352
1395
  { id: 2, name: "message_id", type: "string" },
1353
1396
  { id: 3, name: "selected_context", type: "SelectedContext" },
1397
+ { id: 4, name: "mode", type: "AgentMode" },
1354
1398
  ]);
1355
1399
  // RequestContext — UserMessageAction #2. Slim mcp_meta_tool_options names
1356
1400
  // the advertised catalog; AgentRunRequest.mcp_tools (#4) is empty on real turns.
@@ -1390,7 +1434,8 @@ export function createMessageTypes() {
1390
1434
  { id: 2, name: "request_context", type: "RequestContext" },
1391
1435
  ]);
1392
1436
  addType(root, "CancelAction", [
1393
- { id: 1, name: "conversation_id", type: "string" },
1437
+ // Cursor CLI CancelAction field #1 is the interruption reason, not an id.
1438
+ { id: 1, name: "reason", type: "string" },
1394
1439
  ]);
1395
1440
  // Deferred answers for an AskQuestion the client already replied to with
1396
1441
  // `async`. `original_args` is kept as raw bytes so the exact AskQuestionArgs
@@ -1401,11 +1446,23 @@ export function createMessageTypes() {
1401
1446
  { id: 2, name: "original_args", type: "bytes" },
1402
1447
  { id: 3, name: "result", type: "AskQuestionResult" },
1403
1448
  ]);
1449
+ // Cursor CLI 2026.10.01 descriptors: system context is
1450
+ // injected into the current Run without submitting another user turn.
1451
+ addType(root, "SystemContextInjection", [
1452
+ { id: 1, name: "producer", type: "string" },
1453
+ { id: 2, name: "content", type: "string" },
1454
+ ]);
1455
+ addType(root, "InjectContextAction", [
1456
+ { id: 1, name: "injection_id", type: "string" },
1457
+ { id: 2, name: "expected_run_id", type: "string" },
1458
+ { id: 4, name: "system_context", type: "SystemContextInjection" },
1459
+ ]);
1404
1460
  addType(root, "ConversationAction", [
1405
1461
  { id: 1, name: "user_message_action", type: "UserMessageAction" },
1406
1462
  { id: 2, name: "resume_action", type: "ResumeAction" },
1407
1463
  { id: 3, name: "cancel_action", type: "CancelAction" },
1408
1464
  { id: 8, name: "async_ask_question_completion_action", type: "AsyncAskQuestionCompletionAction" },
1465
+ { id: 19, name: "inject_context_action", type: "InjectContextAction" },
1409
1466
  ], [{
1410
1467
  name: "action",
1411
1468
  fields: [
@@ -1413,6 +1470,7 @@ export function createMessageTypes() {
1413
1470
  "resume_action",
1414
1471
  "cancel_action",
1415
1472
  "async_ask_question_completion_action",
1473
+ "inject_context_action",
1416
1474
  ],
1417
1475
  }]);
1418
1476
  // Seed ConversationStateStructure for turn 1 (system prompt as JSON strings
@@ -1701,11 +1759,11 @@ export function createMessageTypes() {
1701
1759
  { id: 2, name: "value", type: "string" },
1702
1760
  ]);
1703
1761
  addType(root, "AvailableModelVariant", [
1704
- { id: 1, name: "display_name", type: "string" },
1705
- { id: 2, name: "is_max_mode", type: "bool" },
1706
- { id: 3, name: "is_default_max_config", type: "bool" },
1707
- { id: 4, name: "is_default_non_max_config", type: "bool" },
1708
- { id: 5, name: "parameter_values", type: "AvailableModelParameterValue", repeated: true },
1762
+ { id: 1, name: "parameter_values", type: "AvailableModelParameterValue", repeated: true },
1763
+ { id: 2, name: "display_name", type: "string" },
1764
+ { id: 3, name: "is_max_mode", type: "bool" },
1765
+ { id: 4, name: "is_default_max_config", type: "bool" },
1766
+ { id: 5, name: "is_default_non_max_config", type: "bool" },
1709
1767
  ]);
1710
1768
  addType(root, "AvailableModelEntry", [
1711
1769
  { id: 1, name: "name", type: "string" },
@@ -1714,14 +1772,16 @@ export function createMessageTypes() {
1714
1772
  { id: 9, name: "supports_thinking", type: "bool" },
1715
1773
  { id: 10, name: "supports_images", type: "bool" },
1716
1774
  { id: 14, name: "supports_max_mode", type: "bool" },
1717
- { id: 15, name: "context_token_limit", type: "uint32" },
1775
+ { id: 15, name: "context_token_limit", type: "int32" },
1776
+ { id: 16, name: "context_token_limit_for_max_mode", type: "int32" },
1718
1777
  { id: 17, name: "client_display_name", type: "string" },
1719
1778
  { id: 18, name: "server_model_name", type: "string" },
1720
1779
  { id: 29, name: "parameter_definitions", type: "AvailableModelParameterDefinition", repeated: true },
1721
1780
  { id: 30, name: "variants", type: "AvailableModelVariant", repeated: true },
1722
1781
  ]);
1723
1782
  addType(root, "AvailableModelsResponse", [
1724
- { id: 1, name: "models", type: "AvailableModelEntry", repeated: true },
1783
+ { id: 1, name: "model_names", type: "string", repeated: true },
1784
+ { id: 2, name: "models", type: "AvailableModelEntry", repeated: true },
1725
1785
  ]);
1726
1786
  return root;
1727
1787
  }
@@ -37,6 +37,11 @@ export type RunRequestInput = {
37
37
  requestContext?: Record<string, unknown>;
38
38
  /** Resume the supplied checkpoint instead of submitting another user turn. */
39
39
  action?: "user" | "resume";
40
+ /**
41
+ * Cursor mode of this user turn (`UserMessage.mode`, an `agent.v1.AgentMode`
42
+ * value). Cursor CLI sends its current mode on every user message.
43
+ */
44
+ mode?: number;
40
45
  };
41
46
  /**
42
47
  * Seed ConversationStateStructure for the first turn (no checkpoint yet).
@@ -47,6 +47,8 @@ export function buildRunRequest(input) {
47
47
  text: input.text,
48
48
  message_id: msgId,
49
49
  };
50
+ if (input.mode !== undefined)
51
+ userMessage.mode = input.mode;
50
52
  if (input.images?.length) {
51
53
  userMessage.selected_context = {
52
54
  selected_images: input.images.map((image) => ({
@@ -17,18 +17,18 @@
17
17
  * through the Question service and, on "Yes", injects a synthetic user message
18
18
  * carrying `agent: "plan" | "build"`, which `createUserMessage` turns into a
19
19
  * `setAgentModel` primary-agent switch (opencode `tool/plan.ts`,
20
- * `session/prompt.ts`). A provider cannot reach `setAgentModel`, but the two
21
- * observable halves — the approval gate and the behavioural contract — are both
22
- * reachable, so the protocol bridge degrades instead of refusing when the host
23
- * tool is missing. OpenCode 2 additionally exposes `session.switchAgent`; its
20
+ * `session/prompt.ts`). OpenCode 2 exposes `session.switchAgent`; its
24
21
  * entrypoint installs a structural callback that selects the native `plan` or
25
22
  * `build` primary agent after the owning Cursor Run becomes terminal:
26
23
  *
27
24
  * - entering plan/spec needs no host tool at all — approve immediately and let
28
25
  * the injected <system_reminder> carry the contract, exactly as Cursor CLI's
29
26
  * own plan mode is prompt-enforced.
30
- * - leaving plan mode is the gate the user must actually see, so it falls back
31
- * to the host `question` tool and only rejects when that is absent too.
27
+ * - leaving the host's own `plan` agent without its `plan_exit` goes through
28
+ * CreatePlan, whose `question` prompt is the execution approval, so the
29
+ * request is rejected with a pointer there (or, with no `question`, to the
30
+ * user's agent switch); a Cursor-only plan mode has nothing on the host to
31
+ * leave and is approved.
32
32
  *
33
33
  * Resolution is keyed solely on the advertised catalog under canonical
34
34
  * OpenCode tool names — never on any external identity or model id.
@@ -36,7 +36,6 @@
36
36
  * CLI reject reason for user declines: "Mode switch rejected by user"
37
37
  * (chunk-7076/dist/ui.js onSwitchModeReject).
38
38
  */
39
- import { type OpencodeQuestionInput } from "./ask-question.js";
40
39
  /**
41
40
  * `PendingExec.resultField` for a held-open SwitchMode InteractionQuery.
42
41
  * Continuation writes an InteractionResponse, not an ExecClientMessage.
@@ -58,10 +57,16 @@ export declare const MISSING_TARGET_REASON = "Missing targetModeId";
58
57
  * even though the next Run will succeed. CreatePlan already soft-acks the same
59
58
  * case; SwitchMode must match.
60
59
  */
61
- export declare const PLAN_EXIT_UNAVAILABLE_REASON: string;
62
- /** Upstream `PlanExitTool` wording, minus a plan path the provider cannot verify. */
63
- export declare const SWITCH_MODE_EXIT_QUESTION = "Planning is complete. Would you like to switch to the build agent and start implementing?";
64
- export declare const SWITCH_MODE_EXIT_HEADER = "Build Agent";
60
+ /**
61
+ * The host's plan agent is active and offers no `plan_exit`: on such a host
62
+ * the user ends plan mode by switching agents, so the model cannot.
63
+ */
64
+ export declare const PLAN_EXIT_BY_USER_REASON: string;
65
+ /**
66
+ * The host's plan agent is active, offers no `plan_exit`, but can ask: leaving
67
+ * plan mode goes through CreatePlan, whose `question` prompt is the approval.
68
+ */
69
+ export declare const PLAN_EXIT_VIA_CREATE_PLAN_REASON: string;
65
70
  export type SwitchModeHostTool = "plan_enter" | "plan_exit";
66
71
  /**
67
72
  * How a SwitchMode query is satisfied this turn.
@@ -70,7 +75,6 @@ export type SwitchModeHostTool = "plan_enter" | "plan_exit";
70
75
  * - `approve` entering plan mode needs no host tool — approve immediately.
71
76
  * - `ack` lifecycle turn (`allowTools=false`): approve on the wire without
72
77
  * mutating session mode (same soft-ack pattern as CreatePlan).
73
- * - `question` leaving plan mode falls back to the host `question` prompt.
74
78
  * - `reject` nothing can satisfy it; `reason` names the real cause.
75
79
  */
76
80
  export type SwitchModeBridge = {
@@ -80,9 +84,6 @@ export type SwitchModeBridge = {
80
84
  kind: "approve";
81
85
  } | {
82
86
  kind: "ack";
83
- } | {
84
- kind: "question";
85
- input: OpencodeQuestionInput;
86
87
  } | {
87
88
  kind: "reject";
88
89
  reason: string;
@@ -114,21 +115,22 @@ export declare function normalizeSwitchModeId(targetModeId: string): string;
114
115
  * for CLI-shaped behavioral guidance.
115
116
  */
116
117
  export declare function mapSwitchModeTarget(targetModeId: string): SwitchModeMapping;
117
- /** Host `question` input mirroring upstream `PlanExitTool`'s own prompt. */
118
- export declare function switchModeExitQuestionInput(): OpencodeQuestionInput;
119
118
  /**
120
119
  * Resolve how this SwitchMode target is satisfied, keyed only on the advertised
121
120
  * catalog under canonical tool names.
122
121
  *
123
- * A missing plan tool is not a refusal: entering plan mode needs no host tool,
124
- * and leaving it degrades to the `question` prompt so the user still approves
125
- * before any execution starts.
122
+ * A missing plan tool is not a refusal: entering plan mode needs no host tool.
123
+ * Leaving the host's own plan agent without `plan_exit` is approved through
124
+ * CreatePlan (or the user's agent switch when nothing can ask), and a
125
+ * Cursor-only plan mode (no host plan agent) has nothing on the host to leave.
126
126
  */
127
127
  export declare function resolveSwitchModeBridge(targetModeId: string, options: {
128
128
  allowTools: boolean;
129
129
  advertised: ReadonlySet<string> | Iterable<string>;
130
130
  /** Cursor unified mode currently recorded for this session, if any. */
131
131
  activeModeId?: string;
132
+ /** Host primary agent of this Run, when the host reported it. */
133
+ hostAgent?: string;
132
134
  }): SwitchModeBridge;
133
135
  /** Decode a `switch_mode_request_query` body, or undefined when unusable. */
134
136
  export declare function decodeSwitchModeQuery(queryBytes: Uint8Array): DecodedSwitchModeQuery | undefined;
@@ -142,12 +144,6 @@ export declare function switchModeRejectedResult(reason: string): Record<string,
142
144
  * Question/permission declines and empty failures use the CLI user-reject string.
143
145
  */
144
146
  export declare function switchModeResultFromToolOutput(output: string, isError: boolean): Record<string, unknown>;
145
- /**
146
- * Host `question` outcome → Cursor SwitchModeRequestResponse, for the emulated
147
- * plan exit. Only an explicit "Yes" approves; an unanswered or dismissed prompt
148
- * keeps the model in plan mode rather than silently starting execution.
149
- */
150
- export declare function switchModeResultFromQuestionOutput(output: string, isError: boolean): Record<string, unknown>;
151
147
  /** Record the approved Cursor mode for later system-reminder injection. */
152
148
  export declare function setActiveCursorMode(sessionKey: string | undefined, targetModeId: string, options?: {
153
149
  bridgedPlanEntered?: boolean;
@@ -155,8 +151,47 @@ export declare function setActiveCursorMode(sessionKey: string | undefined, targ
155
151
  export declare function getActiveCursorMode(sessionKey: string | undefined): string | undefined;
156
152
  /** True while the provider has an approved Cursor plan/spec mode for this session. */
157
153
  export declare function isCursorPlanModeActive(sessionKey: string | undefined): boolean;
158
- /** True after a bridged plan/spec Run has observed plan_enter removed by the host. */
154
+ /** True while Cursor plan/spec mode is one the host enforces (see `bridgedPlanEntered`). */
159
155
  export declare function isBridgedCursorPlanModeActive(sessionKey: string | undefined): boolean;
156
+ /**
157
+ * Cursor plan mode follows the host's `plan` primary agent, as Cursor CLI's
158
+ * mode follows its own mode switch (Shift+Tab, `/plan`).
159
+ *
160
+ * - A turn under the host plan agent puts Cursor in plan mode, however the
161
+ * host got there (its agent picker, a plan_enter approval).
162
+ * - The host's own exit from that agent (a plan_exit approval, or an agent
163
+ * switch in its UI) ends Cursor plan mode. Only an observed `plan` → other
164
+ * transition counts, so Cursor-only modes (debug, ask, …) under the host's
165
+ * ordinary agent are kept; the next Run then carries the agent-mode
166
+ * reminder instead of a stale "plan mode is still active" one.
167
+ */
168
+ export declare function followHostPlanAgent(sessionKey: string | undefined, previousHostAgent: string | undefined, hostAgent: string | undefined): "entered" | "left" | undefined;
169
+ /**
170
+ * `agent.v1.AgentMode` for a Cursor mode id, as Cursor CLI derives
171
+ * `UserMessage.mode` from its current mode: plan, ask, and debug are their own
172
+ * modes; every other mode (agent, spec, triage, …) runs as agent.
173
+ */
174
+ export declare function cursorAgentModeWireValue(modeId: string | undefined): number;
175
+ /**
176
+ * One-shot translation of the host plan agent's workflow into Cursor terms,
177
+ * delivered once per Cursor conversation while that agent owns the turn.
178
+ *
179
+ * With the host's plan file known, Cursor's native CreatePlan carries out the
180
+ * host instruction "write the plan file, then call plan_exit" (see the `exit`
181
+ * CreatePlan bridge), so the model needs no host tool at all. Without it, the
182
+ * model is given `plan_exit`'s exact call shape from the live schema: the
183
+ * RequestContext overlay is names-only, and otherwise it must look the tool up
184
+ * (or guess its arguments) before it can submit the plan.
185
+ */
186
+ export declare function takeHostPlanAgentNote(sessionKey: string | undefined, conversationId: string, hostAgent: string | undefined, tools: ReadonlyArray<{
187
+ name: string;
188
+ inputSchema?: unknown;
189
+ }>, options?: {
190
+ /** The host's own plan file for this session, when the host defines one. */
191
+ hostPlanFile?: string;
192
+ /** MCP server identity the advertised `plan_exit` is called through. */
193
+ server?: string;
194
+ }): string | undefined;
160
195
  export declare function clearActiveCursorMode(sessionKey: string | undefined): void;
161
196
  export declare function resetActiveCursorModesForTests(): void;
162
197
  /**
@@ -168,6 +203,8 @@ export declare function cursorModeSystemReminder(targetModeId: string, options?:
168
203
  firstTurn?: boolean;
169
204
  planExitAdvertised?: boolean;
170
205
  planStageAdvertised?: boolean;
206
+ /** The host `question` tool is advertised, so CreatePlan asks to start implementing. */
207
+ questionAdvertised?: boolean;
171
208
  }): string | undefined;
172
209
  /**
173
210
  * Consume the active-mode reminder for this OpenCode session.
@@ -175,4 +212,6 @@ export declare function cursorModeSystemReminder(targetModeId: string, options?:
175
212
  */
176
213
  export declare function takeActiveCursorModeReminder(sessionKey: string | undefined, options?: {
177
214
  advertisedTools?: ReadonlySet<string> | Iterable<string>;
215
+ /** Host primary agent of this request; when known it, not the catalog, says whether planning ended. */
216
+ hostAgent?: string;
178
217
  }): string | undefined;