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.
- package/CHANGELOG.md +79 -0
- package/README.md +28 -9
- package/SECURITY.md +43 -7
- package/dist/context/build.d.ts +22 -4
- package/dist/context/build.js +33 -5
- package/dist/context/dynamic-catalog.js +6 -1
- package/dist/context/frozen.js +6 -2
- package/dist/context/index.d.ts +2 -0
- package/dist/context/index.js +2 -0
- package/dist/context/paths.d.ts +44 -17
- package/dist/context/paths.js +65 -18
- package/dist/context/skills-bridge.d.ts +22 -0
- package/dist/context/skills-bridge.js +20 -0
- package/dist/context/skills.d.ts +34 -0
- package/dist/context/skills.js +211 -0
- package/dist/host-agent-mode.d.ts +53 -4
- package/dist/host-agent-mode.js +117 -13
- package/dist/host-plan-file.d.ts +28 -0
- package/dist/host-plan-file.js +76 -0
- package/dist/image-input.d.ts +18 -0
- package/dist/image-input.js +119 -28
- package/dist/image-save.d.ts +10 -2
- package/dist/image-save.js +33 -21
- package/dist/language-model.d.ts +45 -17
- package/dist/language-model.js +925 -292
- package/dist/model-config.d.ts +9 -0
- package/dist/model-config.js +28 -1
- package/dist/opencode2/catalog.d.ts +1 -0
- package/dist/opencode2/catalog.js +2 -0
- package/dist/opencode2/image-save-tool.d.ts +8 -0
- package/dist/opencode2/image-save-tool.js +84 -0
- package/dist/opencode2/types.d.ts +15 -0
- package/dist/parallel-step.d.ts +65 -0
- package/dist/parallel-step.js +102 -0
- package/dist/plugin-opencode2.js +48 -40
- package/dist/plugin.js +69 -18
- package/dist/pricing-data.d.ts +14 -1
- package/dist/pricing-data.js +14 -1
- package/dist/protocol/ask-question.d.ts +12 -1
- package/dist/protocol/ask-question.js +77 -3
- package/dist/protocol/create-plan.d.ts +47 -25
- package/dist/protocol/create-plan.js +33 -23
- package/dist/protocol/interactions.d.ts +26 -17
- package/dist/protocol/interactions.js +80 -31
- package/dist/protocol/messages.js +74 -14
- package/dist/protocol/request.d.ts +5 -0
- package/dist/protocol/request.js +2 -0
- package/dist/protocol/switch-mode.d.ts +66 -27
- package/dist/protocol/switch-mode.js +147 -73
- package/dist/protocol/token-details.d.ts +2 -0
- package/dist/protocol/token-details.js +11 -1
- package/dist/protocol/tool-call-bridge.d.ts +4 -0
- package/dist/protocol/tool-call-bridge.js +14 -0
- package/dist/protocol/tools.d.ts +16 -1
- package/dist/protocol/tools.js +113 -11
- package/dist/replay-safety.d.ts +6 -0
- package/dist/replay-safety.js +56 -6
- package/dist/session.d.ts +35 -1
- package/dist/session.js +45 -1
- package/dist/todo-store.d.ts +2 -0
- package/dist/todo-store.js +19 -0
- package/dist/transport/connect.d.ts +2 -0
- package/dist/transport/connect.js +23 -6
- package/dist/usage.js +23 -6
- package/package.json +3 -2
- package/dist/plan-execution-kickoff.d.ts +0 -57
- 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,
|
|
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
|
|
80
|
-
*
|
|
81
|
-
*
|
|
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.
|
|
249
|
+
toolName: bridge.toolName,
|
|
247
250
|
},
|
|
248
|
-
// Keep Cursor waiting until the host tool returns
|
|
249
|
-
//
|
|
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
|
|
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.
|
|
32
|
-
//
|
|
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", [
|
|
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
|
-
|
|
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
|
-
|
|
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", [
|
|
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
|
-
|
|
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: "
|
|
1705
|
-
{ id: 2, name: "
|
|
1706
|
-
{ id: 3, name: "
|
|
1707
|
-
{ id: 4, name: "
|
|
1708
|
-
{ id: 5, name: "
|
|
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: "
|
|
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: "
|
|
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).
|
package/dist/protocol/request.js
CHANGED
|
@@ -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`).
|
|
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
|
|
31
|
-
*
|
|
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
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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
|
-
*
|
|
125
|
-
*
|
|
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
|
|
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;
|