cursor-opencode-provider 0.2.3 → 0.2.4
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/README.md +27 -12
- package/dist/activity.d.ts +15 -0
- package/dist/activity.js +76 -0
- package/dist/context/build.js +0 -7
- package/dist/errors.d.ts +59 -0
- package/dist/errors.js +199 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.js +4 -0
- package/dist/language-model.d.ts +47 -4
- package/dist/language-model.js +617 -183
- package/dist/models.d.ts +7 -0
- package/dist/models.js +259 -79
- package/dist/plugin.js +35 -2
- package/dist/protocol/exec-variants.d.ts +24 -0
- package/dist/protocol/exec-variants.js +55 -0
- package/dist/protocol/messages.js +105 -25
- package/dist/protocol/request.d.ts +0 -2
- package/dist/protocol/request.js +0 -8
- package/dist/protocol/struct.js +1 -1
- package/dist/protocol/tool-call-bridge.js +2 -0
- package/dist/protocol/tools.d.ts +8 -1
- package/dist/protocol/tools.js +258 -32
- package/dist/session.d.ts +115 -8
- package/dist/session.js +362 -31
- package/dist/shell-timeout.d.ts +50 -0
- package/dist/shell-timeout.js +179 -0
- package/dist/transport/connect.d.ts +37 -1
- package/dist/transport/connect.js +679 -115
- package/package.json +6 -1
package/dist/protocol/tools.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { encodeMessage } from "./messages.js";
|
|
2
2
|
import { encodeJsonAsValue, decodeStructEntriesToJson, readAllFields } from "./struct.js";
|
|
3
3
|
import { trace } from "../debug.js";
|
|
4
|
+
import { cursorExecVariantByRequestName } from "./exec-variants.js";
|
|
4
5
|
// Exec variant field number whose reply is the server-initiated request_context
|
|
5
6
|
// probe (ExecServerMessage #10 → ExecClientMessage #10). request/result share a
|
|
6
7
|
// field number for every exec variant, so this is also the result field.
|
|
@@ -148,6 +149,8 @@ const cursorToolToOpencode = {
|
|
|
148
149
|
ls_args: "read",
|
|
149
150
|
delete_args: "bash",
|
|
150
151
|
shell_stream_args: "bash",
|
|
152
|
+
background_shell_spawn_args: "bash",
|
|
153
|
+
subagent_args: "task",
|
|
151
154
|
mcp_args: "mcp",
|
|
152
155
|
};
|
|
153
156
|
const opencodeToolToCursor = {
|
|
@@ -155,6 +158,7 @@ const opencodeToolToCursor = {
|
|
|
155
158
|
write: "write_args",
|
|
156
159
|
grep: "grep_args",
|
|
157
160
|
bash: "shell_stream_args",
|
|
161
|
+
task: "subagent_args",
|
|
158
162
|
mcp: "mcp_args",
|
|
159
163
|
};
|
|
160
164
|
/** Cursor-only fields that must not be forwarded as OpenCode tool input. */
|
|
@@ -203,26 +207,7 @@ export function mapExecServerToToolName(execField) {
|
|
|
203
207
|
export function mapToolNameToExecField(toolName) {
|
|
204
208
|
return opencodeToolToCursor[toolName];
|
|
205
209
|
}
|
|
206
|
-
|
|
207
|
-
// must reply with. This is keyed off the REQUEST variant, not the opencode tool
|
|
208
|
-
// name, because an MCP call must always answer with `mcp_result` even though the
|
|
209
|
-
// resolved tool name (e.g. "read") looks like a built-in.
|
|
210
|
-
const execVariantToResultField = {
|
|
211
|
-
read_args: "read_result",
|
|
212
|
-
write_args: "write_result",
|
|
213
|
-
pi_read_args: "pi_read_result",
|
|
214
|
-
pi_bash_args: "pi_bash_result",
|
|
215
|
-
pi_edit_args: "pi_edit_result",
|
|
216
|
-
pi_write_args: "pi_write_result",
|
|
217
|
-
pi_grep_args: "pi_grep_result",
|
|
218
|
-
pi_find_args: "pi_find_result",
|
|
219
|
-
pi_ls_args: "pi_ls_result",
|
|
220
|
-
grep_args: "grep_result",
|
|
221
|
-
ls_args: "ls_result",
|
|
222
|
-
delete_args: "delete_result",
|
|
223
|
-
shell_stream_args: "shell_stream",
|
|
224
|
-
mcp_args: "mcp_result",
|
|
225
|
-
};
|
|
210
|
+
const BACKGROUND_SHELL_MARKER = "__CURSOR_BACKGROUND_SHELL__";
|
|
226
211
|
export function parseExecServerMessage(msg) {
|
|
227
212
|
const id = msg.id;
|
|
228
213
|
if (id === undefined)
|
|
@@ -233,12 +218,71 @@ export function parseExecServerMessage(msg) {
|
|
|
233
218
|
"pi_read_args", "pi_bash_args", "pi_edit_args", "pi_write_args",
|
|
234
219
|
"pi_grep_args", "pi_find_args", "pi_ls_args",
|
|
235
220
|
"grep_args", "ls_args",
|
|
236
|
-
"delete_args", "shell_stream_args", "mcp_args",
|
|
221
|
+
"delete_args", "shell_stream_args", "background_shell_spawn_args", "mcp_args",
|
|
222
|
+
"subagent_args",
|
|
237
223
|
]);
|
|
238
224
|
if (!execVariant)
|
|
239
225
|
return undefined;
|
|
240
|
-
|
|
226
|
+
// Use the complete canonical request/result table. In particular, Pi request
|
|
227
|
+
// fields #45..#51 pair with result fields #46..#52 rather than matching ids.
|
|
228
|
+
const resultField = cursorExecVariantByRequestName(execVariant)?.resultName;
|
|
229
|
+
if (!resultField)
|
|
230
|
+
return undefined;
|
|
241
231
|
const execId = msg.exec_id ?? "";
|
|
232
|
+
if (execVariant === "background_shell_spawn_args") {
|
|
233
|
+
const raw = msg.background_shell_spawn_args ?? {};
|
|
234
|
+
const command = str(raw.command);
|
|
235
|
+
const workingDirectory = str(raw.working_directory) ?? "";
|
|
236
|
+
const args = {};
|
|
237
|
+
if (command)
|
|
238
|
+
args.command = buildBackgroundShellCommand(command);
|
|
239
|
+
if (workingDirectory)
|
|
240
|
+
args.workdir = workingDirectory;
|
|
241
|
+
return {
|
|
242
|
+
id,
|
|
243
|
+
execId,
|
|
244
|
+
toolName: "bash",
|
|
245
|
+
args,
|
|
246
|
+
resultField,
|
|
247
|
+
resultMetadata: { command: command ?? "", working_directory: workingDirectory },
|
|
248
|
+
localError: raw.enable_write_shell_stdin_tool === true
|
|
249
|
+
? "Interactive background shells are not available through OpenCode's bash tool."
|
|
250
|
+
: command
|
|
251
|
+
? undefined
|
|
252
|
+
: "Cursor background shell request is missing a command.",
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
if (execVariant === "subagent_args") {
|
|
256
|
+
const raw = msg.subagent_args ?? {};
|
|
257
|
+
const prompt = str(raw.prompt);
|
|
258
|
+
const cursorSubagentType = str(raw.subagent_type);
|
|
259
|
+
const subagentType = cursorSubagentType
|
|
260
|
+
? mapCursorSubagentTypeToOpenCode(cursorSubagentType)
|
|
261
|
+
: undefined;
|
|
262
|
+
const args = {
|
|
263
|
+
description: describeSubagentTask(prompt, subagentType),
|
|
264
|
+
prompt: prompt ?? "",
|
|
265
|
+
subagent_type: subagentType ?? "",
|
|
266
|
+
};
|
|
267
|
+
const resumeAgentId = str(raw.resume_agent_id);
|
|
268
|
+
if (resumeAgentId)
|
|
269
|
+
args.task_id = resumeAgentId;
|
|
270
|
+
// protobufjs materializes an absent proto3 optional bool as false in this
|
|
271
|
+
// reflection schema. OpenCode's foreground default is already false, so
|
|
272
|
+
// only forward the meaningful opt-in value.
|
|
273
|
+
if (raw.run_in_background === true)
|
|
274
|
+
args.background = true;
|
|
275
|
+
return {
|
|
276
|
+
id,
|
|
277
|
+
execId,
|
|
278
|
+
toolName: "task",
|
|
279
|
+
args,
|
|
280
|
+
resultField,
|
|
281
|
+
localError: prompt && cursorSubagentType
|
|
282
|
+
? undefined
|
|
283
|
+
: "Cursor subagent request is missing a required prompt or subagent type.",
|
|
284
|
+
};
|
|
285
|
+
}
|
|
242
286
|
if (execVariant === "mcp_args") {
|
|
243
287
|
// An MCP call to one of the tools we advertised. Resolve the real opencode
|
|
244
288
|
// tool name (Cursor's model may have shortened "opencode-read" → "read") and
|
|
@@ -284,12 +328,43 @@ export function parseExecServerMessage(msg) {
|
|
|
284
328
|
if (!toolName)
|
|
285
329
|
return undefined;
|
|
286
330
|
const mapped = mapCursorArgsToOpencode(toolName, msg[execVariant] ?? {}, execVariant);
|
|
331
|
+
const resultMetadata = execVariant === "shell_stream_args"
|
|
332
|
+
? shellStreamResultMetadata(msg[execVariant] ?? {})
|
|
333
|
+
: undefined;
|
|
334
|
+
if (resultMetadata
|
|
335
|
+
&& resultMetadata.timeout_behavior !== 2
|
|
336
|
+
&& typeof resultMetadata.timeout_ms === "number") {
|
|
337
|
+
// Cursor's protobuf default (timeout=0) means 30 seconds for an ordinary
|
|
338
|
+
// foreground shell. OpenCode instead treats zero literally, so pass the
|
|
339
|
+
// effective native value rather than the raw protobuf default.
|
|
340
|
+
mapped.args.timeout = resultMetadata.timeout_ms;
|
|
341
|
+
}
|
|
287
342
|
return {
|
|
288
343
|
id,
|
|
289
344
|
execId,
|
|
290
345
|
toolName: mapped.toolName,
|
|
291
346
|
args: mapped.args,
|
|
292
347
|
resultField,
|
|
348
|
+
...(resultMetadata ? { resultMetadata } : {}),
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
function shellStreamResultMetadata(raw) {
|
|
352
|
+
const timeout = num(raw.timeout) ?? 0;
|
|
353
|
+
const timeoutBehavior = num(raw.timeout_behavior) ?? 0;
|
|
354
|
+
const hardTimeout = num(raw.hard_timeout);
|
|
355
|
+
// Cursor CLI: a nonzero timeout is used verbatim. A zero foreground timeout
|
|
356
|
+
// defaults to 30s; zero with background/hard-timeout semantics means an
|
|
357
|
+
// immediate soft handoff governed by the separate hard deadline.
|
|
358
|
+
const effectiveTimeout = timeout !== 0
|
|
359
|
+
? timeout
|
|
360
|
+
: (timeoutBehavior === 2 || (hardTimeout !== undefined && hardTimeout > 0) ? 0 : 30_000);
|
|
361
|
+
return {
|
|
362
|
+
shell_stream: true,
|
|
363
|
+
command: str(raw.command) ?? "",
|
|
364
|
+
working_directory: str(raw.working_directory) ?? "",
|
|
365
|
+
timeout_ms: effectiveTimeout,
|
|
366
|
+
timeout_behavior: timeoutBehavior,
|
|
367
|
+
...(hardTimeout !== undefined && hardTimeout > 0 ? { hard_timeout_ms: hardTimeout } : {}),
|
|
293
368
|
};
|
|
294
369
|
}
|
|
295
370
|
/**
|
|
@@ -425,9 +500,42 @@ function num(v) {
|
|
|
425
500
|
return Number(v);
|
|
426
501
|
return undefined;
|
|
427
502
|
}
|
|
503
|
+
function describeSubagentTask(prompt, subagentType) {
|
|
504
|
+
const words = prompt?.replace(/\s+/g, " ").trim().split(" ").filter(Boolean).slice(0, 5);
|
|
505
|
+
if (words?.length)
|
|
506
|
+
return words.join(" ");
|
|
507
|
+
return `${subagentType || "Delegated"} task`;
|
|
508
|
+
}
|
|
509
|
+
function mapCursorSubagentTypeToOpenCode(subagentType) {
|
|
510
|
+
// Cursor's built-in general agent uses a camelCase protocol identifier, and
|
|
511
|
+
// its native Bugbot reviewer has no OpenCode-specific agent definition.
|
|
512
|
+
// OpenCode `general` is the general-purpose equivalent; `explore` preserves
|
|
513
|
+
// Bugbot's review-only/read-oriented semantics while retaining grep/read/bash.
|
|
514
|
+
// Preserve every other value so `explore` and configured custom agents keep
|
|
515
|
+
// their exact advertised names.
|
|
516
|
+
if (subagentType === "generalPurpose")
|
|
517
|
+
return "general";
|
|
518
|
+
if (subagentType === "bugbot")
|
|
519
|
+
return "explore";
|
|
520
|
+
return subagentType;
|
|
521
|
+
}
|
|
428
522
|
function shellQuote(s) {
|
|
429
523
|
return `'${s.replace(/'/g, `'\\''`)}'`;
|
|
430
524
|
}
|
|
525
|
+
/**
|
|
526
|
+
* OpenCode's bash tool is foreground-only. Detach the requested command inside
|
|
527
|
+
* that one foreground call and print a private marker containing the spawned
|
|
528
|
+
* PID and log path. With stdin and all output redirected, the host shell can
|
|
529
|
+
* return immediately instead of retaining OpenCode's tool pipe.
|
|
530
|
+
*/
|
|
531
|
+
function buildBackgroundShellCommand(command) {
|
|
532
|
+
return [
|
|
533
|
+
'bg_log="$(mktemp "${TMPDIR:-/tmp}/cursor-opencode-bg.XXXXXX")" || exit 1',
|
|
534
|
+
`nohup sh -c ${shellQuote(command)} >"$bg_log" 2>&1 </dev/null &`,
|
|
535
|
+
"bg_pid=$!",
|
|
536
|
+
`printf '${BACKGROUND_SHELL_MARKER}%s:%s\\n' "$bg_pid" "$bg_log"`,
|
|
537
|
+
].join("\n");
|
|
538
|
+
}
|
|
431
539
|
/**
|
|
432
540
|
* Map Cursor McpArgs back to the OpenCode tool id.
|
|
433
541
|
* Prefers provider_identifier + bare tool_name (github + create_pull_request
|
|
@@ -505,9 +613,32 @@ export function buildExecClientMessages(input) {
|
|
|
505
613
|
if (input.output) {
|
|
506
614
|
frames.push(encodeShellStream(input.execId, undefined, { stdout: { data: input.output } }));
|
|
507
615
|
}
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
616
|
+
if (input.shellOutcome?.kind === "backgrounded") {
|
|
617
|
+
frames.push(encodeShellStream(input.execId, input.executionTimeMs, {
|
|
618
|
+
backgrounded: {
|
|
619
|
+
shell_id: input.shellOutcome.shellId,
|
|
620
|
+
command: input.shellOutcome.command,
|
|
621
|
+
working_directory: input.shellOutcome.workingDirectory,
|
|
622
|
+
pid: input.shellOutcome.pid,
|
|
623
|
+
ms_to_wait: input.shellOutcome.msToWait,
|
|
624
|
+
reason: input.shellOutcome.reason,
|
|
625
|
+
},
|
|
626
|
+
}));
|
|
627
|
+
}
|
|
628
|
+
else if (input.shellOutcome?.kind === "timeout") {
|
|
629
|
+
frames.push(encodeShellStream(input.execId, input.executionTimeMs, {
|
|
630
|
+
// Native CLI represents timeout structurally. ShellAbortReason.TIMEOUT=2.
|
|
631
|
+
exit: { code: 0, aborted: true, abort_reason: 2 },
|
|
632
|
+
}));
|
|
633
|
+
}
|
|
634
|
+
else {
|
|
635
|
+
const exitCode = input.shellOutcome?.kind === "exit"
|
|
636
|
+
? Math.max(0, Math.min(0xffff_ffff, input.shellOutcome.code))
|
|
637
|
+
: 0;
|
|
638
|
+
frames.push(encodeShellStream(input.execId, input.executionTimeMs, {
|
|
639
|
+
exit: { code: exitCode, aborted: false },
|
|
640
|
+
}));
|
|
641
|
+
}
|
|
511
642
|
}
|
|
512
643
|
}
|
|
513
644
|
else {
|
|
@@ -515,7 +646,7 @@ export function buildExecClientMessages(input) {
|
|
|
515
646
|
id: input.execId,
|
|
516
647
|
local_execution_time_ms: input.executionTimeMs ?? 0,
|
|
517
648
|
};
|
|
518
|
-
clientMsg[resultField] = buildTypedExecResult(resultField, input.output, input.error, input.toolName);
|
|
649
|
+
clientMsg[resultField] = buildTypedExecResult(resultField, input.output, input.error, input.toolName, input.resultMetadata);
|
|
519
650
|
frames.push(encodeMessage("AgentClientMessage", {
|
|
520
651
|
exec_client_message: clientMsg,
|
|
521
652
|
}));
|
|
@@ -602,7 +733,7 @@ export function unwrapReadOutput(output) {
|
|
|
602
733
|
* OpenCode returns free-form text; we wrap it in the minimal success shape the
|
|
603
734
|
* server accepts (verified against agent.v1 wire captures).
|
|
604
735
|
*/
|
|
605
|
-
export function buildTypedExecResult(resultField, output, error, toolName) {
|
|
736
|
+
export function buildTypedExecResult(resultField, output, error, toolName, resultMetadata) {
|
|
606
737
|
switch (resultField) {
|
|
607
738
|
case "read_result":
|
|
608
739
|
if (error)
|
|
@@ -673,6 +804,31 @@ export function buildTypedExecResult(resultField, output, error, toolName) {
|
|
|
673
804
|
if (error)
|
|
674
805
|
return { error: { path: "", error } };
|
|
675
806
|
return { success: { path: "", deleted_file: "" } };
|
|
807
|
+
case "background_shell_spawn_result": {
|
|
808
|
+
const command = str(resultMetadata?.command) ?? "";
|
|
809
|
+
const workingDirectory = str(resultMetadata?.working_directory) ?? "";
|
|
810
|
+
if (error)
|
|
811
|
+
return { error: { command, working_directory: workingDirectory, error } };
|
|
812
|
+
const match = new RegExp(`${BACKGROUND_SHELL_MARKER}(\\d+):([^\\r\\n]+)`).exec(output);
|
|
813
|
+
const pid = match ? Number(match[1]) : 0;
|
|
814
|
+
if (!Number.isSafeInteger(pid) || pid <= 0 || pid > 0xffff_ffff) {
|
|
815
|
+
return {
|
|
816
|
+
error: {
|
|
817
|
+
command,
|
|
818
|
+
working_directory: workingDirectory,
|
|
819
|
+
error: "OpenCode did not return a valid background shell process id.",
|
|
820
|
+
},
|
|
821
|
+
};
|
|
822
|
+
}
|
|
823
|
+
return {
|
|
824
|
+
success: {
|
|
825
|
+
shell_id: pid,
|
|
826
|
+
command,
|
|
827
|
+
working_directory: workingDirectory,
|
|
828
|
+
pid,
|
|
829
|
+
},
|
|
830
|
+
};
|
|
831
|
+
}
|
|
676
832
|
case "ls_result": {
|
|
677
833
|
if (error)
|
|
678
834
|
return { error: { path: "", error } };
|
|
@@ -706,6 +862,27 @@ export function buildTypedExecResult(resultField, output, error, toolName) {
|
|
|
706
862
|
is_error: false,
|
|
707
863
|
},
|
|
708
864
|
};
|
|
865
|
+
case "subagent_result": {
|
|
866
|
+
const task = parseOpenCodeTaskOutput(output);
|
|
867
|
+
if (error || task.state === "error") {
|
|
868
|
+
return {
|
|
869
|
+
error: {
|
|
870
|
+
...(task.agentId ? { agent_id: task.agentId } : {}),
|
|
871
|
+
error: error ?? task.message ?? output,
|
|
872
|
+
},
|
|
873
|
+
};
|
|
874
|
+
}
|
|
875
|
+
return {
|
|
876
|
+
success: {
|
|
877
|
+
agent_id: task.agentId ?? "",
|
|
878
|
+
...(task.message !== undefined ? { final_message: task.message } : {}),
|
|
879
|
+
tool_call_count: 0,
|
|
880
|
+
// OpenCode marks an asynchronous launch as state="running". Cursor's
|
|
881
|
+
// canonical USER_REQUEST enum value is 2; foreground/default is 0.
|
|
882
|
+
background_reason: task.state === "running" ? 2 : 0,
|
|
883
|
+
},
|
|
884
|
+
};
|
|
885
|
+
}
|
|
709
886
|
default:
|
|
710
887
|
// Unknown variant: best-effort success wrapper so the server sees a oneof.
|
|
711
888
|
if (error)
|
|
@@ -713,6 +890,25 @@ export function buildTypedExecResult(resultField, output, error, toolName) {
|
|
|
713
890
|
return { success: { content: output } };
|
|
714
891
|
}
|
|
715
892
|
}
|
|
893
|
+
function parseOpenCodeTaskOutput(output) {
|
|
894
|
+
// Attribute order is not guaranteed; accept id/state in either order and
|
|
895
|
+
// ignore additional attributes OpenCode may emit on the <task> open tag.
|
|
896
|
+
const open = /<task\b([^>]*)>/i.exec(output);
|
|
897
|
+
if (!open)
|
|
898
|
+
return { message: output };
|
|
899
|
+
const attrs = open[1];
|
|
900
|
+
const agentId = /\bid="([^"]+)"/i.exec(attrs)?.[1];
|
|
901
|
+
const state = /\bstate="(running|completed|error)"/i.exec(attrs)?.[1];
|
|
902
|
+
if (!agentId || !state)
|
|
903
|
+
return { message: output };
|
|
904
|
+
const tag = state === "error" ? "task_error" : "task_result";
|
|
905
|
+
const body = new RegExp(`<${tag}>\\n?([\\s\\S]*?)\\n?</${tag}>`, "i").exec(output);
|
|
906
|
+
return {
|
|
907
|
+
agentId,
|
|
908
|
+
state,
|
|
909
|
+
message: body?.[1] ?? output,
|
|
910
|
+
};
|
|
911
|
+
}
|
|
716
912
|
function extractPathTag(output) {
|
|
717
913
|
const m = output.match(/<path>([^<]+)<\/path>/);
|
|
718
914
|
return m?.[1];
|
|
@@ -824,16 +1020,28 @@ export function buildMcpStateResult(execId, args, requestContext) {
|
|
|
824
1020
|
? fsOptions.mcp_descriptors.map(recordValue).filter((d) => !!d)
|
|
825
1021
|
: [];
|
|
826
1022
|
const descriptors = nested.length > 0 ? nested : descriptorsFromFlatTools(requestContext.tools);
|
|
1023
|
+
const flatTools = Array.isArray(requestContext.tools)
|
|
1024
|
+
? requestContext.tools.map(recordValue).filter((tool) => !!tool)
|
|
1025
|
+
: [];
|
|
827
1026
|
const servers = descriptors
|
|
828
1027
|
.filter((descriptor) => {
|
|
829
1028
|
const id = stringValue(descriptor.server_identifier);
|
|
830
1029
|
return requested.size === 0 || (id !== undefined && requested.has(id));
|
|
831
1030
|
})
|
|
832
|
-
.map((descriptor) =>
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
1031
|
+
.map((descriptor) => {
|
|
1032
|
+
const serverIdentifier = stringValue(descriptor.server_identifier) ?? stringValue(descriptor.server_name) ?? "";
|
|
1033
|
+
const tools = Array.isArray(descriptor.tools)
|
|
1034
|
+
? descriptor.tools
|
|
1035
|
+
.map(recordValue)
|
|
1036
|
+
.filter((tool) => !!tool)
|
|
1037
|
+
.map((tool) => mcpStateToolDefinition(serverIdentifier, tool, flatTools))
|
|
1038
|
+
: [];
|
|
1039
|
+
return {
|
|
1040
|
+
server_name: stringValue(descriptor.server_name) ?? serverIdentifier,
|
|
1041
|
+
server_identifier: serverIdentifier,
|
|
1042
|
+
tools,
|
|
1043
|
+
};
|
|
1044
|
+
});
|
|
837
1045
|
return encodeMessage("AgentClientMessage", {
|
|
838
1046
|
exec_client_message: {
|
|
839
1047
|
id: execId,
|
|
@@ -841,6 +1049,24 @@ export function buildMcpStateResult(execId, args, requestContext) {
|
|
|
841
1049
|
},
|
|
842
1050
|
});
|
|
843
1051
|
}
|
|
1052
|
+
/**
|
|
1053
|
+
* Exec #36 uses McpToolDefinition, not the narrower McpToolDescriptor used by
|
|
1054
|
+
* RequestContext's filesystem/meta-tool catalogs. Rehydrate the full identity
|
|
1055
|
+
* from RequestContext.tools so Cursor's native get_mcp_tools can correlate the
|
|
1056
|
+
* discovered definition with the later provider_identifier/tool_name request.
|
|
1057
|
+
*/
|
|
1058
|
+
function mcpStateToolDefinition(serverIdentifier, descriptor, flatTools) {
|
|
1059
|
+
const toolName = stringValue(descriptor.tool_name) ?? "";
|
|
1060
|
+
const advertised = flatTools.find((tool) => stringValue(tool.provider_identifier) === serverIdentifier
|
|
1061
|
+
&& stringValue(tool.tool_name) === toolName);
|
|
1062
|
+
return {
|
|
1063
|
+
name: stringValue(advertised?.name) ?? `${serverIdentifier}-${toolName}`,
|
|
1064
|
+
description: stringValue(advertised?.description) ?? stringValue(descriptor.description) ?? "",
|
|
1065
|
+
input_schema: advertised?.input_schema ?? descriptor.input_schema,
|
|
1066
|
+
provider_identifier: serverIdentifier,
|
|
1067
|
+
tool_name: toolName,
|
|
1068
|
+
};
|
|
1069
|
+
}
|
|
844
1070
|
function recordValue(value) {
|
|
845
1071
|
return value && typeof value === "object" && !Array.isArray(value)
|
|
846
1072
|
? value
|
package/dist/session.d.ts
CHANGED
|
@@ -1,8 +1,26 @@
|
|
|
1
1
|
import type { BidiStream } from "./transport/connect.js";
|
|
2
|
+
import { type CursorProviderError } from "./errors.js";
|
|
3
|
+
import { type SessionActivitySource } from "./activity.js";
|
|
2
4
|
export type Frame = {
|
|
3
5
|
flags: number;
|
|
4
6
|
payload: Uint8Array;
|
|
5
7
|
};
|
|
8
|
+
export type CursorContinuationOptions = {
|
|
9
|
+
semanticIdleMs?: number;
|
|
10
|
+
/** @deprecated Use semanticIdleMs. Kept as a strict alias for compatibility. */
|
|
11
|
+
softHealthMs?: number;
|
|
12
|
+
/** Pending-tool inactivity window, renewed by OpenCode session progress. */
|
|
13
|
+
hardCapMs?: number;
|
|
14
|
+
heartbeatMs?: number;
|
|
15
|
+
};
|
|
16
|
+
export type CursorContinuationPolicy = {
|
|
17
|
+
semanticIdleMs: number;
|
|
18
|
+
hardCapMs: number;
|
|
19
|
+
heartbeatMs: number;
|
|
20
|
+
};
|
|
21
|
+
export declare const DEFAULT_CONTINUATION_POLICY: Readonly<CursorContinuationPolicy>;
|
|
22
|
+
export declare function resolveContinuationPolicy(options: CursorContinuationOptions | undefined): CursorContinuationPolicy;
|
|
23
|
+
export type PendingExecState = "pending" | "claimed" | "delivered";
|
|
6
24
|
/**
|
|
7
25
|
* A held-open Run stream. Cursor drives the agentic loop server-side and
|
|
8
26
|
* expects tool results on the SAME bidi stream. opencode, by contrast, owns
|
|
@@ -14,11 +32,16 @@ export type Frame = {
|
|
|
14
32
|
export type PendingExec = {
|
|
15
33
|
/** ExecClientMessage result field to reply with (matches the request variant). */
|
|
16
34
|
resultField: string;
|
|
35
|
+
state: PendingExecState;
|
|
36
|
+
registeredAt: number;
|
|
37
|
+
hardDeadlineAt: number;
|
|
17
38
|
/**
|
|
18
39
|
* Resolved opencode tool name (read/write/grep/…). Used on continuation so
|
|
19
40
|
* mcp_result can unwrap read envelopes even if the prompt omits toolName.
|
|
20
41
|
*/
|
|
21
42
|
toolName?: string;
|
|
43
|
+
/** Original request fields required by a typed result message. */
|
|
44
|
+
resultMetadata?: Record<string, unknown>;
|
|
22
45
|
/**
|
|
23
46
|
* True when this pending entry was synthesized from a Cursor display-only
|
|
24
47
|
* tool_call_* frame (no ExecServerMessage). Continuation must not write an
|
|
@@ -26,6 +49,43 @@ export type PendingExec = {
|
|
|
26
49
|
*/
|
|
27
50
|
bridged?: boolean;
|
|
28
51
|
};
|
|
52
|
+
export type ContinuationTerminalReason = "hard-cap-expired" | "remote-clean-close" | "remote-error" | "result-write-failed" | "ambiguous-partial-write" | "heartbeat-write-failed" | "reply-write-failed" | "process-disposed";
|
|
53
|
+
export type SessionCloseReason = ContinuationTerminalReason | "ordinary-cleanup" | "turn-ended" | "initial-write-failed";
|
|
54
|
+
export type ContinuationClaim = {
|
|
55
|
+
session: CursorSession;
|
|
56
|
+
execId: number;
|
|
57
|
+
pending: PendingExec;
|
|
58
|
+
};
|
|
59
|
+
export type ContinuationClassification = {
|
|
60
|
+
kind: "deliverable";
|
|
61
|
+
session: CursorSession;
|
|
62
|
+
pending: PendingExec;
|
|
63
|
+
} | {
|
|
64
|
+
kind: "duplicate";
|
|
65
|
+
reason: "in-flight" | "delivered";
|
|
66
|
+
} | {
|
|
67
|
+
kind: "terminal";
|
|
68
|
+
reason: ContinuationTerminalReason;
|
|
69
|
+
} | {
|
|
70
|
+
kind: "missing";
|
|
71
|
+
reason: "missing-process-local-state";
|
|
72
|
+
};
|
|
73
|
+
export type DeliveryOutcome = {
|
|
74
|
+
kind: "delivered";
|
|
75
|
+
framesWritten: number;
|
|
76
|
+
} | {
|
|
77
|
+
kind: "duplicate";
|
|
78
|
+
reason: "in-flight" | "delivered";
|
|
79
|
+
framesWritten: 0;
|
|
80
|
+
} | {
|
|
81
|
+
kind: "terminal";
|
|
82
|
+
reason: ContinuationTerminalReason;
|
|
83
|
+
framesWritten: number;
|
|
84
|
+
} | {
|
|
85
|
+
kind: "missing";
|
|
86
|
+
reason: "missing-process-local-state";
|
|
87
|
+
framesWritten: 0;
|
|
88
|
+
};
|
|
29
89
|
export type CursorSession = {
|
|
30
90
|
/**
|
|
31
91
|
* Stable per-Run-stream id (distinct from Cursor's own conversation_id).
|
|
@@ -36,8 +96,10 @@ export type CursorSession = {
|
|
|
36
96
|
/**
|
|
37
97
|
* Cursor conversation_id for this Run — used to store/echo
|
|
38
98
|
* conversation_checkpoint_update (CLI parity).
|
|
39
|
-
|
|
99
|
+
*/
|
|
40
100
|
conversationId: string;
|
|
101
|
+
/** OpenCode session whose own or descendant activity renews tool leases. */
|
|
102
|
+
openCodeSessionId?: string;
|
|
41
103
|
stream: BidiStream;
|
|
42
104
|
frames: AsyncIterator<Frame>;
|
|
43
105
|
pending: Map<number, PendingExec>;
|
|
@@ -77,26 +139,62 @@ export type CursorSession = {
|
|
|
77
139
|
* Prevents a late cancel/abort from a prior ReadableStream from destroying
|
|
78
140
|
* the Run connection after tool results were delivered (pending cleared) but
|
|
79
141
|
* Cursor is still generating.
|
|
80
|
-
|
|
142
|
+
*/
|
|
81
143
|
pumpActive: boolean;
|
|
144
|
+
/** The active pull owner; stale cancel callbacks cannot affect a newer pump. */
|
|
145
|
+
pumpOwner: symbol | null;
|
|
82
146
|
heartbeat: ReturnType<typeof setInterval> | null;
|
|
83
|
-
|
|
147
|
+
heartbeatCancel: (() => void) | null;
|
|
148
|
+
hardDeadlineTimer: ReturnType<typeof setTimeout> | null;
|
|
149
|
+
semanticDeadlineCancel: (() => void) | null;
|
|
150
|
+
terminalUnsubscribe: (() => void) | null;
|
|
151
|
+
deferredTerminalReason: "remote-clean-close" | "remote-error" | null;
|
|
152
|
+
policy: CursorContinuationPolicy;
|
|
153
|
+
createdAt: number;
|
|
154
|
+
lastInboundAt: number;
|
|
155
|
+
lastHeartbeatWriteAt: number;
|
|
156
|
+
semanticDeadlineAt: number;
|
|
157
|
+
closeError: CursorProviderError | null;
|
|
158
|
+
closed: boolean;
|
|
159
|
+
};
|
|
160
|
+
type SessionManagerOptions = {
|
|
161
|
+
now?: () => number;
|
|
162
|
+
setTimer?: (callback: () => void, delayMs: number) => ReturnType<typeof setTimeout>;
|
|
163
|
+
clearTimer?: (timer: ReturnType<typeof setTimeout>) => void;
|
|
164
|
+
activitySource?: SessionActivitySource;
|
|
165
|
+
tombstoneTtlMs?: number;
|
|
166
|
+
tombstoneLimit?: number;
|
|
84
167
|
};
|
|
85
168
|
export declare class SessionManager {
|
|
86
169
|
private byExecId;
|
|
87
|
-
private
|
|
88
|
-
|
|
89
|
-
|
|
170
|
+
private sessions;
|
|
171
|
+
private tombstones;
|
|
172
|
+
private readonly now;
|
|
173
|
+
private readonly setTimer;
|
|
174
|
+
private readonly clearTimer;
|
|
175
|
+
private readonly activitySource;
|
|
176
|
+
private readonly tombstoneTtlMs;
|
|
177
|
+
private readonly tombstoneLimit;
|
|
178
|
+
constructor(options?: SessionManagerOptions);
|
|
179
|
+
registerSession(session: CursorSession): void;
|
|
180
|
+
recordSemanticProgress(session: CursorSession, at?: number): void;
|
|
181
|
+
recordHeartbeatWrite(session: CursorSession): void;
|
|
90
182
|
/** Register that `session` is awaiting a result for `execId`. */
|
|
91
|
-
registerPending(execId: number, session: CursorSession, resultField: string, toolName?: string, bridged?: boolean): void;
|
|
183
|
+
registerPending(execId: number, session: CursorSession, resultField: string, toolName?: string, bridged?: boolean, resultMetadata?: Record<string, unknown>): void;
|
|
92
184
|
/** The pending exec info for an id on a specific session, if still awaiting it. */
|
|
93
185
|
pendingFor(sessionId: string, execId: number): PendingExec | undefined;
|
|
186
|
+
classify(sessionId: string, execId: number): ContinuationClassification;
|
|
187
|
+
claim(sessionId: string, execId: number): ContinuationClaim | ContinuationClassification;
|
|
188
|
+
deliverClaim(claim: ContinuationClaim, frames: readonly Uint8Array[]): DeliveryOutcome;
|
|
94
189
|
/** Find the live session awaiting one of the given exec ids. */
|
|
95
190
|
findByExecIds(sessionId: string, execIds: number[]): CursorSession | undefined;
|
|
96
191
|
/** Mark an exec id as resolved (its result has been delivered). */
|
|
97
192
|
resolve(sessionId: string, execId: number): void;
|
|
193
|
+
beginPump(session: CursorSession, owner: symbol): void;
|
|
194
|
+
isPumpOwner(session: CursorSession, owner: symbol): boolean;
|
|
195
|
+
endPump(session: CursorSession, owner: symbol): boolean;
|
|
98
196
|
private key;
|
|
99
|
-
close(session: CursorSession): void;
|
|
197
|
+
close(session: CursorSession, reason?: SessionCloseReason, error?: CursorProviderError): void;
|
|
100
198
|
/**
|
|
101
199
|
* Close only if nothing is awaiting a tool result AND no pull() is actively
|
|
102
200
|
* pumping this session. OpenCode aborts each doStream after finishReason
|
|
@@ -107,5 +205,14 @@ export declare class SessionManager {
|
|
|
107
205
|
*/
|
|
108
206
|
closeUnlessPending(session: CursorSession): boolean;
|
|
109
207
|
dispose(): void;
|
|
208
|
+
sweepHardDeadlines(): void;
|
|
209
|
+
private onStreamTerminal;
|
|
210
|
+
private refreshHardDeadline;
|
|
211
|
+
private hardDeadlineExpired;
|
|
212
|
+
private scheduleHardDeadline;
|
|
213
|
+
private getTombstone;
|
|
214
|
+
private putTombstone;
|
|
215
|
+
private isTerminalReason;
|
|
110
216
|
}
|
|
111
217
|
export declare const sessionManager: SessionManager;
|
|
218
|
+
export {};
|