@oh-my-pi/pi-ai 17.1.7 → 17.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +64 -0
- package/dist/types/auth-broker/server.d.ts +5 -0
- package/dist/types/auth-broker/types.d.ts +4 -0
- package/dist/types/auth-storage.d.ts +26 -1
- package/dist/types/error/classes.d.ts +5 -1
- package/dist/types/error/rate-limit.d.ts +5 -4
- package/dist/types/providers/anthropic-client.d.ts +13 -1
- package/dist/types/providers/anthropic.d.ts +9 -1
- package/dist/types/providers/cursor/exec-modern.d.ts +98 -0
- package/dist/types/providers/cursor-pi-args.d.ts +99 -0
- package/dist/types/providers/cursor.d.ts +53 -3
- package/dist/types/providers/error-message.d.ts +2 -4
- package/dist/types/providers/openai-codex-responses.d.ts +10 -0
- package/dist/types/providers/openai-responses-wire.d.ts +8 -0
- package/dist/types/providers/openai-shared.d.ts +13 -1
- package/dist/types/registry/exa.d.ts +8 -0
- package/dist/types/registry/registry.d.ts +7 -1
- package/dist/types/registry/xai.d.ts +4 -1
- package/dist/types/stream.d.ts +2 -0
- package/dist/types/types.d.ts +101 -1
- package/dist/types/usage/umans.d.ts +2 -0
- package/dist/types/utils/block-symbols.d.ts +11 -0
- package/dist/types/utils/harmony-leak.d.ts +15 -7
- package/dist/types/utils/schema/normalize.d.ts +1 -0
- package/dist/types/utils.d.ts +8 -0
- package/package.json +4 -4
- package/src/auth-broker/client.ts +5 -1
- package/src/auth-broker/server.ts +103 -9
- package/src/auth-broker/snapshot-cache.ts +12 -3
- package/src/auth-broker/types.ts +6 -0
- package/src/auth-storage.ts +447 -28
- package/src/error/classes.ts +98 -3
- package/src/error/flags.ts +6 -1
- package/src/error/rate-limit.ts +18 -12
- package/src/providers/amazon-bedrock.ts +3 -3
- package/src/providers/anthropic-client.ts +24 -2
- package/src/providers/anthropic.ts +31 -6
- package/src/providers/cursor/exec-modern.ts +495 -0
- package/src/providers/cursor/proto/agent.proto +1007 -0
- package/src/providers/cursor-pi-args.ts +135 -0
- package/src/providers/cursor.ts +1138 -66
- package/src/providers/devin.ts +2 -1
- package/src/providers/error-message.ts +3 -1
- package/src/providers/google-vertex.ts +3 -5
- package/src/providers/openai-codex-responses.ts +109 -27
- package/src/providers/openai-completions.ts +19 -4
- package/src/providers/openai-responses-wire.ts +8 -0
- package/src/providers/openai-responses.ts +12 -1
- package/src/providers/openai-shared.ts +83 -14
- package/src/registry/api-key-validation.ts +18 -34
- package/src/registry/exa.ts +19 -0
- package/src/registry/novita.ts +6 -3
- package/src/registry/registry.ts +3 -1
- package/src/registry/xai.ts +17 -1
- package/src/stream.ts +3 -4
- package/src/types.ts +115 -0
- package/src/usage/umans.ts +192 -0
- package/src/utils/block-symbols.ts +12 -0
- package/src/utils/harmony-leak.ts +32 -0
- package/src/utils/idle-iterator.ts +2 -2
- package/src/utils/schema/normalize.ts +140 -38
- package/src/utils.ts +173 -3
package/src/providers/cursor.ts
CHANGED
|
@@ -10,11 +10,18 @@ import {
|
|
|
10
10
|
AgentRunRequestSchema,
|
|
11
11
|
type AgentServerMessage,
|
|
12
12
|
AgentServerMessageSchema,
|
|
13
|
+
AgentStoreConflictErrorSchema,
|
|
14
|
+
AgentStoreConflictResultSchema,
|
|
13
15
|
AssistantMessageSchema,
|
|
14
16
|
BackgroundShellSpawnResultSchema,
|
|
17
|
+
CanvasDiagnosticsErrorSchema,
|
|
18
|
+
CanvasDiagnosticsResultSchema,
|
|
15
19
|
ClientHeartbeatSchema,
|
|
20
|
+
ComputerUseErrorSchema,
|
|
16
21
|
ComputerUseResultSchema,
|
|
17
22
|
ConversationActionSchema,
|
|
23
|
+
ConversationSearchErrorSchema,
|
|
24
|
+
ConversationSearchResultSchema,
|
|
18
25
|
type ConversationStateStructure,
|
|
19
26
|
ConversationStateStructureSchema,
|
|
20
27
|
ConversationStepSchema,
|
|
@@ -31,9 +38,14 @@ import {
|
|
|
31
38
|
type ExecClientMessage,
|
|
32
39
|
ExecClientMessageSchema,
|
|
33
40
|
ExecClientStreamCloseSchema,
|
|
41
|
+
ExecClientThrowSchema,
|
|
34
42
|
type ExecServerMessage,
|
|
35
43
|
FetchErrorSchema,
|
|
36
44
|
FetchResultSchema,
|
|
45
|
+
ForceBackgroundShellResultSchema,
|
|
46
|
+
ForceBackgroundShellStatus,
|
|
47
|
+
ForceBackgroundSubagentResultSchema,
|
|
48
|
+
ForceBackgroundSubagentStatus,
|
|
37
49
|
GetBlobResultSchema,
|
|
38
50
|
GrepContentMatchSchema,
|
|
39
51
|
GrepContentResultSchema,
|
|
@@ -49,7 +61,11 @@ import {
|
|
|
49
61
|
GrepUnionResultSchema,
|
|
50
62
|
KvClientMessageSchema,
|
|
51
63
|
type KvServerMessage,
|
|
64
|
+
ListMcpResourcesErrorSchema,
|
|
65
|
+
type ListMcpResourcesExecResult,
|
|
66
|
+
ListMcpResourcesExecResult_McpResourceSchema,
|
|
52
67
|
ListMcpResourcesExecResultSchema,
|
|
68
|
+
ListMcpResourcesSuccessSchema,
|
|
53
69
|
type LsDirectoryTreeNode,
|
|
54
70
|
type LsDirectoryTreeNode_File,
|
|
55
71
|
LsDirectoryTreeNode_FileSchema,
|
|
@@ -58,8 +74,11 @@ import {
|
|
|
58
74
|
LsRejectedSchema,
|
|
59
75
|
LsResultSchema,
|
|
60
76
|
LsSuccessSchema,
|
|
77
|
+
McpAllowlistPrecheckResultSchema,
|
|
78
|
+
McpApprovedSchema,
|
|
61
79
|
McpErrorSchema,
|
|
62
80
|
McpImageContentSchema,
|
|
81
|
+
McpRejectedSchema,
|
|
63
82
|
McpResultSchema,
|
|
64
83
|
McpSuccessSchema,
|
|
65
84
|
McpTextContentSchema,
|
|
@@ -68,10 +87,15 @@ import {
|
|
|
68
87
|
McpToolResultContentItemSchema,
|
|
69
88
|
ModelDetailsSchema,
|
|
70
89
|
ReadErrorSchema,
|
|
90
|
+
ReadMcpResourceErrorSchema,
|
|
91
|
+
type ReadMcpResourceExecResult,
|
|
71
92
|
ReadMcpResourceExecResultSchema,
|
|
93
|
+
ReadMcpResourceNotFoundSchema,
|
|
94
|
+
ReadMcpResourceSuccessSchema,
|
|
72
95
|
ReadRejectedSchema,
|
|
73
96
|
ReadResultSchema,
|
|
74
97
|
ReadSuccessSchema,
|
|
98
|
+
RecordScreenFailureSchema,
|
|
75
99
|
RecordScreenResultSchema,
|
|
76
100
|
RequestContextResultSchema,
|
|
77
101
|
RequestContextSchema,
|
|
@@ -81,6 +105,7 @@ import {
|
|
|
81
105
|
SelectedContextSchema,
|
|
82
106
|
SelectedImageSchema,
|
|
83
107
|
SetBlobResultSchema,
|
|
108
|
+
ShellAllowlistPrecheckResultSchema,
|
|
84
109
|
type ShellArgs,
|
|
85
110
|
ShellFailureSchema,
|
|
86
111
|
ShellRejectedSchema,
|
|
@@ -93,8 +118,15 @@ import {
|
|
|
93
118
|
ShellStreamStderrSchema,
|
|
94
119
|
ShellStreamStdoutSchema,
|
|
95
120
|
ShellSuccessSchema,
|
|
121
|
+
SmartModeClassifierErrorSchema,
|
|
122
|
+
SmartModeClassifierResultSchema,
|
|
123
|
+
SubagentAwaitNotFoundSchema,
|
|
124
|
+
SubagentAwaitResultSchema,
|
|
125
|
+
SubagentErrorSchema,
|
|
126
|
+
SubagentResultSchema,
|
|
96
127
|
UserMessageActionSchema,
|
|
97
128
|
UserMessageSchema,
|
|
129
|
+
WebFetchAllowlistPrecheckResultSchema,
|
|
98
130
|
WriteErrorSchema,
|
|
99
131
|
WriteRejectedSchema,
|
|
100
132
|
WriteResultSchema,
|
|
@@ -142,6 +174,7 @@ import {
|
|
|
142
174
|
kCursorExecResolved,
|
|
143
175
|
kStreamingBlockIndex,
|
|
144
176
|
kStreamingBlockKind,
|
|
177
|
+
kStreamingEnvelopeId,
|
|
145
178
|
kStreamingLastParseLen,
|
|
146
179
|
kStreamingPartialJson,
|
|
147
180
|
} from "../utils/block-symbols";
|
|
@@ -150,12 +183,47 @@ import { AssistantMessageEventStream } from "../utils/event-stream";
|
|
|
150
183
|
import { connectProxiedSocket, getProxyForProvider, shouldBypassProxy } from "../utils/proxy";
|
|
151
184
|
import { createRequestDebugSession, isRequestDebugEnabled, type RequestDebugResponseLog } from "../utils/request-debug";
|
|
152
185
|
import { toolWireSchema } from "../utils/schema/wire";
|
|
186
|
+
import {
|
|
187
|
+
buildMcpStateResult,
|
|
188
|
+
buildNeutralHookResult,
|
|
189
|
+
buildPiBashError,
|
|
190
|
+
buildPiBashResult,
|
|
191
|
+
buildPiEditError,
|
|
192
|
+
buildPiEditRejected,
|
|
193
|
+
buildPiEditResult,
|
|
194
|
+
buildPiFindError,
|
|
195
|
+
buildPiFindResult,
|
|
196
|
+
buildPiGrepError,
|
|
197
|
+
buildPiGrepResult,
|
|
198
|
+
buildPiLsError,
|
|
199
|
+
buildPiLsResult,
|
|
200
|
+
buildPiReadError,
|
|
201
|
+
buildPiReadResult,
|
|
202
|
+
buildPiWriteError,
|
|
203
|
+
buildPiWriteRejected,
|
|
204
|
+
buildPiWriteResult,
|
|
205
|
+
piEscapeRegexLiteral,
|
|
206
|
+
piGrepSkip,
|
|
207
|
+
piJoinPath,
|
|
208
|
+
piLimit,
|
|
209
|
+
piLsPath,
|
|
210
|
+
piReadDisplayPath,
|
|
211
|
+
piTimeout,
|
|
212
|
+
} from "./cursor/exec-modern";
|
|
153
213
|
|
|
154
214
|
export const CURSOR_API_URL = "https://api2.cursor.sh";
|
|
155
|
-
export const CURSOR_CLIENT_VERSION = "cli-2026.
|
|
215
|
+
export const CURSOR_CLIENT_VERSION = "cli-2026.07.23-e383d2b";
|
|
156
216
|
|
|
157
217
|
const CURSOR_PROXY_TUNNEL_TIMEOUT_MS = 30_000;
|
|
158
218
|
|
|
219
|
+
/**
|
|
220
|
+
* Text for a recognised frame this client answers with its own typed error
|
|
221
|
+
* variant. Phrased as a client capability statement, not a tool failure: the
|
|
222
|
+
* model reads it and should route around the capability, not retry the call.
|
|
223
|
+
*/
|
|
224
|
+
const NOT_IMPLEMENTED_SUFFIX = "not implemented by this client";
|
|
225
|
+
const NOT_IMPLEMENTED = `Not implemented by this client`;
|
|
226
|
+
|
|
159
227
|
const conversationStateCache = new Map<string, ConversationStateStructure>();
|
|
160
228
|
const conversationBlobStores = new Map<string, Map<string, Uint8Array>>();
|
|
161
229
|
|
|
@@ -421,6 +489,10 @@ export const streamCursor: StreamFunction<"cursor-agent"> = (
|
|
|
421
489
|
let h2Settled = false;
|
|
422
490
|
let sawTurnEnded = false;
|
|
423
491
|
let endStreamError: Error | null = null;
|
|
492
|
+
// Reachable from the catch: a stream that dies mid-turn must still close
|
|
493
|
+
// and pair the blocks it left open, and `state` itself is scoped to the
|
|
494
|
+
// try below.
|
|
495
|
+
let openBlockState: BlockState | undefined;
|
|
424
496
|
const settleH2 = (error?: unknown): void => {
|
|
425
497
|
if (h2Settled) return;
|
|
426
498
|
h2Settled = true;
|
|
@@ -520,6 +592,7 @@ export const streamCursor: StreamFunction<"cursor-agent"> = (
|
|
|
520
592
|
get currentToolCall() {
|
|
521
593
|
return currentToolCall;
|
|
522
594
|
},
|
|
595
|
+
openToolCalls: new Map<string, ToolCallState>(),
|
|
523
596
|
resolvedMcpToolCallIds,
|
|
524
597
|
get firstTokenTime() {
|
|
525
598
|
return firstTokenTime;
|
|
@@ -539,6 +612,7 @@ export const streamCursor: StreamFunction<"cursor-agent"> = (
|
|
|
539
612
|
onTodoSnapshot: options?.execHandlers?.todoSync?.bind(options.execHandlers),
|
|
540
613
|
onToolResult: options?.onToolResult,
|
|
541
614
|
};
|
|
615
|
+
openBlockState = state;
|
|
542
616
|
|
|
543
617
|
const onConversationCheckpoint = (checkpoint: ConversationStateStructure) => {
|
|
544
618
|
conversationStateCache.set(conversationId, checkpoint);
|
|
@@ -677,17 +751,7 @@ export const streamCursor: StreamFunction<"cursor-agent"> = (
|
|
|
677
751
|
|
|
678
752
|
endCurrentTextBlock(output, stream, state);
|
|
679
753
|
endCurrentThinkingBlock(output, stream, state);
|
|
680
|
-
|
|
681
|
-
const idx = output.content.indexOf(state.currentToolCall);
|
|
682
|
-
state.currentToolCall.arguments = parseStreamingJson(state.currentToolCall[kStreamingPartialJson]);
|
|
683
|
-
clearStreamingPartialJson(state.currentToolCall);
|
|
684
|
-
stream.push({
|
|
685
|
-
type: "toolcall_end",
|
|
686
|
-
contentIndex: idx,
|
|
687
|
-
toolCall: state.currentToolCall,
|
|
688
|
-
partial: output,
|
|
689
|
-
});
|
|
690
|
-
}
|
|
754
|
+
flushOpenToolCalls(output, stream, state);
|
|
691
755
|
|
|
692
756
|
calculateCost(model, output.usage);
|
|
693
757
|
|
|
@@ -708,6 +772,19 @@ export const streamCursor: StreamFunction<"cursor-agent"> = (
|
|
|
708
772
|
// (handlers have no cancellation contract and must not delay the
|
|
709
773
|
// terminal error the user asked for).
|
|
710
774
|
await drainInFlightDispatches();
|
|
775
|
+
// A stream that dies mid-turn leaves blocks open, and this is the path
|
|
776
|
+
// it takes: `settleH2` rejects when the transport closes without
|
|
777
|
+
// `turnEnded`, so the success-path flush above never runs. Closing
|
|
778
|
+
// them here settles their live cards and pairs the server-owned calls
|
|
779
|
+
// (`connect_scm`, native todo) that nothing else answers — an
|
|
780
|
+
// unpaired call is stripped from every rebuilt transcript.
|
|
781
|
+
// Undefined only when the failure predates the state's construction,
|
|
782
|
+
// in which case no block was ever opened.
|
|
783
|
+
if (openBlockState) {
|
|
784
|
+
endCurrentTextBlock(output, stream, openBlockState);
|
|
785
|
+
endCurrentThinkingBlock(output, stream, openBlockState);
|
|
786
|
+
flushOpenToolCalls(output, stream, openBlockState);
|
|
787
|
+
}
|
|
711
788
|
const result = await AIError.finalize(error, { api: model.api, signal: options?.signal });
|
|
712
789
|
output.stopReason = result.stopReason;
|
|
713
790
|
output.errorStatus = result.status;
|
|
@@ -736,7 +813,8 @@ export type ToolCallState = ToolCall & {
|
|
|
736
813
|
[kStreamingBlockIndex]: number;
|
|
737
814
|
[kStreamingPartialJson]?: string;
|
|
738
815
|
[kStreamingLastParseLen]?: number;
|
|
739
|
-
[kStreamingBlockKind]: "mcp" | "todo" | "cursor-exec";
|
|
816
|
+
[kStreamingBlockKind]: "mcp" | "todo" | "cursor-exec" | "connect-scm";
|
|
817
|
+
[kStreamingEnvelopeId]?: string;
|
|
740
818
|
[kCursorExecResolved]?: true;
|
|
741
819
|
};
|
|
742
820
|
|
|
@@ -744,6 +822,17 @@ export interface BlockState {
|
|
|
744
822
|
currentTextBlock: (TextContent & { [kStreamingBlockIndex]: number }) | null;
|
|
745
823
|
currentThinkingBlock: (ThinkingContent & { [kStreamingBlockIndex]: number }) | null;
|
|
746
824
|
currentToolCall: ToolCallState | null;
|
|
825
|
+
/**
|
|
826
|
+
* Open streamed tool-call blocks, keyed by the interaction envelope's
|
|
827
|
+
* `call_id`.
|
|
828
|
+
*
|
|
829
|
+
* Cursor interleaves calls: two `toolCallStarted` frames can arrive before
|
|
830
|
+
* either completes. A single "current" slot would let the second overwrite
|
|
831
|
+
* the first, orphaning a block that nothing then settles. Every keyed block
|
|
832
|
+
* stays reachable until its own completion, and `currentToolCall` remains
|
|
833
|
+
* only as the fallback for frames that carry no `call_id`.
|
|
834
|
+
*/
|
|
835
|
+
openToolCalls: Map<string, ToolCallState>;
|
|
747
836
|
/** MCP call IDs synthesized from exec frames before their redundant streamed block arrives. */
|
|
748
837
|
resolvedMcpToolCallIds: Set<string>;
|
|
749
838
|
firstTokenTime: number | undefined;
|
|
@@ -1195,6 +1284,14 @@ async function handleExecServerMessage(
|
|
|
1195
1284
|
}
|
|
1196
1285
|
|
|
1197
1286
|
if (!execCase) {
|
|
1287
|
+
// A frame carrying a oneof number this build's `agent.proto` does not
|
|
1288
|
+
// model at all: protobuf decodes it into unknown fields and leaves
|
|
1289
|
+
// `message.case` unset, so the client cannot even name what was asked.
|
|
1290
|
+
// Returning silently strands the exec id — the server waits on a reply
|
|
1291
|
+
// that never comes. Distinct from the `default:` branch below, which
|
|
1292
|
+
// names a frame it recognises but cannot serve.
|
|
1293
|
+
log("warn", "unknownExecVariant", { id: execMsg.id, execId: execMsg.execId });
|
|
1294
|
+
sendExecClientThrow(h2Request, execMsg, "Unknown exec message variant", "unknown_exec_variant");
|
|
1198
1295
|
return;
|
|
1199
1296
|
}
|
|
1200
1297
|
|
|
@@ -1202,12 +1299,22 @@ async function handleExecServerMessage(
|
|
|
1202
1299
|
case "readArgs": {
|
|
1203
1300
|
const args = execMsg.message.value;
|
|
1204
1301
|
if (!args.toolCallId) args.toolCallId = crypto.randomUUID();
|
|
1205
|
-
|
|
1302
|
+
// The same composed selector the bridge executes: showing a bare path
|
|
1303
|
+
// for a ranged read makes the returned slice look like the whole
|
|
1304
|
+
// file in every rebuilt transcript.
|
|
1305
|
+
synthesizeCursorExecToolCall(output, stream, state, args.toolCallId, "read", {
|
|
1306
|
+
path: piReadDisplayPath(args.path, args.offset, args.limit),
|
|
1307
|
+
});
|
|
1206
1308
|
const { execResult } = await resolveExecHandler(
|
|
1207
1309
|
args,
|
|
1208
1310
|
execHandlers?.read?.bind(execHandlers),
|
|
1209
1311
|
onToolResult,
|
|
1210
|
-
toolResult =>
|
|
1312
|
+
toolResult =>
|
|
1313
|
+
buildReadResultFromToolResult(
|
|
1314
|
+
args.path,
|
|
1315
|
+
toolResult,
|
|
1316
|
+
args.offset !== undefined || args.limit !== undefined,
|
|
1317
|
+
),
|
|
1211
1318
|
reason => buildReadRejectedResult(args.path, reason),
|
|
1212
1319
|
error => buildReadErrorResult(args.path, error),
|
|
1213
1320
|
{ toolCallId: args.toolCallId, toolName: "read" },
|
|
@@ -1256,6 +1363,7 @@ async function handleExecServerMessage(
|
|
|
1256
1363
|
pattern: args.pattern,
|
|
1257
1364
|
path: searchPath,
|
|
1258
1365
|
case: args.caseInsensitive === true ? false : undefined,
|
|
1366
|
+
skip: piGrepSkip(args.offset),
|
|
1259
1367
|
});
|
|
1260
1368
|
const { execResult } = await resolveExecHandler(
|
|
1261
1369
|
args,
|
|
@@ -1418,6 +1526,36 @@ async function handleExecServerMessage(
|
|
|
1418
1526
|
case "mcpArgs": {
|
|
1419
1527
|
const args = execMsg.message.value;
|
|
1420
1528
|
const mcpCall = decodeMcpCall(args);
|
|
1529
|
+
// An approval probe, not an invocation: the frame asks whether the
|
|
1530
|
+
// call would be permitted. Running the tool to find out fires a side
|
|
1531
|
+
// effect the user has not been asked about, and fires it again when
|
|
1532
|
+
// the real frame follows — so this must answer without executing.
|
|
1533
|
+
//
|
|
1534
|
+
// The host resolves it against the same policy the wrapper applies at
|
|
1535
|
+
// execution time. Only a definite allow is approved: a pending prompt
|
|
1536
|
+
// cannot be asked through this frame, and answering yes on its behalf
|
|
1537
|
+
// would pre-authorize a call the user never saw. Without a handler
|
|
1538
|
+
// there is nothing to decide with, so it is refused. Either way no
|
|
1539
|
+
// block is synthesized — nothing ran.
|
|
1540
|
+
if (mcpCall.approvalOnly) {
|
|
1541
|
+
const approved = (await execHandlers?.mcpApprovalPreflight?.(mcpCall)) === true;
|
|
1542
|
+
sendExecClientMessage(
|
|
1543
|
+
h2Request,
|
|
1544
|
+
execMsg,
|
|
1545
|
+
"mcpResult",
|
|
1546
|
+
create(McpResultSchema, {
|
|
1547
|
+
result: approved
|
|
1548
|
+
? { case: "approved", value: create(McpApprovedSchema, {}) }
|
|
1549
|
+
: {
|
|
1550
|
+
case: "rejected",
|
|
1551
|
+
value: create(McpRejectedSchema, {
|
|
1552
|
+
reason: `Tool "${mcpCall.toolName || mcpCall.name}" is not approved to run without asking.`,
|
|
1553
|
+
}),
|
|
1554
|
+
},
|
|
1555
|
+
}),
|
|
1556
|
+
);
|
|
1557
|
+
return;
|
|
1558
|
+
}
|
|
1421
1559
|
if (execHandlers?.mcp) {
|
|
1422
1560
|
const existingBlock = output.content.find(
|
|
1423
1561
|
block => block.type === "toolCall" && block.id === mcpCall.toolCallId,
|
|
@@ -1449,54 +1587,571 @@ async function handleExecServerMessage(
|
|
|
1449
1587
|
return;
|
|
1450
1588
|
}
|
|
1451
1589
|
case "listMcpResourcesExecArgs": {
|
|
1452
|
-
|
|
1590
|
+
// A host holding live MCP connections answers from them; without a
|
|
1591
|
+
// handler the honest answer is an explicit empty success. An
|
|
1592
|
+
// unset-oneof result would read as "the call produced nothing".
|
|
1593
|
+
const args = execMsg.message.value;
|
|
1594
|
+
let execResult: ListMcpResourcesExecResult;
|
|
1595
|
+
// The model consumes this catalog, so it needs a block and a paired
|
|
1596
|
+
// result or the listing is invisible in the UI and gone from every
|
|
1597
|
+
// rebuilt history. Only synthesized when a handler exists: without
|
|
1598
|
+
// one the frame is a fixed empty answer that executed nothing.
|
|
1599
|
+
const toolCallId = execHandlers?.listMcpResources ? crypto.randomUUID() : undefined;
|
|
1600
|
+
if (toolCallId) {
|
|
1601
|
+
synthesizeCursorExecToolCall(output, stream, state, toolCallId, "list_mcp_resources", {
|
|
1602
|
+
server: args.server,
|
|
1603
|
+
});
|
|
1604
|
+
}
|
|
1605
|
+
try {
|
|
1606
|
+
const resources = (await execHandlers?.listMcpResources?.({ server: args.server })) ?? [];
|
|
1607
|
+
execResult = create(ListMcpResourcesExecResultSchema, {
|
|
1608
|
+
result: {
|
|
1609
|
+
case: "success",
|
|
1610
|
+
value: create(ListMcpResourcesSuccessSchema, {
|
|
1611
|
+
resources: resources.map(resource =>
|
|
1612
|
+
create(ListMcpResourcesExecResult_McpResourceSchema, {
|
|
1613
|
+
uri: resource.uri,
|
|
1614
|
+
name: resource.name,
|
|
1615
|
+
description: resource.description,
|
|
1616
|
+
mimeType: resource.mimeType,
|
|
1617
|
+
server: resource.server,
|
|
1618
|
+
}),
|
|
1619
|
+
),
|
|
1620
|
+
}),
|
|
1621
|
+
},
|
|
1622
|
+
});
|
|
1623
|
+
} catch (error) {
|
|
1624
|
+
execResult = create(ListMcpResourcesExecResultSchema, {
|
|
1625
|
+
result: {
|
|
1626
|
+
case: "error",
|
|
1627
|
+
value: create(ListMcpResourcesErrorSchema, {
|
|
1628
|
+
error: error instanceof Error ? error.message : String(error),
|
|
1629
|
+
}),
|
|
1630
|
+
},
|
|
1631
|
+
});
|
|
1632
|
+
}
|
|
1633
|
+
if (toolCallId) {
|
|
1634
|
+
// Derived from the answer that goes on the wire, so the block can
|
|
1635
|
+
// never disagree with what the model was told.
|
|
1636
|
+
const settled = execResult.result;
|
|
1637
|
+
const text =
|
|
1638
|
+
settled.case === "success"
|
|
1639
|
+
? formatListedMcpResources(settled.value.resources)
|
|
1640
|
+
: settled.case === "error"
|
|
1641
|
+
? settled.value.error || "Failed to list MCP resources"
|
|
1642
|
+
: (settled.value?.reason ?? "Failed to list MCP resources");
|
|
1643
|
+
await pairSynthesizedExecResult(
|
|
1644
|
+
state,
|
|
1645
|
+
onToolResult,
|
|
1646
|
+
toolCallId,
|
|
1647
|
+
"list_mcp_resources",
|
|
1648
|
+
text,
|
|
1649
|
+
settled.case !== "success",
|
|
1650
|
+
);
|
|
1651
|
+
}
|
|
1453
1652
|
sendExecClientMessage(h2Request, execMsg, "listMcpResourcesExecResult", execResult);
|
|
1454
1653
|
return;
|
|
1455
1654
|
}
|
|
1456
1655
|
case "readMcpResourceExecArgs": {
|
|
1457
|
-
const
|
|
1656
|
+
const args = execMsg.message.value;
|
|
1657
|
+
let execResult: ReadMcpResourceExecResult;
|
|
1658
|
+
// The read runs locally, and in download mode it writes a workspace
|
|
1659
|
+
// file — an operation with no transcript block is invisible in the UI
|
|
1660
|
+
// and absent from every rebuilt history. Only synthesized when a
|
|
1661
|
+
// handler exists: without one the frame is a fixed `not_found` that
|
|
1662
|
+
// executed nothing, and a block would claim work that never happened.
|
|
1663
|
+
const toolCallId = execHandlers?.readMcpResource ? crypto.randomUUID() : undefined;
|
|
1664
|
+
if (toolCallId) {
|
|
1665
|
+
synthesizeCursorExecToolCall(output, stream, state, toolCallId, "read_mcp_resource", {
|
|
1666
|
+
server: args.server,
|
|
1667
|
+
uri: args.uri,
|
|
1668
|
+
download_path: args.downloadPath,
|
|
1669
|
+
});
|
|
1670
|
+
}
|
|
1671
|
+
try {
|
|
1672
|
+
// `null` is the handler's "no such server or uri", which is exactly
|
|
1673
|
+
// `not_found`; a throw is a real failure and must not masquerade as
|
|
1674
|
+
// a missing resource.
|
|
1675
|
+
const content = await execHandlers?.readMcpResource?.({
|
|
1676
|
+
server: args.server,
|
|
1677
|
+
uri: args.uri,
|
|
1678
|
+
downloadPath: args.downloadPath,
|
|
1679
|
+
});
|
|
1680
|
+
execResult = content
|
|
1681
|
+
? create(ReadMcpResourceExecResultSchema, {
|
|
1682
|
+
result: {
|
|
1683
|
+
case: "success",
|
|
1684
|
+
value: create(ReadMcpResourceSuccessSchema, {
|
|
1685
|
+
uri: content.uri,
|
|
1686
|
+
name: content.name,
|
|
1687
|
+
description: content.description,
|
|
1688
|
+
mimeType: content.mimeType,
|
|
1689
|
+
downloadPath: content.downloadPath,
|
|
1690
|
+
// A download returns no content to the model: the file is
|
|
1691
|
+
// on disk and the path is the answer. Otherwise the wire's
|
|
1692
|
+
// content oneof carries one of the two, text winning when
|
|
1693
|
+
// a host supplies both.
|
|
1694
|
+
content:
|
|
1695
|
+
content.downloadPath !== undefined
|
|
1696
|
+
? { case: undefined }
|
|
1697
|
+
: content.text !== undefined
|
|
1698
|
+
? { case: "text", value: content.text }
|
|
1699
|
+
: content.blob !== undefined
|
|
1700
|
+
? { case: "blob", value: content.blob }
|
|
1701
|
+
: { case: undefined },
|
|
1702
|
+
}),
|
|
1703
|
+
},
|
|
1704
|
+
})
|
|
1705
|
+
: create(ReadMcpResourceExecResultSchema, {
|
|
1706
|
+
result: { case: "notFound", value: create(ReadMcpResourceNotFoundSchema, { uri: args.uri }) },
|
|
1707
|
+
});
|
|
1708
|
+
} catch (error) {
|
|
1709
|
+
execResult = create(ReadMcpResourceExecResultSchema, {
|
|
1710
|
+
result: {
|
|
1711
|
+
case: "error",
|
|
1712
|
+
value: create(ReadMcpResourceErrorSchema, {
|
|
1713
|
+
uri: args.uri,
|
|
1714
|
+
error: error instanceof Error ? error.message : String(error),
|
|
1715
|
+
}),
|
|
1716
|
+
},
|
|
1717
|
+
});
|
|
1718
|
+
}
|
|
1719
|
+
if (toolCallId) {
|
|
1720
|
+
// Derived from the answer that actually goes on the wire, so no exit
|
|
1721
|
+
// can drift out of sync with what the model was told.
|
|
1722
|
+
const settled = execResult.result;
|
|
1723
|
+
let text: string;
|
|
1724
|
+
switch (settled.case) {
|
|
1725
|
+
case "success":
|
|
1726
|
+
text = settled.value.downloadPath
|
|
1727
|
+
? `Downloaded ${args.uri} to ${settled.value.downloadPath}`
|
|
1728
|
+
: `Read ${args.uri}`;
|
|
1729
|
+
break;
|
|
1730
|
+
case "notFound":
|
|
1731
|
+
text = `No such resource: ${args.uri}`;
|
|
1732
|
+
break;
|
|
1733
|
+
// The wire union carries a refusal variant this client never
|
|
1734
|
+
// builds today — the handler answers content or `null`. Handled
|
|
1735
|
+
// anyway so the switch stays total: it holds a `reason`, not an
|
|
1736
|
+
// `error`, so a collapsed default would have read `undefined`.
|
|
1737
|
+
case "rejected":
|
|
1738
|
+
text = `Refused: ${settled.value.reason}`;
|
|
1739
|
+
break;
|
|
1740
|
+
default:
|
|
1741
|
+
text = settled.value?.error ?? `Failed to read ${args.uri}`;
|
|
1742
|
+
break;
|
|
1743
|
+
}
|
|
1744
|
+
await pairSynthesizedExecResult(
|
|
1745
|
+
state,
|
|
1746
|
+
onToolResult,
|
|
1747
|
+
toolCallId,
|
|
1748
|
+
"read_mcp_resource",
|
|
1749
|
+
text,
|
|
1750
|
+
settled.case !== "success",
|
|
1751
|
+
);
|
|
1752
|
+
}
|
|
1458
1753
|
sendExecClientMessage(h2Request, execMsg, "readMcpResourceExecResult", execResult);
|
|
1459
1754
|
return;
|
|
1460
1755
|
}
|
|
1461
1756
|
case "recordScreenArgs": {
|
|
1462
|
-
const execResult = create(RecordScreenResultSchema, {
|
|
1757
|
+
const execResult = create(RecordScreenResultSchema, {
|
|
1758
|
+
result: { case: "failure", value: create(RecordScreenFailureSchema, { error: NOT_IMPLEMENTED }) },
|
|
1759
|
+
});
|
|
1463
1760
|
sendExecClientMessage(h2Request, execMsg, "recordScreenResult", execResult);
|
|
1464
1761
|
return;
|
|
1465
1762
|
}
|
|
1466
1763
|
case "computerUseArgs": {
|
|
1467
|
-
const execResult = create(ComputerUseResultSchema, {
|
|
1764
|
+
const execResult = create(ComputerUseResultSchema, {
|
|
1765
|
+
result: { case: "error", value: create(ComputerUseErrorSchema, { error: NOT_IMPLEMENTED }) },
|
|
1766
|
+
});
|
|
1468
1767
|
sendExecClientMessage(h2Request, execMsg, "computerUseResult", execResult);
|
|
1469
1768
|
return;
|
|
1470
1769
|
}
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
//
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1770
|
+
case "piReadArgs": {
|
|
1771
|
+
const args = execMsg.message.value;
|
|
1772
|
+
const toolCallId = crypto.randomUUID();
|
|
1773
|
+
// The displayed block must show the operation that actually runs: the
|
|
1774
|
+
// bridge composes the same range selector onto the path.
|
|
1775
|
+
synthesizeCursorExecToolCall(output, stream, state, toolCallId, "read", {
|
|
1776
|
+
path: piReadDisplayPath(args.path, args.offset, args.limit),
|
|
1777
|
+
});
|
|
1778
|
+
const { execResult } = await resolveExecHandler(
|
|
1779
|
+
{ args, toolCallId },
|
|
1780
|
+
execHandlers?.piRead?.bind(execHandlers),
|
|
1781
|
+
onToolResult,
|
|
1782
|
+
buildPiReadResult,
|
|
1783
|
+
buildPiReadError,
|
|
1784
|
+
buildPiReadError,
|
|
1785
|
+
{ toolCallId, toolName: "read" },
|
|
1786
|
+
);
|
|
1787
|
+
sendExecClientMessage(h2Request, execMsg, "piReadResult", execResult);
|
|
1788
|
+
return;
|
|
1789
|
+
}
|
|
1790
|
+
case "piBashArgs": {
|
|
1791
|
+
const args = execMsg.message.value;
|
|
1792
|
+
const toolCallId = crypto.randomUUID();
|
|
1793
|
+
synthesizeCursorExecToolCall(output, stream, state, toolCallId, "bash", {
|
|
1794
|
+
command: args.command,
|
|
1795
|
+
timeout: piTimeout(args.timeout),
|
|
1796
|
+
});
|
|
1797
|
+
const { execResult } = await resolveExecHandler(
|
|
1798
|
+
{ args, toolCallId },
|
|
1799
|
+
execHandlers?.piBash?.bind(execHandlers),
|
|
1800
|
+
onToolResult,
|
|
1801
|
+
buildPiBashResult,
|
|
1802
|
+
buildPiBashError,
|
|
1803
|
+
buildPiBashError,
|
|
1804
|
+
{ toolCallId, toolName: "bash" },
|
|
1805
|
+
);
|
|
1806
|
+
sendExecClientMessage(h2Request, execMsg, "piBashResult", execResult);
|
|
1807
|
+
return;
|
|
1808
|
+
}
|
|
1809
|
+
case "piEditArgs": {
|
|
1810
|
+
const args = execMsg.message.value;
|
|
1811
|
+
const toolCallId = crypto.randomUUID();
|
|
1812
|
+
// `PiEditReplacement` is the local `edit` tool's replace mode verbatim:
|
|
1813
|
+
// snake_case `old_text`/`new_text` entries against one path.
|
|
1814
|
+
synthesizeCursorExecToolCall(output, stream, state, toolCallId, "edit", {
|
|
1815
|
+
path: args.path,
|
|
1816
|
+
edits: args.edits.map(edit => ({ old_text: edit.oldText, new_text: edit.newText })),
|
|
1817
|
+
});
|
|
1818
|
+
const { execResult } = await resolveExecHandler(
|
|
1819
|
+
{ args, toolCallId },
|
|
1820
|
+
execHandlers?.piEdit?.bind(execHandlers),
|
|
1821
|
+
onToolResult,
|
|
1822
|
+
buildPiEditResult,
|
|
1823
|
+
buildPiEditRejected,
|
|
1824
|
+
buildPiEditError,
|
|
1825
|
+
{ toolCallId, toolName: "edit" },
|
|
1826
|
+
);
|
|
1827
|
+
sendExecClientMessage(h2Request, execMsg, "piEditResult", execResult);
|
|
1828
|
+
return;
|
|
1829
|
+
}
|
|
1830
|
+
case "piWriteArgs": {
|
|
1831
|
+
const args = execMsg.message.value;
|
|
1832
|
+
const toolCallId = crypto.randomUUID();
|
|
1833
|
+
synthesizeCursorExecToolCall(output, stream, state, toolCallId, "write", {
|
|
1834
|
+
path: args.path,
|
|
1835
|
+
content: args.content,
|
|
1836
|
+
});
|
|
1837
|
+
const { execResult } = await resolveExecHandler(
|
|
1838
|
+
{ args, toolCallId },
|
|
1839
|
+
execHandlers?.piWrite?.bind(execHandlers),
|
|
1840
|
+
onToolResult,
|
|
1841
|
+
buildPiWriteResult,
|
|
1842
|
+
buildPiWriteRejected,
|
|
1843
|
+
buildPiWriteError,
|
|
1844
|
+
{ toolCallId, toolName: "write" },
|
|
1845
|
+
);
|
|
1846
|
+
sendExecClientMessage(h2Request, execMsg, "piWriteResult", execResult);
|
|
1847
|
+
return;
|
|
1848
|
+
}
|
|
1849
|
+
case "piGrepArgs": {
|
|
1850
|
+
const args = execMsg.message.value;
|
|
1851
|
+
const toolCallId = crypto.randomUUID();
|
|
1852
|
+
synthesizeCursorExecToolCall(output, stream, state, toolCallId, "grep", {
|
|
1853
|
+
pattern: args.literal === true ? piEscapeRegexLiteral(args.pattern) : args.pattern,
|
|
1854
|
+
path: args.glob ? piJoinPath(args.path, args.glob) : args.path || ".",
|
|
1855
|
+
case: args.ignoreCase === true ? false : undefined,
|
|
1856
|
+
// Neither field exists in the model-facing `grep` schema — the bridge
|
|
1857
|
+
// serves them by building a scoped tool instead. Recorded anyway, for
|
|
1858
|
+
// the same reason `pi_read` renders its range into the displayed path:
|
|
1859
|
+
// a capped or context-widened search is otherwise replayed as an
|
|
1860
|
+
// ordinary grep sitting next to output no ordinary grep produces.
|
|
1861
|
+
context: args.context,
|
|
1862
|
+
limit: piLimit(args.limit),
|
|
1863
|
+
});
|
|
1864
|
+
const { execResult } = await resolveExecHandler(
|
|
1865
|
+
{ args, toolCallId },
|
|
1866
|
+
execHandlers?.piGrep?.bind(execHandlers),
|
|
1867
|
+
onToolResult,
|
|
1868
|
+
buildPiGrepResult,
|
|
1869
|
+
buildPiGrepError,
|
|
1870
|
+
buildPiGrepError,
|
|
1871
|
+
{ toolCallId, toolName: "grep" },
|
|
1872
|
+
);
|
|
1873
|
+
sendExecClientMessage(h2Request, execMsg, "piGrepResult", execResult);
|
|
1874
|
+
return;
|
|
1875
|
+
}
|
|
1876
|
+
case "piFindArgs": {
|
|
1877
|
+
const args = execMsg.message.value;
|
|
1878
|
+
const toolCallId = crypto.randomUUID();
|
|
1879
|
+
synthesizeCursorExecToolCall(output, stream, state, toolCallId, "glob", {
|
|
1880
|
+
path: piJoinPath(args.path, args.pattern),
|
|
1881
|
+
limit: piLimit(args.limit),
|
|
1882
|
+
});
|
|
1883
|
+
const { execResult } = await resolveExecHandler(
|
|
1884
|
+
{ args, toolCallId },
|
|
1885
|
+
execHandlers?.piFind?.bind(execHandlers),
|
|
1886
|
+
onToolResult,
|
|
1887
|
+
buildPiFindResult,
|
|
1888
|
+
buildPiFindError,
|
|
1889
|
+
buildPiFindError,
|
|
1890
|
+
{ toolCallId, toolName: "glob" },
|
|
1891
|
+
);
|
|
1892
|
+
sendExecClientMessage(h2Request, execMsg, "piFindResult", execResult);
|
|
1893
|
+
return;
|
|
1894
|
+
}
|
|
1895
|
+
case "piLsArgs": {
|
|
1896
|
+
const args = execMsg.message.value;
|
|
1897
|
+
const toolCallId = crypto.randomUUID();
|
|
1898
|
+
// Same mapping as the legacy `lsArgs` frame: the local `read` tool lists
|
|
1899
|
+
// directories, so the synthesized block must name `read` to match the
|
|
1900
|
+
// bridge's own `toolResult`.
|
|
1901
|
+
synthesizeCursorExecToolCall(output, stream, state, toolCallId, "read", { path: piLsPath(args.path) });
|
|
1902
|
+
const { execResult } = await resolveExecHandler(
|
|
1903
|
+
{ args, toolCallId },
|
|
1904
|
+
execHandlers?.piLs?.bind(execHandlers),
|
|
1905
|
+
onToolResult,
|
|
1906
|
+
buildPiLsResult,
|
|
1907
|
+
buildPiLsError,
|
|
1908
|
+
buildPiLsError,
|
|
1909
|
+
{ toolCallId, toolName: "read" },
|
|
1910
|
+
);
|
|
1911
|
+
sendExecClientMessage(h2Request, execMsg, "piLsResult", execResult);
|
|
1912
|
+
return;
|
|
1913
|
+
}
|
|
1914
|
+
case "miniSweAgentBashArgs": {
|
|
1915
|
+
// Same `ShellArgs`/`ShellResult` pair as `shellArgs`, under its own frame
|
|
1916
|
+
// number, so the existing shell handler answers it unchanged.
|
|
1917
|
+
const args = execMsg.message.value;
|
|
1918
|
+
if (!args.toolCallId) args.toolCallId = crypto.randomUUID();
|
|
1919
|
+
const normalizedArgs: ShellArgs = { ...args, workingDirectory: args.workingDirectory || process.cwd() };
|
|
1920
|
+
synthesizeCursorExecToolCall(output, stream, state, args.toolCallId, "bash", {
|
|
1921
|
+
command: args.command,
|
|
1922
|
+
cwd: args.workingDirectory || undefined,
|
|
1923
|
+
timeout: args.timeout && args.timeout > 0 ? args.timeout : undefined,
|
|
1924
|
+
});
|
|
1925
|
+
const { execResult } = await resolveExecHandler(
|
|
1926
|
+
normalizedArgs,
|
|
1927
|
+
execHandlers?.shell?.bind(execHandlers),
|
|
1928
|
+
onToolResult,
|
|
1929
|
+
toolResult => buildShellResultFromToolResult(normalizedArgs, toolResult),
|
|
1930
|
+
reason => buildShellRejectedResult(normalizedArgs.command, normalizedArgs.workingDirectory, reason),
|
|
1931
|
+
error => buildShellFailureResult(normalizedArgs.command, normalizedArgs.workingDirectory, error),
|
|
1932
|
+
{ toolCallId: args.toolCallId, toolName: "bash" },
|
|
1933
|
+
);
|
|
1934
|
+
sendExecClientMessage(h2Request, execMsg, "miniSweAgentBashResult", sanitizeShellExecResult(execResult));
|
|
1935
|
+
return;
|
|
1936
|
+
}
|
|
1937
|
+
case "redactedReadArgs": {
|
|
1938
|
+
// Same `ReadArgs`/`ReadResult` pair as `readArgs`, but the server expects
|
|
1939
|
+
// the client to strip secrets from the content first. No redaction is
|
|
1940
|
+
// implemented here, and serving a plain read would hand back exactly the
|
|
1941
|
+
// unredacted bytes the frame exists to withhold.
|
|
1942
|
+
const args = execMsg.message.value;
|
|
1943
|
+
sendExecClientMessage(
|
|
1944
|
+
h2Request,
|
|
1945
|
+
execMsg,
|
|
1946
|
+
"redactedReadResult",
|
|
1947
|
+
buildReadErrorResult(args.path, "Secret redaction is not implemented by this client"),
|
|
1948
|
+
);
|
|
1949
|
+
return;
|
|
1950
|
+
}
|
|
1951
|
+
case "mcpStateExecArgs": {
|
|
1952
|
+
const args = execMsg.message.value;
|
|
1953
|
+
sendExecClientMessage(
|
|
1954
|
+
h2Request,
|
|
1955
|
+
execMsg,
|
|
1956
|
+
"mcpStateExecResult",
|
|
1957
|
+
buildMcpStateResult(requestContextTools, args.serverIdentifiers),
|
|
1958
|
+
);
|
|
1959
|
+
return;
|
|
1960
|
+
}
|
|
1961
|
+
case "executeHookArgs": {
|
|
1962
|
+
const args = execMsg.message.value;
|
|
1963
|
+
const execResult = buildNeutralHookResult(args.request);
|
|
1964
|
+
if (!execResult) {
|
|
1965
|
+
sendExecClientThrow(
|
|
1966
|
+
h2Request,
|
|
1967
|
+
execMsg,
|
|
1968
|
+
`Unsupported hook request: ${args.request?.request.case ?? "unset"}`,
|
|
1969
|
+
"unknown_hook_request",
|
|
1970
|
+
);
|
|
1971
|
+
return;
|
|
1972
|
+
}
|
|
1973
|
+
sendExecClientMessage(h2Request, execMsg, "executeHookResult", execResult);
|
|
1974
|
+
return;
|
|
1975
|
+
}
|
|
1976
|
+
case "subagentArgs": {
|
|
1977
|
+
const args = execMsg.message.value;
|
|
1978
|
+
const execResult = create(SubagentResultSchema, {
|
|
1979
|
+
result: {
|
|
1980
|
+
case: "error",
|
|
1981
|
+
value: create(SubagentErrorSchema, { error: `Subagents are ${NOT_IMPLEMENTED_SUFFIX}` }),
|
|
1982
|
+
},
|
|
1983
|
+
});
|
|
1984
|
+
log("exec", "subagentRejected", { subagentType: args.subagentType });
|
|
1985
|
+
sendExecClientMessage(h2Request, execMsg, "subagentResult", execResult);
|
|
1986
|
+
return;
|
|
1987
|
+
}
|
|
1988
|
+
case "subagentAwaitArgs": {
|
|
1989
|
+
// No subagent was ever spawned, so every awaited id is genuinely unknown.
|
|
1990
|
+
const args = execMsg.message.value;
|
|
1991
|
+
const execResult = create(SubagentAwaitResultSchema, {
|
|
1992
|
+
result: {
|
|
1993
|
+
case: "notFound",
|
|
1994
|
+
value: create(SubagentAwaitNotFoundSchema, { agentId: args.agentId }),
|
|
1995
|
+
},
|
|
1996
|
+
});
|
|
1997
|
+
sendExecClientMessage(h2Request, execMsg, "subagentAwaitResult", execResult);
|
|
1998
|
+
return;
|
|
1999
|
+
}
|
|
2000
|
+
case "forceBackgroundShellArgs": {
|
|
2001
|
+
// Backgrounding targets a running tool call by id. This client runs every
|
|
2002
|
+
// shell to completion in band, so there is never one to move.
|
|
2003
|
+
const execResult = create(ForceBackgroundShellResultSchema, {
|
|
2004
|
+
status: ForceBackgroundShellStatus.NOT_FOUND,
|
|
2005
|
+
});
|
|
2006
|
+
sendExecClientMessage(h2Request, execMsg, "forceBackgroundShellResult", execResult);
|
|
2007
|
+
return;
|
|
2008
|
+
}
|
|
2009
|
+
case "forceBackgroundSubagentArgs": {
|
|
2010
|
+
const execResult = create(ForceBackgroundSubagentResultSchema, {
|
|
2011
|
+
status: ForceBackgroundSubagentStatus.NOT_FOUND,
|
|
2012
|
+
});
|
|
2013
|
+
sendExecClientMessage(h2Request, execMsg, "forceBackgroundSubagentResult", execResult);
|
|
2014
|
+
return;
|
|
2015
|
+
}
|
|
2016
|
+
case "smartModeClassifierArgs": {
|
|
2017
|
+
// The classifier decides whether a risky action needs approval. Answering
|
|
2018
|
+
// `ALLOW` would silently wave through actions the server asked us to
|
|
2019
|
+
// judge, so the honest answer is that no classifier exists here.
|
|
2020
|
+
const execResult = create(SmartModeClassifierResultSchema, {
|
|
2021
|
+
result: {
|
|
2022
|
+
case: "error",
|
|
2023
|
+
value: create(SmartModeClassifierErrorSchema, {
|
|
2024
|
+
error: `Smart-mode classification is ${NOT_IMPLEMENTED_SUFFIX}`,
|
|
2025
|
+
}),
|
|
2026
|
+
},
|
|
2027
|
+
});
|
|
2028
|
+
sendExecClientMessage(h2Request, execMsg, "smartModeClassifierResult", execResult);
|
|
2029
|
+
return;
|
|
2030
|
+
}
|
|
2031
|
+
case "canvasDiagnosticsArgs": {
|
|
2032
|
+
const args = execMsg.message.value;
|
|
2033
|
+
const execResult = create(CanvasDiagnosticsResultSchema, {
|
|
2034
|
+
result: {
|
|
2035
|
+
case: "error",
|
|
2036
|
+
value: create(CanvasDiagnosticsErrorSchema, {
|
|
2037
|
+
path: args.path,
|
|
2038
|
+
error: `Canvas diagnostics are ${NOT_IMPLEMENTED_SUFFIX}`,
|
|
2039
|
+
}),
|
|
2040
|
+
},
|
|
2041
|
+
});
|
|
2042
|
+
sendExecClientMessage(h2Request, execMsg, "canvasDiagnosticsResult", execResult);
|
|
2043
|
+
return;
|
|
2044
|
+
}
|
|
2045
|
+
case "shellAllowlistPrecheckArgs": {
|
|
2046
|
+
// The prechecks ask "is this pre-approved, so may it skip the approval
|
|
2047
|
+
// prompt?". This client keeps no allowlist, so the answer is always no:
|
|
2048
|
+
// `false` costs an approval round-trip, `true` would grant one that was
|
|
2049
|
+
// never configured.
|
|
2050
|
+
sendExecClientMessage(
|
|
2051
|
+
h2Request,
|
|
2052
|
+
execMsg,
|
|
2053
|
+
"shellAllowlistPrecheckResult",
|
|
2054
|
+
create(ShellAllowlistPrecheckResultSchema, { allowlisted: false }),
|
|
2055
|
+
);
|
|
2056
|
+
return;
|
|
2057
|
+
}
|
|
2058
|
+
case "mcpAllowlistPrecheckArgs": {
|
|
2059
|
+
sendExecClientMessage(
|
|
2060
|
+
h2Request,
|
|
2061
|
+
execMsg,
|
|
2062
|
+
"mcpAllowlistPrecheckResult",
|
|
2063
|
+
create(McpAllowlistPrecheckResultSchema, { allowlisted: false }),
|
|
2064
|
+
);
|
|
2065
|
+
return;
|
|
2066
|
+
}
|
|
2067
|
+
case "webFetchAllowlistPrecheckArgs": {
|
|
2068
|
+
sendExecClientMessage(
|
|
2069
|
+
h2Request,
|
|
2070
|
+
execMsg,
|
|
2071
|
+
"webFetchAllowlistPrecheckResult",
|
|
2072
|
+
create(WebFetchAllowlistPrecheckResultSchema, { allowlisted: false }),
|
|
2073
|
+
);
|
|
2074
|
+
return;
|
|
2075
|
+
}
|
|
2076
|
+
case "conversationSearchArgs": {
|
|
2077
|
+
// Cursor conversation history lives server-side; this client keeps no
|
|
2078
|
+
// local index of it to search.
|
|
2079
|
+
//
|
|
2080
|
+
// The streamed `search_conversations_tool_call` envelope announces this
|
|
2081
|
+
// call but the interaction decoder builds no block for it, so the block
|
|
2082
|
+
// and its paired result are synthesized here — exactly like every other
|
|
2083
|
+
// exec frame. Without the pair, `buildSessionContext` strips the whole
|
|
2084
|
+
// interaction on replay. The frame carries its own `tool_call_id`, so
|
|
2085
|
+
// the streamed announcement and this block agree on the key.
|
|
2086
|
+
const args = execMsg.message.value;
|
|
2087
|
+
const toolCallId = args.toolCallId || crypto.randomUUID();
|
|
2088
|
+
const error = `Conversation search is ${NOT_IMPLEMENTED_SUFFIX}`;
|
|
2089
|
+
synthesizeCursorExecToolCall(output, stream, state, toolCallId, "search_conversations", {
|
|
2090
|
+
query: args.query,
|
|
2091
|
+
limit: args.limit,
|
|
2092
|
+
});
|
|
2093
|
+
await pairSynthesizedExecResult(state, onToolResult, toolCallId, "search_conversations", error);
|
|
2094
|
+
const execResult = create(ConversationSearchResultSchema, {
|
|
2095
|
+
result: { case: "error", value: create(ConversationSearchErrorSchema, { error }) },
|
|
1478
2096
|
});
|
|
1479
|
-
|
|
1480
|
-
|
|
2097
|
+
sendExecClientMessage(h2Request, execMsg, "conversationSearchResult", execResult);
|
|
2098
|
+
return;
|
|
2099
|
+
}
|
|
2100
|
+
case "agentStoreConflictArgs": {
|
|
2101
|
+
// The agent store is Cursor's own on-disk journal; this client never
|
|
2102
|
+
// writes one, so it has no conflict events to replay.
|
|
2103
|
+
const execResult = create(AgentStoreConflictResultSchema, {
|
|
2104
|
+
result: {
|
|
2105
|
+
case: "error",
|
|
2106
|
+
value: create(AgentStoreConflictErrorSchema, {
|
|
2107
|
+
error: `Agent store conflicts are ${NOT_IMPLEMENTED_SUFFIX}`,
|
|
2108
|
+
}),
|
|
2109
|
+
},
|
|
1481
2110
|
});
|
|
1482
|
-
h2Request
|
|
2111
|
+
sendExecClientMessage(h2Request, execMsg, "agentStoreConflictResult", execResult);
|
|
2112
|
+
return;
|
|
2113
|
+
}
|
|
2114
|
+
case "gitDiffRequest": {
|
|
2115
|
+
// `GetDiffResponse` has no error variant: it models five output formats
|
|
2116
|
+
// plus before/after file contents and nothing else. Any in-band answer is
|
|
2117
|
+
// therefore a claim that a diff was computed, so a `throw` is the only
|
|
2118
|
+
// truthful reply.
|
|
2119
|
+
sendExecClientThrow(h2Request, execMsg, `Git diff is ${NOT_IMPLEMENTED_SUFFIX}`, "exec_variant_unsupported");
|
|
2120
|
+
return;
|
|
2121
|
+
}
|
|
2122
|
+
default: {
|
|
2123
|
+
// A frame number this build recognises structurally but has no answer
|
|
2124
|
+
// for. Distinct from the unset-case path above: there the client cannot
|
|
2125
|
+
// even name the frame.
|
|
2126
|
+
log("warn", "unhandledExecMessage", { execCase });
|
|
2127
|
+
sendExecClientThrow(
|
|
2128
|
+
h2Request,
|
|
2129
|
+
execMsg,
|
|
2130
|
+
`No handler for exec message of type ${execCase}`,
|
|
2131
|
+
"exec_variant_unsupported",
|
|
2132
|
+
);
|
|
1483
2133
|
}
|
|
1484
2134
|
}
|
|
1485
2135
|
}
|
|
1486
2136
|
|
|
1487
|
-
|
|
2137
|
+
/**
|
|
2138
|
+
* Send one typed answer on the exec channel.
|
|
2139
|
+
*
|
|
2140
|
+
* `ExecClientMessage["message"]` is a discriminated union pairing each case
|
|
2141
|
+
* with its own result type, so the generic is keyed on the case: passing a
|
|
2142
|
+
* `ReadResult` under `"shellResult"` is a compile error rather than a wire
|
|
2143
|
+
* message the server rejects at runtime.
|
|
2144
|
+
*/
|
|
2145
|
+
function sendExecClientMessage<TCase extends NonNullable<ExecClientMessage["message"]["case"]>>(
|
|
1488
2146
|
h2Request: http2.ClientHttp2Stream,
|
|
1489
2147
|
execMsg: ExecServerMessage,
|
|
1490
|
-
messageCase:
|
|
1491
|
-
value:
|
|
2148
|
+
messageCase: TCase,
|
|
2149
|
+
value: Extract<ExecClientMessage["message"], { case: TCase }>["value"],
|
|
1492
2150
|
): void {
|
|
1493
2151
|
const execClientMessage = create(ExecClientMessageSchema, {
|
|
1494
2152
|
id: execMsg.id,
|
|
1495
2153
|
execId: execMsg.execId,
|
|
1496
|
-
message: {
|
|
1497
|
-
case: messageCase,
|
|
1498
|
-
value: value as any,
|
|
1499
|
-
},
|
|
2154
|
+
message: { case: messageCase, value } as ExecClientMessage["message"],
|
|
1500
2155
|
});
|
|
1501
2156
|
|
|
1502
2157
|
const clientMessage = create(AgentClientMessageSchema, {
|
|
@@ -1509,6 +2164,42 @@ function sendExecClientMessage<T>(
|
|
|
1509
2164
|
log("execClientMessage", messageCase, value);
|
|
1510
2165
|
}
|
|
1511
2166
|
|
|
2167
|
+
/**
|
|
2168
|
+
* Fail one exec frame in band.
|
|
2169
|
+
*
|
|
2170
|
+
* `ExecClientThrow` is the protocol's failure channel for a frame that cannot
|
|
2171
|
+
* be answered at all — as opposed to a frame answered with its own typed error
|
|
2172
|
+
* variant, which means "the tool ran and failed". Cursor's own executor sends
|
|
2173
|
+
* exactly this for a frame no handler claims
|
|
2174
|
+
* (`agent-exec/dist/index.js`: `No handler found for server message of type …`
|
|
2175
|
+
* → `case: 'throw'` then `streamClose`), so the server already knows how to
|
|
2176
|
+
* recover from it: it surfaces the error to the model instead of blocking on a
|
|
2177
|
+
* reply that never comes.
|
|
2178
|
+
*
|
|
2179
|
+
* The alternative this replaces — writing an `ExecClientMessage` whose `message`
|
|
2180
|
+
* oneof is unset — is not a valid answer: the server sees a reply carrying no
|
|
2181
|
+
* result and cannot tell it apart from a malformed frame.
|
|
2182
|
+
*/
|
|
2183
|
+
function sendExecClientThrow(
|
|
2184
|
+
h2Request: http2.ClientHttp2Stream,
|
|
2185
|
+
execMsg: ExecServerMessage,
|
|
2186
|
+
error: string,
|
|
2187
|
+
errorCode?: string,
|
|
2188
|
+
): void {
|
|
2189
|
+
const controlMessage = create(ExecClientControlMessageSchema, {
|
|
2190
|
+
message: {
|
|
2191
|
+
case: "throw",
|
|
2192
|
+
value: create(ExecClientThrowSchema, { id: execMsg.id, error, errorCode }),
|
|
2193
|
+
},
|
|
2194
|
+
});
|
|
2195
|
+
const clientMessage = create(AgentClientMessageSchema, {
|
|
2196
|
+
message: { case: "execClientControlMessage", value: controlMessage },
|
|
2197
|
+
});
|
|
2198
|
+
h2Request.write(frameConnectMessage(toBinary(AgentClientMessageSchema, clientMessage)));
|
|
2199
|
+
log("execClientControl", "throw", { id: execMsg.id, execId: execMsg.execId, error, errorCode });
|
|
2200
|
+
sendExecClientStreamClose(h2Request, execMsg);
|
|
2201
|
+
}
|
|
2202
|
+
|
|
1512
2203
|
function sendExecClientStreamClose(h2Request: http2.ClientHttp2Stream, execMsg: ExecServerMessage): void {
|
|
1513
2204
|
const closeMessage = create(ExecClientControlMessageSchema, {
|
|
1514
2205
|
message: {
|
|
@@ -1696,6 +2387,27 @@ function toolResultToText(toolResult: ToolResultMessage): string {
|
|
|
1696
2387
|
return toolResult.content.map(item => (item.type === "text" ? item.text : `[${item.mimeType} image]`)).join("\n");
|
|
1697
2388
|
}
|
|
1698
2389
|
|
|
2390
|
+
/**
|
|
2391
|
+
* The catalog as the paired transcript result records it.
|
|
2392
|
+
*
|
|
2393
|
+
* Cursor receives every resource's identity on the wire, but rebuilt history is
|
|
2394
|
+
* serialized from this local result — so recording only a count leaves the
|
|
2395
|
+
* model, one reload later, aware that it once saw N resources and unable to
|
|
2396
|
+
* name any of them. The URI is what a follow-up `read_mcp_resource` needs, so
|
|
2397
|
+
* it leads; name and mime type follow only when the server supplied them.
|
|
2398
|
+
*/
|
|
2399
|
+
function formatListedMcpResources(
|
|
2400
|
+
resources: { uri: string; name?: string; mimeType?: string; server?: string }[],
|
|
2401
|
+
): string {
|
|
2402
|
+
if (resources.length === 0) return "No MCP resources available";
|
|
2403
|
+
const lines = resources.map(resource => {
|
|
2404
|
+
const qualifiers = [resource.name, resource.mimeType].filter(part => !!part).join(", ");
|
|
2405
|
+
const server = resource.server ? `[${resource.server}] ` : "";
|
|
2406
|
+
return qualifiers ? `- ${server}${resource.uri} (${qualifiers})` : `- ${server}${resource.uri}`;
|
|
2407
|
+
});
|
|
2408
|
+
return [`Listed ${resources.length} MCP resource(s):`, ...lines].join("\n");
|
|
2409
|
+
}
|
|
2410
|
+
|
|
1699
2411
|
function toolResultWasTruncated(toolResult: ToolResultMessage): boolean {
|
|
1700
2412
|
if (!toolResult.details || typeof toolResult.details !== "object") {
|
|
1701
2413
|
return false;
|
|
@@ -1712,21 +2424,52 @@ function toolResultDetailBoolean(toolResult: ToolResultMessage, key: string): bo
|
|
|
1712
2424
|
return typeof value === "boolean" ? value : false;
|
|
1713
2425
|
}
|
|
1714
2426
|
|
|
1715
|
-
|
|
2427
|
+
/**
|
|
2428
|
+
* The file's own line count, when the tool recorded one.
|
|
2429
|
+
*
|
|
2430
|
+
* `details.meta.truncation.totalLines` is the whole file; the flat
|
|
2431
|
+
* `details.truncation.totalLines` counts from the window's start line and is
|
|
2432
|
+
* deliberately not consulted here. Absent for a read that returned the file
|
|
2433
|
+
* whole, where the payload IS the file and counting it is exact.
|
|
2434
|
+
*/
|
|
2435
|
+
function readTotalLinesFromDetails(toolResult: ToolResultMessage): number | undefined {
|
|
2436
|
+
if (!toolResult.details || typeof toolResult.details !== "object") return undefined;
|
|
2437
|
+
const meta = (toolResult.details as { meta?: { truncation?: { totalLines?: unknown } } }).meta;
|
|
2438
|
+
const totalLines = meta?.truncation?.totalLines;
|
|
2439
|
+
return typeof totalLines === "number" && Number.isFinite(totalLines) ? totalLines : undefined;
|
|
2440
|
+
}
|
|
2441
|
+
|
|
2442
|
+
function readFileSizeFromDetails(toolResult: ToolResultMessage): number | undefined {
|
|
2443
|
+
const details = toolResult.details;
|
|
2444
|
+
if (!details || typeof details !== "object" || !("fileSize" in details)) return undefined;
|
|
2445
|
+
const { fileSize } = details;
|
|
2446
|
+
return typeof fileSize === "number" && Number.isSafeInteger(fileSize) && fileSize >= 0 ? fileSize : undefined;
|
|
2447
|
+
}
|
|
2448
|
+
|
|
2449
|
+
function buildReadResultFromToolResult(path: string, toolResult: ToolResultMessage, rangeApplied = false) {
|
|
1716
2450
|
const text = toolResultToText(toolResult);
|
|
1717
2451
|
if (toolResult.isError) {
|
|
1718
2452
|
return buildReadErrorResult(path, text || "Read failed");
|
|
1719
2453
|
}
|
|
1720
|
-
|
|
2454
|
+
// Counting the payload is only the file's length when the payload is the
|
|
2455
|
+
// whole file. Under a composed window it is the window's, and answering a
|
|
2456
|
+
// 20-line page of a 100-line file with `total_lines: 20` tells a paginating
|
|
2457
|
+
// server it has reached the end.
|
|
2458
|
+
const totalLines = readTotalLinesFromDetails(toolResult) ?? (text ? text.split("\n").length : 0);
|
|
1721
2459
|
return create(ReadResultSchema, {
|
|
1722
2460
|
result: {
|
|
1723
2461
|
case: "success",
|
|
1724
2462
|
value: create(ReadSuccessSchema, {
|
|
1725
2463
|
path,
|
|
1726
2464
|
totalLines,
|
|
1727
|
-
fileSize: BigInt(Buffer.byteLength(text, "utf-8")),
|
|
2465
|
+
fileSize: BigInt(readFileSizeFromDetails(toolResult) ?? Buffer.byteLength(text, "utf-8")),
|
|
1728
2466
|
truncated: toolResultWasTruncated(toolResult),
|
|
1729
2467
|
output: { case: "content", value: text },
|
|
2468
|
+
// Set when this client composed the frame's window onto the read,
|
|
2469
|
+
// left false when it read the file whole. The proto names the
|
|
2470
|
+
// field but nothing here pins the server's use of it, so the only
|
|
2471
|
+
// safe contract is that it describes what we actually did.
|
|
2472
|
+
rangeApplied,
|
|
1730
2473
|
}),
|
|
1731
2474
|
},
|
|
1732
2475
|
});
|
|
@@ -1952,7 +2695,7 @@ function buildLsRejectedResult(path: string, reason: string) {
|
|
|
1952
2695
|
}
|
|
1953
2696
|
|
|
1954
2697
|
function buildGrepResultFromToolResult(
|
|
1955
|
-
args: { pattern: string; path?: string; outputMode?: string },
|
|
2698
|
+
args: { pattern: string; path?: string; outputMode?: string; offset?: number },
|
|
1956
2699
|
toolResult: ToolResultMessage,
|
|
1957
2700
|
) {
|
|
1958
2701
|
const text = toolResultToText(toolResult);
|
|
@@ -1980,6 +2723,11 @@ function buildGrepResultFromToolResult(
|
|
|
1980
2723
|
totalFiles: files.length,
|
|
1981
2724
|
clientTruncated,
|
|
1982
2725
|
ripgrepTruncated: false,
|
|
2726
|
+
// Echoes the offset this client actually applied; absent when
|
|
2727
|
+
// the frame requested none. The proto names the field but
|
|
2728
|
+
// nothing here pins the server's use of it, so it reports what
|
|
2729
|
+
// we did rather than asserting a pagination protocol.
|
|
2730
|
+
offsetApplied: args.offset,
|
|
1983
2731
|
}),
|
|
1984
2732
|
},
|
|
1985
2733
|
});
|
|
@@ -2008,6 +2756,7 @@ function buildGrepResultFromToolResult(
|
|
|
2008
2756
|
totalMatches,
|
|
2009
2757
|
clientTruncated,
|
|
2010
2758
|
ripgrepTruncated: false,
|
|
2759
|
+
offsetApplied: args.offset,
|
|
2011
2760
|
}),
|
|
2012
2761
|
},
|
|
2013
2762
|
});
|
|
@@ -2055,6 +2804,7 @@ function buildGrepResultFromToolResult(
|
|
|
2055
2804
|
totalMatchedLines,
|
|
2056
2805
|
clientTruncated,
|
|
2057
2806
|
ripgrepTruncated: false,
|
|
2807
|
+
offsetApplied: args.offset,
|
|
2058
2808
|
}),
|
|
2059
2809
|
},
|
|
2060
2810
|
});
|
|
@@ -2184,6 +2934,7 @@ function decodeMcpCall(args: {
|
|
|
2184
2934
|
toolCallId: string;
|
|
2185
2935
|
providerIdentifier: string;
|
|
2186
2936
|
toolName: string;
|
|
2937
|
+
smartModeApprovalOnly?: boolean;
|
|
2187
2938
|
}): CursorMcpCall {
|
|
2188
2939
|
const decodedArgs: Record<string, unknown> = {};
|
|
2189
2940
|
for (const [key, value] of Object.entries(args.args ?? {})) {
|
|
@@ -2196,6 +2947,7 @@ function decodeMcpCall(args: {
|
|
|
2196
2947
|
toolCallId: args.toolCallId,
|
|
2197
2948
|
args: decodedArgs,
|
|
2198
2949
|
rawArgs: args.args ?? {},
|
|
2950
|
+
approvalOnly: args.smartModeApprovalOnly === true,
|
|
2199
2951
|
};
|
|
2200
2952
|
}
|
|
2201
2953
|
|
|
@@ -2304,6 +3056,205 @@ function selectMcpCall(toolCall: CursorMcpToolCallCarrier | undefined): CursorMc
|
|
|
2304
3056
|
return toolCall?.mcpToolCall;
|
|
2305
3057
|
}
|
|
2306
3058
|
|
|
3059
|
+
/**
|
|
3060
|
+
* The streamed `ToolCall` variants whose block the exec channel owns.
|
|
3061
|
+
*
|
|
3062
|
+
* Each of these is announced on the interaction stream AND dispatched as its
|
|
3063
|
+
* own `ExecServerMessage` frame — the Pi family (45-51), plus the two MCP
|
|
3064
|
+
* resource frames — so the block is synthesized once, by the exec handler,
|
|
3065
|
+
* which is the side that has the result.
|
|
3066
|
+
*
|
|
3067
|
+
* `connect_scm` is deliberately NOT here: `ExecServerMessage` has no
|
|
3068
|
+
* connect-SCM case at all (field 44 is `git_diff_request`), so nothing on the
|
|
3069
|
+
* exec channel ever answers it and the streamed announcement is the only
|
|
3070
|
+
* signal. `search_conversations` is not here either: frame 53 answers it, but
|
|
3071
|
+
* it carries its own `tool_call_id` on the streamed envelope and pairs there,
|
|
3072
|
+
* so the exec branch does not synthesize a block for it.
|
|
3073
|
+
*/
|
|
3074
|
+
const EXEC_OWNED_TOOL_CALL_CASES: ReadonlySet<string> = new Set([
|
|
3075
|
+
"piReadToolCall",
|
|
3076
|
+
"piBashToolCall",
|
|
3077
|
+
"piEditToolCall",
|
|
3078
|
+
"piWriteToolCall",
|
|
3079
|
+
"piGrepToolCall",
|
|
3080
|
+
"piFindToolCall",
|
|
3081
|
+
"piLsToolCall",
|
|
3082
|
+
"listMcpResourcesToolCall",
|
|
3083
|
+
"readMcpResourceToolCall",
|
|
3084
|
+
]);
|
|
3085
|
+
|
|
3086
|
+
function isExecOwnedToolCall(toolCall: { tool?: { case?: string } } | undefined): boolean {
|
|
3087
|
+
const variant = toolCall?.tool?.case;
|
|
3088
|
+
return variant !== undefined && EXEC_OWNED_TOOL_CALL_CASES.has(variant);
|
|
3089
|
+
}
|
|
3090
|
+
|
|
3091
|
+
/**
|
|
3092
|
+
* Retain a freshly opened streamed tool-call block.
|
|
3093
|
+
*
|
|
3094
|
+
* Keyed by the interaction envelope's `call_id`, which is the only key every
|
|
3095
|
+
* `ToolCall*Update` for that call shares. The block's own `id` is deliberately
|
|
3096
|
+
* not the key: MCP, Pi and connect-SCM blocks are filed under the id carried
|
|
3097
|
+
* inside the call's `args`, because that is what the exec channel pairs its
|
|
3098
|
+
* result under and what the transcript files the visible block under.
|
|
3099
|
+
*
|
|
3100
|
+
* `currentToolCall` is still set, as the fallback for frames that carry no
|
|
3101
|
+
* `call_id` (proto3-optional, and unset on what older builds send).
|
|
3102
|
+
*/
|
|
3103
|
+
/**
|
|
3104
|
+
* Close every tool-call block still open when the stream ends.
|
|
3105
|
+
*
|
|
3106
|
+
* Not just the last one started: with interleaved calls several can be open at
|
|
3107
|
+
* once, and an unclosed block leaves its live card animating and its call
|
|
3108
|
+
* unpaired.
|
|
3109
|
+
*
|
|
3110
|
+
* Only blocks fed by a streamed argument buffer get reparsed. Todo,
|
|
3111
|
+
* connect-SCM and MCP-settled frames arrive with complete `arguments` and
|
|
3112
|
+
* never set the partial buffer; `parseStreamingJson(undefined)` returns `{}`,
|
|
3113
|
+
* so reparsing unconditionally would erase the arguments of every such block
|
|
3114
|
+
* caught open by a truncated stream.
|
|
3115
|
+
*
|
|
3116
|
+
* Server-owned blocks are also paired here. `connect-scm` and `todo` are
|
|
3117
|
+
* stamped {@link kCursorExecResolved} the moment they open, so `agent-loop.ts`
|
|
3118
|
+
* synthesizes no placeholder for them and only their `toolCallCompleted` frame
|
|
3119
|
+
* pairs a result. A transport that closes before that frame would leave the
|
|
3120
|
+
* call unpaired, and `buildSessionContext` strips a dangling call from every
|
|
3121
|
+
* rebuilt transcript — the interaction disappears. An interrupted result is
|
|
3122
|
+
* emitted instead.
|
|
3123
|
+
*
|
|
3124
|
+
* MCP blocks are excluded even when resolved: the exec dispatch that marked
|
|
3125
|
+
* them owns their result, and `drainInFlightDispatches` awaits it before this
|
|
3126
|
+
* runs, so pairing here would duplicate one against the same `toolCallId`.
|
|
3127
|
+
*/
|
|
3128
|
+
export function flushOpenToolCalls(
|
|
3129
|
+
output: AssistantMessage,
|
|
3130
|
+
stream: AssistantMessageEventStream,
|
|
3131
|
+
state: BlockState,
|
|
3132
|
+
): void {
|
|
3133
|
+
const openBlocks = new Set<ToolCallState>(state.openToolCalls.values());
|
|
3134
|
+
if (state.currentToolCall) openBlocks.add(state.currentToolCall);
|
|
3135
|
+
for (const block of openBlocks) {
|
|
3136
|
+
const idx = output.content.indexOf(block);
|
|
3137
|
+
const partialJson = block[kStreamingPartialJson];
|
|
3138
|
+
if (partialJson !== undefined) {
|
|
3139
|
+
block.arguments = parseStreamingJson(partialJson);
|
|
3140
|
+
clearStreamingPartialJson(block);
|
|
3141
|
+
}
|
|
3142
|
+
const kind = block[kStreamingBlockKind];
|
|
3143
|
+
if (kind === "connect-scm" || kind === "todo") {
|
|
3144
|
+
state.onToolResult?.({
|
|
3145
|
+
role: "toolResult",
|
|
3146
|
+
toolCallId: block.id,
|
|
3147
|
+
toolName: block.name,
|
|
3148
|
+
content: [{ type: "text", text: "The connection to Cursor closed before this call completed." }],
|
|
3149
|
+
isError: true,
|
|
3150
|
+
timestamp: Date.now(),
|
|
3151
|
+
});
|
|
3152
|
+
}
|
|
3153
|
+
stream.push({ type: "toolcall_end", contentIndex: idx, toolCall: block, partial: output });
|
|
3154
|
+
}
|
|
3155
|
+
state.openToolCalls.clear();
|
|
3156
|
+
state.setToolCall(null);
|
|
3157
|
+
}
|
|
3158
|
+
|
|
3159
|
+
function retainStreamedCall(state: BlockState, block: ToolCallState, envelopeId: string | undefined): void {
|
|
3160
|
+
if (envelopeId) state.openToolCalls.set(envelopeId, block);
|
|
3161
|
+
state.setToolCall(block);
|
|
3162
|
+
}
|
|
3163
|
+
|
|
3164
|
+
/**
|
|
3165
|
+
* The open block a streamed update addresses, or `null` to ignore the update.
|
|
3166
|
+
*
|
|
3167
|
+
* Cursor interleaves calls: `start A, start B, complete A` is legal, so the
|
|
3168
|
+
* update must reach block A even though B opened last. An id naming no open
|
|
3169
|
+
* block is ignored rather than misapplied — settling the wrong block would pair
|
|
3170
|
+
* it with another call's result.
|
|
3171
|
+
*
|
|
3172
|
+
* A missing id falls back to the current block: the correlation key is
|
|
3173
|
+
* optional, and dropping those updates would strand a block stamped
|
|
3174
|
+
* {@link kCursorExecResolved}, which nothing else settles and whose whole
|
|
3175
|
+
* interaction is then stripped from every rebuilt transcript.
|
|
3176
|
+
*/
|
|
3177
|
+
function resolveStreamedCall(state: BlockState, envelopeId: string | undefined): ToolCallState | null {
|
|
3178
|
+
if (!envelopeId) return state.currentToolCall;
|
|
3179
|
+
const keyed = state.openToolCalls.get(envelopeId);
|
|
3180
|
+
if (keyed) return keyed;
|
|
3181
|
+
// Blocks opened before this build tracked envelope ids, and blocks opened
|
|
3182
|
+
// from a frame that carried none, are only reachable as `currentToolCall`.
|
|
3183
|
+
const current = state.currentToolCall;
|
|
3184
|
+
return current && current[kStreamingEnvelopeId] === undefined ? current : null;
|
|
3185
|
+
}
|
|
3186
|
+
|
|
3187
|
+
/** Release a settled block from both the keyed map and the current slot. */
|
|
3188
|
+
function releaseStreamedCall(state: BlockState, block: ToolCallState): void {
|
|
3189
|
+
const envelopeId = block[kStreamingEnvelopeId];
|
|
3190
|
+
if (envelopeId) state.openToolCalls.delete(envelopeId);
|
|
3191
|
+
if (state.currentToolCall === block) state.setToolCall(null);
|
|
3192
|
+
}
|
|
3193
|
+
|
|
3194
|
+
interface CursorConnectScmRepository {
|
|
3195
|
+
owner?: string;
|
|
3196
|
+
repo?: string;
|
|
3197
|
+
}
|
|
3198
|
+
|
|
3199
|
+
interface CursorConnectScmCall {
|
|
3200
|
+
args?: {
|
|
3201
|
+
toolCallId?: string;
|
|
3202
|
+
/** `ConnectScmArgs.target` oneof; `github` is its only member today. */
|
|
3203
|
+
target?: { case?: string; value?: { repository?: CursorConnectScmRepository } };
|
|
3204
|
+
github?: { repository?: CursorConnectScmRepository };
|
|
3205
|
+
};
|
|
3206
|
+
/** `ConnectScmResult.result` oneof: `success` | `error` | `rejected`. */
|
|
3207
|
+
result?: { result?: { case?: string; value?: { error?: string; reason?: string } } };
|
|
3208
|
+
}
|
|
3209
|
+
|
|
3210
|
+
interface CursorConnectScmCarrier {
|
|
3211
|
+
tool?: { case?: string; value?: unknown };
|
|
3212
|
+
connectScmToolCall?: CursorConnectScmCall;
|
|
3213
|
+
}
|
|
3214
|
+
|
|
3215
|
+
/**
|
|
3216
|
+
* The streamed `connect_scm_tool_call` variant, if this update carries one.
|
|
3217
|
+
*
|
|
3218
|
+
* Same oneof-vs-flattened handling as {@link selectMcpCall}: a wire-decoded
|
|
3219
|
+
* `ToolCall` exposes its variant as `{ case, value }`, while hand-shaped test
|
|
3220
|
+
* fixtures use the flat property.
|
|
3221
|
+
*/
|
|
3222
|
+
function selectConnectScmCall(toolCall: CursorConnectScmCarrier | undefined): CursorConnectScmCall | undefined {
|
|
3223
|
+
const oneof = toolCall?.tool;
|
|
3224
|
+
if (oneof?.case === "connectScmToolCall") return oneof.value as CursorConnectScmCall;
|
|
3225
|
+
return toolCall?.connectScmToolCall;
|
|
3226
|
+
}
|
|
3227
|
+
|
|
3228
|
+
/** The repository a connect-SCM call targets, across the `target` oneof. */
|
|
3229
|
+
function selectConnectScmRepository(call: CursorConnectScmCall | undefined): CursorConnectScmRepository | undefined {
|
|
3230
|
+
const target = call?.args?.target;
|
|
3231
|
+
if (target?.case === "github") return target.value?.repository;
|
|
3232
|
+
return call?.args?.github?.repository;
|
|
3233
|
+
}
|
|
3234
|
+
|
|
3235
|
+
/**
|
|
3236
|
+
* Render a settled `ConnectScmResult` as the text of its paired tool result.
|
|
3237
|
+
*
|
|
3238
|
+
* Returns `isError` because the three outcomes are not interchangeable: only
|
|
3239
|
+
* `success` means the repository was connected, and reporting a rejection as
|
|
3240
|
+
* success would tell the model to proceed against a repo it cannot reach.
|
|
3241
|
+
*/
|
|
3242
|
+
function describeConnectScmResult(call: CursorConnectScmCall | undefined): { text: string; isError: boolean } {
|
|
3243
|
+
const result = call?.result?.result;
|
|
3244
|
+
switch (result?.case) {
|
|
3245
|
+
case "success":
|
|
3246
|
+
return { text: "SCM connected", isError: false };
|
|
3247
|
+
case "error":
|
|
3248
|
+
return { text: result.value?.error || "SCM connection failed", isError: true };
|
|
3249
|
+
case "rejected":
|
|
3250
|
+
return { text: result.value?.reason || "SCM connection rejected", isError: true };
|
|
3251
|
+
default:
|
|
3252
|
+
// A completion carrying no result settles the block anyway: it is
|
|
3253
|
+
// stamped resolved, so nothing downstream would ever pair it.
|
|
3254
|
+
return { text: "SCM connection reported no result", isError: true };
|
|
3255
|
+
}
|
|
3256
|
+
}
|
|
3257
|
+
|
|
2307
3258
|
/**
|
|
2308
3259
|
* Extract the authoritative full todo list from a completed native todo call.
|
|
2309
3260
|
*
|
|
@@ -2637,6 +3588,42 @@ export function synthesizeCursorExecToolCall(
|
|
|
2637
3588
|
stream.push({ type: "toolcall_end", contentIndex: idx, toolCall: block, partial: output });
|
|
2638
3589
|
}
|
|
2639
3590
|
|
|
3591
|
+
/**
|
|
3592
|
+
* Pair a `toolResult` for a synthesized block the client answered itself,
|
|
3593
|
+
* without ever consulting a handler.
|
|
3594
|
+
*
|
|
3595
|
+
* {@link resolveExecHandler} does this for every frame backed by a local tool.
|
|
3596
|
+
* Frames answered from a fixed verdict — no handler, no local execution — still
|
|
3597
|
+
* need the pair for the same reason: the block was stamped
|
|
3598
|
+
* {@link kCursorExecResolved}, so `agent-loop.ts` emits no placeholder for it
|
|
3599
|
+
* and `buildSessionContext` strips an unpaired call, taking the whole
|
|
3600
|
+
* interaction out of every rebuilt transcript.
|
|
3601
|
+
*
|
|
3602
|
+
* `isError` defaults true because most such verdicts are refusals; the MCP
|
|
3603
|
+
* resource frames run locally and can genuinely succeed, and a success filed
|
|
3604
|
+
* as an error would render as a failed call in every rebuilt transcript.
|
|
3605
|
+
*/
|
|
3606
|
+
async function pairSynthesizedExecResult(
|
|
3607
|
+
state: BlockState,
|
|
3608
|
+
onToolResult: CursorToolResultHandler | undefined,
|
|
3609
|
+
toolCallId: string,
|
|
3610
|
+
toolName: string,
|
|
3611
|
+
text: string,
|
|
3612
|
+
isError = true,
|
|
3613
|
+
): Promise<void> {
|
|
3614
|
+
const synthesized: ToolResultMessage = {
|
|
3615
|
+
role: "toolResult",
|
|
3616
|
+
toolCallId,
|
|
3617
|
+
toolName,
|
|
3618
|
+
content: [{ type: "text", text }],
|
|
3619
|
+
isError,
|
|
3620
|
+
timestamp: Date.now(),
|
|
3621
|
+
};
|
|
3622
|
+
const sink = onToolResult ?? state.onToolResult;
|
|
3623
|
+
if (!sink) return;
|
|
3624
|
+
await sink(synthesized);
|
|
3625
|
+
}
|
|
3626
|
+
|
|
2640
3627
|
/** Exported for tests: drives one Cursor interaction update through the streaming state machine. */
|
|
2641
3628
|
export function processInteractionUpdate(
|
|
2642
3629
|
update: any,
|
|
@@ -2683,6 +3670,43 @@ export function processInteractionUpdate(
|
|
|
2683
3670
|
stream.push({ type: "thinking_delta", contentIndex: idx, delta, partial: output });
|
|
2684
3671
|
} else if (updateCase === "thinkingCompleted") {
|
|
2685
3672
|
endCurrentThinkingBlock(output, stream, state);
|
|
3673
|
+
} else if (updateCase === "toolCallStarted" && selectConnectScmCall(update.message.value.toolCall)) {
|
|
3674
|
+
// `connect_scm` is resolved entirely server-side and has NO exec frame:
|
|
3675
|
+
// `ExecServerMessage` carries no connect-SCM case (field 44 is
|
|
3676
|
+
// `git_diff_request`), so the streamed pair is the only signal this client
|
|
3677
|
+
// sees. The authoritative outcome rides on the COMPLETION's `result`
|
|
3678
|
+
// oneof, so the block is opened here and settled there — answering now
|
|
3679
|
+
// would persist a verdict before the server has given one.
|
|
3680
|
+
//
|
|
3681
|
+
// Stamped resolved so `agent-loop.ts` runs no local tool for it: there is
|
|
3682
|
+
// no local `connect_scm`, and the completion pairs the result itself.
|
|
3683
|
+
endCurrentTextBlock(output, stream, state);
|
|
3684
|
+
endCurrentThinkingBlock(output, stream, state);
|
|
3685
|
+
const scmCall = selectConnectScmCall(update.message.value.toolCall);
|
|
3686
|
+
const repository = selectConnectScmRepository(scmCall);
|
|
3687
|
+
const block: ToolCallState = {
|
|
3688
|
+
type: "toolCall",
|
|
3689
|
+
id: scmCall?.args?.toolCallId || update.message.value.callId || crypto.randomUUID(),
|
|
3690
|
+
name: "connect_scm",
|
|
3691
|
+
arguments: repository ? { owner: repository.owner, repo: repository.repo } : {},
|
|
3692
|
+
[kStreamingBlockIndex]: output.content.length,
|
|
3693
|
+
[kStreamingBlockKind]: "connect-scm",
|
|
3694
|
+
[kStreamingEnvelopeId]: update.message.value.callId || undefined,
|
|
3695
|
+
[kCursorExecResolved]: true,
|
|
3696
|
+
};
|
|
3697
|
+
output.content.push(block);
|
|
3698
|
+
retainStreamedCall(state, block, update.message.value.callId);
|
|
3699
|
+
stream.push({ type: "toolcall_start", contentIndex: output.content.length - 1, partial: output });
|
|
3700
|
+
} else if (updateCase === "toolCallStarted" && isExecOwnedToolCall(update.message.value.toolCall)) {
|
|
3701
|
+
// The exec channel already synthesized this block (and marked it resolved)
|
|
3702
|
+
// when it ran the tool locally, so the streamed announcement must not
|
|
3703
|
+
// create a second one. Modern builds stream a `pi_*_tool_call` envelope
|
|
3704
|
+
// alongside every `ExecServerMessage` 45-51 frame; before this branch the
|
|
3705
|
+
// duplicate was avoided only because the decoder recognised neither, which
|
|
3706
|
+
// would silently start double-rendering the moment a variant was added.
|
|
3707
|
+
endCurrentTextBlock(output, stream, state);
|
|
3708
|
+
endCurrentThinkingBlock(output, stream, state);
|
|
3709
|
+
log("exec", "streamedToolCallOwnedByExec", { case: update.message.value.toolCall?.tool?.case });
|
|
2686
3710
|
} else if (updateCase === "toolCallStarted") {
|
|
2687
3711
|
endCurrentTextBlock(output, stream, state);
|
|
2688
3712
|
endCurrentThinkingBlock(output, stream, state);
|
|
@@ -2707,12 +3731,13 @@ export function processInteractionUpdate(
|
|
|
2707
3731
|
[kStreamingBlockIndex]: output.content.length,
|
|
2708
3732
|
[kStreamingPartialJson]: "",
|
|
2709
3733
|
[kStreamingBlockKind]: "mcp",
|
|
3734
|
+
[kStreamingEnvelopeId]: update.message.value.callId || undefined,
|
|
2710
3735
|
};
|
|
2711
3736
|
if (resolvedByExec) {
|
|
2712
3737
|
markCursorExecResolved(block);
|
|
2713
3738
|
}
|
|
2714
3739
|
output.content.push(block);
|
|
2715
|
-
state.
|
|
3740
|
+
retainStreamedCall(state, block, update.message.value.callId);
|
|
2716
3741
|
stream.push({ type: "toolcall_start", contentIndex: output.content.length - 1, partial: output });
|
|
2717
3742
|
return;
|
|
2718
3743
|
}
|
|
@@ -2733,54 +3758,92 @@ export function processInteractionUpdate(
|
|
|
2733
3758
|
arguments: buildTodoDisplayArgs(toolCall),
|
|
2734
3759
|
[kStreamingBlockIndex]: output.content.length,
|
|
2735
3760
|
[kStreamingBlockKind]: "todo",
|
|
3761
|
+
// Only the real envelope id is a correlation key; the minted
|
|
3762
|
+
// fallback below names no frame the server will ever send back.
|
|
3763
|
+
[kStreamingEnvelopeId]: update.message.value.callId || undefined,
|
|
2736
3764
|
[kCursorExecResolved]: true,
|
|
2737
3765
|
};
|
|
2738
3766
|
output.content.push(block);
|
|
2739
|
-
state.
|
|
3767
|
+
retainStreamedCall(state, block, update.message.value.callId);
|
|
2740
3768
|
stream.push({ type: "toolcall_start", contentIndex: output.content.length - 1, partial: output });
|
|
2741
3769
|
}
|
|
2742
3770
|
}
|
|
2743
3771
|
} else if (updateCase === "toolCallDelta" || updateCase === "partialToolCall") {
|
|
2744
|
-
|
|
3772
|
+
// Same correlation rule as the completion path below: an argument delta
|
|
3773
|
+
// belonging to a different call must not be appended to this block's
|
|
3774
|
+
// buffer, which would corrupt the JSON both of them parse.
|
|
3775
|
+
const target = resolveStreamedCall(state, update.message.value.callId);
|
|
3776
|
+
if (target?.[kStreamingBlockKind] === "mcp") {
|
|
2745
3777
|
// Cursor's `args_text_delta` is "aggregated args text so far" per agent.proto: each
|
|
2746
3778
|
// delta is a cumulative snapshot of the JSON-text args. Strip the prefix we already
|
|
2747
3779
|
// have to recover the new suffix; fall back to treating the value as an incremental
|
|
2748
3780
|
// fragment when it doesn't extend the buffer.
|
|
2749
3781
|
const snapshot: string = update.message.value.argsTextDelta || "";
|
|
2750
|
-
const current =
|
|
3782
|
+
const current = target[kStreamingPartialJson] ?? "";
|
|
2751
3783
|
const chunk = snapshot.startsWith(current) ? snapshot.slice(current.length) : snapshot;
|
|
2752
3784
|
if (chunk.length === 0) {
|
|
2753
3785
|
return;
|
|
2754
3786
|
}
|
|
2755
3787
|
const nextBuffer = current + chunk;
|
|
2756
|
-
|
|
3788
|
+
target[kStreamingPartialJson] = nextBuffer;
|
|
2757
3789
|
// Throttle mid-stream parses to keep total parse work O(N) instead of O(N²)
|
|
2758
3790
|
// in the argument-buffer length; the authoritative full parse runs in
|
|
2759
3791
|
// `toolCallCompleted` (mcp branch) and the fallback end-of-stream path.
|
|
2760
|
-
const throttled = parseStreamingJsonThrottled(nextBuffer,
|
|
3792
|
+
const throttled = parseStreamingJsonThrottled(nextBuffer, target[kStreamingLastParseLen] ?? 0);
|
|
2761
3793
|
if (throttled) {
|
|
2762
|
-
|
|
2763
|
-
|
|
3794
|
+
target.arguments = throttled.value;
|
|
3795
|
+
target[kStreamingLastParseLen] = throttled.parsedLen;
|
|
2764
3796
|
}
|
|
2765
|
-
const idx = output.content.indexOf(
|
|
3797
|
+
const idx = output.content.indexOf(target);
|
|
2766
3798
|
stream.push({ type: "toolcall_delta", contentIndex: idx, delta: chunk, partial: output });
|
|
2767
3799
|
}
|
|
2768
3800
|
} else if (updateCase === "toolCallCompleted") {
|
|
2769
|
-
|
|
3801
|
+
// Correlate on the envelope's `call_id`, NOT the block id: MCP, Pi and SCM
|
|
3802
|
+
// blocks are filed under the id inside the call's `args` (which is what
|
|
3803
|
+
// the exec channel pairs its result under), and that need not equal the
|
|
3804
|
+
// envelope id. Cursor also interleaves calls, so the block this settles
|
|
3805
|
+
// is looked up by id rather than assumed to be the last one opened —
|
|
3806
|
+
// otherwise an unrelated completion closes whichever block is current and
|
|
3807
|
+
// pairs it with the wrong result.
|
|
3808
|
+
const settled = resolveStreamedCall(state, update.message.value.callId);
|
|
3809
|
+
if (settled) {
|
|
2770
3810
|
const toolCall = update.message.value.toolCall;
|
|
2771
|
-
if (
|
|
3811
|
+
if (settled[kStreamingBlockKind] === "mcp") {
|
|
2772
3812
|
// Authoritative full parse of the accumulated argument buffer; the delta
|
|
2773
3813
|
// path throttles mid-stream parses, so `arguments` may lag the buffer.
|
|
2774
|
-
const partial =
|
|
3814
|
+
const partial = settled[kStreamingPartialJson];
|
|
2775
3815
|
if (partial !== undefined) {
|
|
2776
|
-
|
|
3816
|
+
settled.arguments = parseStreamingJson(partial);
|
|
2777
3817
|
}
|
|
2778
3818
|
const decodedArgs = decodeMcpArgsMap(selectMcpCall(toolCall)?.args?.args);
|
|
2779
|
-
|
|
2780
|
-
|
|
3819
|
+
settled.arguments = mergeCursorMcpToolCallArgs(
|
|
3820
|
+
settled.arguments as Record<string, unknown> | undefined,
|
|
2781
3821
|
decodedArgs,
|
|
2782
3822
|
);
|
|
2783
|
-
} else if (
|
|
3823
|
+
} else if (settled[kStreamingBlockKind] === "connect-scm") {
|
|
3824
|
+
// The authoritative outcome arrives only here, on the completion's
|
|
3825
|
+
// `ConnectScmResult` oneof. The block was stamped resolved at start,
|
|
3826
|
+
// so nothing downstream pairs it: settling is this branch's job, and
|
|
3827
|
+
// a completion with no `toolCall` still settles rather than leaking a
|
|
3828
|
+
// dangling call into every rebuilt transcript.
|
|
3829
|
+
//
|
|
3830
|
+
// Late args are merged too — a start frame may announce the call
|
|
3831
|
+
// before the target repository is known.
|
|
3832
|
+
const scmCall = selectConnectScmCall(toolCall);
|
|
3833
|
+
const repository = selectConnectScmRepository(scmCall);
|
|
3834
|
+
if (repository) {
|
|
3835
|
+
settled.arguments = { owner: repository.owner, repo: repository.repo };
|
|
3836
|
+
}
|
|
3837
|
+
const { text, isError } = describeConnectScmResult(scmCall);
|
|
3838
|
+
state.onToolResult?.({
|
|
3839
|
+
role: "toolResult",
|
|
3840
|
+
toolCallId: settled.id,
|
|
3841
|
+
toolName: "connect_scm",
|
|
3842
|
+
content: [{ type: "text", text }],
|
|
3843
|
+
isError,
|
|
3844
|
+
timestamp: Date.now(),
|
|
3845
|
+
});
|
|
3846
|
+
} else if (settled[kStreamingBlockKind] === "todo") {
|
|
2784
3847
|
// Only the server's success snapshot is authoritative: the request args
|
|
2785
3848
|
// may differ from what was actually stored after a merge, and on
|
|
2786
3849
|
// `UpdateTodosError` nothing was stored at all. No snapshot => leave
|
|
@@ -2794,7 +3857,7 @@ export function processInteractionUpdate(
|
|
|
2794
3857
|
const snapshot = toolCall ? extractTodoSnapshot(toolCall) : null;
|
|
2795
3858
|
const error = toolCall ? extractTodoError(toolCall) : null;
|
|
2796
3859
|
if (snapshot) {
|
|
2797
|
-
|
|
3860
|
+
settled.arguments = { todos: snapshot.todos, merged: snapshot.merged };
|
|
2798
3861
|
}
|
|
2799
3862
|
// The host settles EVERY completed native todo call, successful or
|
|
2800
3863
|
// not: the interactive card only resolves on a matching
|
|
@@ -2810,7 +3873,7 @@ export function processInteractionUpdate(
|
|
|
2810
3873
|
let persisted: ToolResultMessage | undefined;
|
|
2811
3874
|
let hostError: string | null = null;
|
|
2812
3875
|
try {
|
|
2813
|
-
persisted = state.onTodoSnapshot?.(snapshot,
|
|
3876
|
+
persisted = state.onTodoSnapshot?.(snapshot, settled.id, error) ?? undefined;
|
|
2814
3877
|
} catch (callbackError) {
|
|
2815
3878
|
// A throwing host callback (e.g. session persistence failing on
|
|
2816
3879
|
// disk error) must not leave the resolved block unpaired: the
|
|
@@ -2820,14 +3883,12 @@ export function processInteractionUpdate(
|
|
|
2820
3883
|
hostError = callbackError instanceof Error ? callbackError.message : String(callbackError);
|
|
2821
3884
|
log("error", "onTodoSnapshot", { error: hostError });
|
|
2822
3885
|
}
|
|
2823
|
-
state.onToolResult?.(
|
|
2824
|
-
persisted ?? buildTodoToolResult(state.currentToolCall.id, snapshot, hostError ?? error),
|
|
2825
|
-
);
|
|
3886
|
+
state.onToolResult?.(persisted ?? buildTodoToolResult(settled.id, snapshot, hostError ?? error));
|
|
2826
3887
|
}
|
|
2827
|
-
const idx = output.content.indexOf(
|
|
2828
|
-
clearStreamingPartialJson(
|
|
2829
|
-
stream.push({ type: "toolcall_end", contentIndex: idx, toolCall:
|
|
2830
|
-
state
|
|
3888
|
+
const idx = output.content.indexOf(settled);
|
|
3889
|
+
clearStreamingPartialJson(settled);
|
|
3890
|
+
stream.push({ type: "toolcall_end", contentIndex: idx, toolCall: settled, partial: output });
|
|
3891
|
+
releaseStreamedCall(state, settled);
|
|
2831
3892
|
}
|
|
2832
3893
|
} else if (updateCase === "turnEnded") {
|
|
2833
3894
|
output.stopReason = "stop";
|
|
@@ -2877,7 +3938,18 @@ function readCursorBlob(blobStore: Map<string, Uint8Array>, blobId: Uint8Array):
|
|
|
2877
3938
|
return data;
|
|
2878
3939
|
}
|
|
2879
3940
|
|
|
2880
|
-
|
|
3941
|
+
/**
|
|
3942
|
+
* Local tools Cursor already drives natively over the exec channel, so
|
|
3943
|
+
* advertising them again as MCP tools would give the model two ways to call the
|
|
3944
|
+
* same thing.
|
|
3945
|
+
*
|
|
3946
|
+
* `lsp` is deliberately NOT here. The native `diagnosticsArgs` frame covers
|
|
3947
|
+
* exactly one of the tool's actions (`action: "diagnostics"`); the rest —
|
|
3948
|
+
* `definition`, `references`, `rename`, `code_actions`, `hover`,
|
|
3949
|
+
* `implementation`, `type_definition`, `symbols`, ... — have no native frame at
|
|
3950
|
+
* all, so filtering the whole tool out hid every one of them from the model.
|
|
3951
|
+
*/
|
|
3952
|
+
const CURSOR_NATIVE_TOOL_NAMES = new Set(["bash", "read", "write", "delete", "ls", "grep", "todo"]);
|
|
2881
3953
|
|
|
2882
3954
|
export function buildMcpToolDefinitions(tools: Tool[] | undefined): McpToolDefinition[] {
|
|
2883
3955
|
if (!tools || tools.length === 0) {
|