@xiaoso/dsh-tool-plus 0.1.16 → 0.1.17-beta.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/lib/{abort-DZ54CaKU.mjs → abort-BTblp0Rt.mjs} +2 -2
- package/lib/{alibaba-coding-plan-DODpkcqi.mjs → alibaba-coding-plan-t3R4YKCB.mjs} +3 -3
- package/lib/{alibaba-token-plan-C6lpKCda.mjs → alibaba-token-plan-CO4djG1y.mjs} +4 -4
- package/lib/{alibaba-token-plan-CHGyWOfX.mjs → alibaba-token-plan-CUjYJX0-.mjs} +1 -1
- package/lib/{anthropic-Dv_GGX6g.mjs → anthropic-nf7w6so1.mjs} +48 -21
- package/lib/{api-key-validation-MnMf_ihX.mjs → api-key-validation-C_tZ7Y9a.mjs} +2 -2
- package/lib/behavior-BwvTUtag.mjs +111 -0
- package/lib/{browser-render-BZXt548F.mjs → browser-render-BQaEVK_n.mjs} +4 -4
- package/lib/{reference-CtvM4zLU.mjs → bundled-references-CrQ4dQcT.mjs} +57 -4
- package/lib/cascade-BWjRTD1q.mjs +30265 -0
- package/lib/{cline-pass-BT9CBEOj.mjs → cline-pass-Bjm-O8GG.mjs} +2 -2
- package/lib/cloudflare-CKXRJ0l9.mjs +37 -0
- package/lib/{cloudflare-ai-gateway-DReN6ohJ.mjs → cloudflare-ai-gateway-BpnBSjwV.mjs} +8 -4
- package/lib/cloudflare-workers-ai-odj9XswL.mjs +32 -0
- package/lib/collapse-_nkb2dn7.mjs +1945 -0
- package/lib/{coreweave-C82HrLpb.mjs → coreweave-CGDZOD8-.mjs} +1 -1
- package/lib/{coreweave-C68UgRQO.mjs → coreweave-DwffcgW5.mjs} +4 -4
- package/lib/cursor-BgOuV7Qf.mjs +794 -0
- package/lib/{cursor-_gHHq-Dq.mjs → cursor-CF1iHHbv.mjs} +68 -19
- package/lib/{cursor-proto-Cr_WEMuv.mjs → cursor-proto-BuNTjX3L.mjs} +823 -23
- package/lib/{devin-QpfTSl9H.mjs → devin-7OEeEb80.mjs} +112 -31
- package/lib/{devin-proto-DYhp4Ty6.mjs → devin-proto-C-QreeAv.mjs} +71 -5
- package/lib/{dom-DG3sPESc.mjs → dom-BmbZv4v9.mjs} +598 -182
- package/lib/{error-O63pC8zg.mjs → error-8LVo7tug.mjs} +725 -104
- package/lib/factory-droid-ChhmsGWb.mjs +71 -0
- package/lib/{fs-promises-shim-B2YFWkcd.mjs → fs-promises-shim-CANAxFWh.mjs} +10 -1
- package/lib/{github-copilot-BMEcw8N1.mjs → github-copilot-QwevdT30.mjs} +4 -4
- package/lib/{github-copilot-headers-DnjGEbko.mjs → github-copilot-headers-CAUZ0rUD.mjs} +138 -26
- package/lib/{gitlab-duo-DnHqJjFY.mjs → gitlab-duo-DigV7k_o.mjs} +2 -2
- package/lib/{google-antigravity-B1mSckrK.mjs → google-antigravity-D-oe0r8A.mjs} +6 -7
- package/lib/{google-gemini-cli-DJwThCCn.mjs → google-gemini-cli-CQBHgGJT.mjs} +5 -6
- package/lib/{google-oauth-shared-BctLpVUN.mjs → google-oauth-shared-CvgVIUwJ.mjs} +2 -2
- package/lib/google-validation-CdvRj4ec.mjs +126 -0
- package/lib/id-EJwVLDHE.mjs +2548 -0
- package/lib/index.mjs +248 -258
- package/lib/{kilo-BEgEwMJQ.mjs → kilo-5r6OffCa.mjs} +2 -2
- package/lib/{kimi-T9NldEIJ.mjs → kimi-DOOtbXmm.mjs} +9 -10
- package/lib/{launch-BIWNyrBB.mjs → launch-DkAjib7h.mjs} +1 -1
- package/lib/{markit-Dvp2JPdT.mjs → markit-CCcyeAds.mjs} +359 -140
- package/lib/{models-DnXMYGcJ.mjs → models-DCpDT_im.mjs} +199203 -112275
- package/lib/{muse-code-CNvqqAp6.mjs → muse-code-CYf8zj_B.mjs} +5 -5
- package/lib/{openai-codex-D5-CyTxa.mjs → openai-codex-Cj-TvSFh.mjs} +13 -10
- package/lib/{perplexity-A2PEoGaC.mjs → perplexity-Dh_Qgsmx.mjs} +68 -25
- package/lib/{pkce-BYjmugmw.mjs → pkce-DO-jBJLV.mjs} +1 -1
- package/lib/pricing-DusBEhwm.mjs +593 -0
- package/lib/{readability-uGhqb4GC.mjs → readability-XHK0o3bv.mjs} +287 -27
- package/lib/{scrapers-C1ujjV8k.mjs → scrapers-B6PADOVm.mjs} +13 -13
- package/lib/slugify-ynYWBIA5.mjs +11 -0
- package/lib/snowflake-B_jap6lz.mjs +666 -0
- package/lib/snowflake-Cu1ZyOp6.mjs +151 -0
- package/lib/{src-AELUUb5U.mjs → src-BhinCSpi.mjs} +1172 -458
- package/lib/{stream-AVOoe5Hj.mjs → stream-D2ivg3GH.mjs} +3740 -5311
- package/lib/{turndown-D6fof1OX.mjs → turndown-Vya8SavL.mjs} +82 -30
- package/lib/types/tools/hashline/native/index.d.ts +2 -2
- package/lib/types/tools/hashline/native/policy.d.ts +19 -21
- package/lib/types/tools/omp/session/agent-storage.d.ts +3 -3
- package/lib/types/tools/shared/bun-named-shim.d.ts +5 -0
- package/lib/{types-BLrLohwK.mjs → types-tH1v9uRm.mjs} +2 -2
- package/lib/{utils-cZX3f_Ls.mjs → utils-C_Ncya1r.mjs} +1737 -509
- package/lib/utils-D2tK38xq.mjs +34006 -0
- package/lib/{utils-I4I8azHN.mjs → utils-DrDwmNtu.mjs} +8 -3
- package/lib/{xai-oauth-CbeldUSV.mjs → xai-oauth-DLLzX6my.mjs} +4 -4
- package/lib/{xiaomi-Dl3VSs04.mjs → xiaomi-CqK-KivK.mjs} +2 -2
- package/lib/{zai-FyjGSypr.mjs → zai-DoJ2xPIH.mjs} +2 -2
- package/lib/{zip-DRrycScC.mjs → zip-DG1mUV6X.mjs} +1 -1
- package/package.json +8 -8
- package/lib/amazon-bedrock-Dx0rr9dY.mjs +0 -996
- package/lib/anthropic-ClCw0PV_.mjs +0 -3624
- package/lib/anthropic-wire-lbGAFzYb.mjs +0 -927
- package/lib/azure-openai-responses-WE_bgFWp.mjs +0 -285
- package/lib/bundled-references-DY3EJxZV.mjs +0 -56
- package/lib/cloudflare-ai-gateway-CrMa97Ap.mjs +0 -44
- package/lib/collapse-CkMNRNr7.mjs +0 -2304
- package/lib/cursor-C6jaIJDg.mjs +0 -283
- package/lib/cursor-D-XZYDf_.mjs +0 -4544
- package/lib/deterministic-id-CUEIMSqR.mjs +0 -17
- package/lib/devin-ExGIghmx.mjs +0 -69
- package/lib/devin-u0ljw5P6.mjs +0 -628
- package/lib/effort-BdtQMN-_.mjs +0 -23
- package/lib/event-stream-BKEtb45Y.mjs +0 -167
- package/lib/gemini-headers-Co9cVvex.mjs +0 -54
- package/lib/google-auth-qAN-6DMW.mjs +0 -232
- package/lib/google-gemini-cli-B2Aw1S5w.mjs +0 -844
- package/lib/google-lTxuq28n.mjs +0 -38
- package/lib/google-shared-Y01j9xOr.mjs +0 -856
- package/lib/google-validation-BEkNs-Bx.mjs +0 -18
- package/lib/google-vertex-DcM-FRC5.mjs +0 -99
- package/lib/hosts-00rjRzUD.mjs +0 -203
- package/lib/id-AL5obQzR.mjs +0 -71
- package/lib/idle-iterator-D0ZXEcUq.mjs +0 -366
- package/lib/ollama-6R0oXZks.mjs +0 -574
- package/lib/openai-codex-responses-C8bnBWOe.mjs +0 -3232
- package/lib/openai-completions-DNr3mUsn.mjs +0 -1430
- package/lib/openai-configuration-update-xMYUjaK_.mjs +0 -150
- package/lib/openai-reasoning-fallback-Ju-5pSR8.mjs +0 -380
- package/lib/openai-responses-CjpafAEr.mjs +0 -861
- package/lib/protobuf-BMgDYu_U.mjs +0 -717
- package/lib/provider-response-SqKSkkUc.mjs +0 -22
- package/lib/proxy-DxvCQOCA.mjs +0 -230
- package/lib/request-debug-BwPOqN_j.mjs +0 -183
- package/lib/sanitize-text-BCf5KmCf.mjs +0 -33
- package/lib/sse-debug-B-tkLCPW.mjs +0 -11
- package/lib/stream-markup-healing-CN3RsqG9.mjs +0 -267
- package/lib/strict-tool-validation-CBTz1dAA.mjs +0 -105
- package/lib/taxonomy-DSIM99rS.mjs +0 -15392
- package/lib/thinking-ngMwf_1Y.mjs +0 -336
- package/lib/type-guards-CttAANEx.mjs +0 -11
- package/lib/utils-CE3DSebh.mjs +0 -11763
- package/lib/vision-guard-BCpXWUj3.mjs +0 -77
package/lib/cursor-D-XZYDf_.mjs
DELETED
|
@@ -1,4544 +0,0 @@
|
|
|
1
|
-
import { installBunShim as __installBunShim } from "./bun-shim.mjs";
|
|
2
|
-
__installBunShim();
|
|
3
|
-
import { n as fs_promises_shim_exports } from "./fs-promises-shim-B2YFWkcd.mjs";
|
|
4
|
-
import { C as warn, M as truncate, N as $env, _ as parseStreamingJson, b as debug, g as parseJsonWithRepair, v as parseStreamingJsonThrottled } from "./src-AELUUb5U.mjs";
|
|
5
|
-
import { _ as AbortError, c as finalize, h as MissingApiKeyError, r as ValidationError, s as ProviderResponseError } from "./error-O63pC8zg.mjs";
|
|
6
|
-
import { A as toolWireSchema, C as kStreamingLastParseLen, I as sanitizeSchemaForCursor, S as kStreamingEnvelopeId, b as kStreamingBlockIndex, g as clearStreamingPartialJson, o as normalizeSystemPrompts, s as normalizeToolCallId, v as kCursorExecResolved, w as kStreamingPartialJson, x as kStreamingBlockKind } from "./utils-cZX3f_Ls.mjs";
|
|
7
|
-
import { t as sanitizeText } from "./sanitize-text-BCf5KmCf.mjs";
|
|
8
|
-
import { n as isRecord$1 } from "./type-guards-CttAANEx.mjs";
|
|
9
|
-
import { n as classifyModel, r as collapseVariantId } from "./taxonomy-DSIM99rS.mjs";
|
|
10
|
-
import { n as getProxyForUrl, t as connectProxiedSocket } from "./proxy-DxvCQOCA.mjs";
|
|
11
|
-
import { n as THINKING_EFFORTS } from "./effort-BdtQMN-_.mjs";
|
|
12
|
-
import { t as calculateCost } from "./models-DnXMYGcJ.mjs";
|
|
13
|
-
import { t as AssistantMessageEventStream } from "./event-stream-BKEtb45Y.mjs";
|
|
14
|
-
import { n as isRequestDebugEnabled, t as createRequestDebugSession } from "./request-debug-BwPOqN_j.mjs";
|
|
15
|
-
import { i as fromBinary, n as decodeJsonValue, o as toBinary, r as encodeJsonValue, t as create } from "./protobuf-BMgDYu_U.mjs";
|
|
16
|
-
import { $ as ForceBackgroundSubagentResultSchema, $n as SubagentResultSchema, $t as PiFindExecSuccessSchema, A as CursorRuleTypeGlobalSchema, An as RequestContextSuccessSchema, At as McpRejectedSchema, B as ExaFetchRequestResponseSchema, Bn as ShellResultSchema, Bt as McpToolNotFoundSchema, C as ConversationStateStructureSchema, Cn as ReadRejectedSchema, Ct as LsResultSchema, D as CreatePlanRequestResponseSchema, Dn as RecordScreenResultSchema, Dt as McpArgsSchema, E as CreatePlanErrorSchema, En as RecordScreenFailureSchema, Et as McpApprovedSchema, F as DeleteSuccessSchema, Fn as SelectedImageSchema, Ft as McpSuccessSchema, G as ExecClientMessageSchema, Gn as ShellStreamStdoutSchema, Gt as PiBashExecResultSchema, H as ExaSearchRequestResponseSchema, Hn as ShellStreamSchema, Ht as McpToolResultSchema, I as DiagnosticsErrorSchema, In as SetBlobResultSchema, It as McpTextContentSchema, J as ExecuteHookResponseSchema, Jn as SmartModeClassifierResultSchema, Jt as PiEditExecRejectedSchema, K as ExecClientStreamCloseSchema, Kn as ShellSuccessSchema, Kt as PiBashExecSuccessSchema, L as DiagnosticsRejectedSchema, Ln as ShellAllowlistPrecheckResultSchema, Lt as McpToolCallSchema, M as DeleteErrorSchema, Mn as RequestedModel_ModelParameterbytesSchema, Mt as McpStateExecResultSchema, N as DeleteRejectedSchema, Nn as ResumeActionSchema, Nt as McpStateServerSchema, O as CreatePlanResultSchema, On as RequestContextResultSchema, Ot as McpErrorSchema, P as DeleteResultSchema, Pn as SelectedContextSchema, Pt as McpStateSuccessSchema, Q as ForceBackgroundShellResultSchema, Qn as SubagentErrorSchema, Qt as PiFindExecResultSchema, R as DiagnosticsResultSchema, Rn as ShellFailureSchema, Rt as McpToolDefinitionSchema, S as ConversationSearchResultSchema, Sn as ReadMcpResourceSuccessSchema, St as LsRejectedSchema, T as ConversationTurnStructureSchema, Tn as ReadSuccessSchema, Tt as McpAllowlistPrecheckResultSchema, U as ExaSearchRequestResponse_ApprovedSchema, Un as ShellStreamStartSchema, Ut as ModelDetailsSchema, V as ExaFetchRequestResponse_ApprovedSchema, Vn as ShellStreamExitSchema, Vt as McpToolResultContentItemSchema, W as ExecClientControlMessageSchema, Wn as ShellStreamStderrSchema, Wt as PiBashExecErrorSchema, X as FetchErrorSchema, Xn as SubagentAwaitNotFoundSchema, Xt as PiEditExecSuccessSchema, Y as ExecuteHookResultSchema, Yn as StopRequestResponseSchema, Yt as PiEditExecResultSchema, Z as FetchResultSchema, Zn as SubagentAwaitResultSchema, Zt as PiFindExecErrorSchema, _ as ClientHeartbeatSchema, _n as PreToolUseRequestResponseSchema, _r as WriteShellStdinResultSchema, _t as ListMcpResourcesExecResult_McpResourceSchema, a as AgentRunRequestSchema, an as PiLsExecSuccessSchema, ar as ToolCallSchema, at as GrepCountResultSchema, b as ConversationActionSchema, bn as ReadMcpResourceExecResultSchema, bt as LsDirectoryTreeNode_FileSchema, c as AgentStoreConflictResultSchema, cn as PiReadExecSuccessSchema, cr as WebFetchAllowlistPrecheckResultSchema, ct as GrepFileMatchSchema, d as AskQuestionResultSchema, dn as PiWriteExecRejectedSchema, dr as WebSearchRequestResponseSchema, dt as GrepSuccessSchema, en as PiGrepExecErrorSchema, er as SubagentStartRequestResponseSchema, et as GetBlobResultSchema, f as AssistantMessageSchema, fn as PiWriteExecResultSchema, fr as WebSearchRequestResponse_ApprovedSchema, ft as GrepUnionResultSchema, g as CanvasDiagnosticsResultSchema, gn as PreCompactRequestResponseSchema, gr as WriteShellStdinErrorSchema, gt as ListMcpResourcesExecResultSchema, h as CanvasDiagnosticsErrorSchema, hn as PostToolUseRequestResponseSchema, hr as WriteResultSchema, ht as ListMcpResourcesErrorSchema, i as AgentConversationTurnStructureSchema, in as PiLsExecResultSchema, ir as ThinkingMessageSchema, it as GrepContentResultSchema, j as CursorRuleTypeSchema, jn as RequestedModelSchema, jt as McpResultSchema, k as CursorRuleSchema, kn as RequestContextSchema, kt as McpImageContentSchema, l as AskQuestionInteractionResponseSchema, ln as PiTruncationSchema, lr as WebFetchRequestResponseSchema, lt as GrepFilesResultSchema, m as BeforeSubmitPromptRequestResponseSchema, mn as PostToolUseFailureRequestResponseSchema, mr as WriteRejectedSchema, mt as KvClientMessageSchema, n as AfterAgentThoughtRequestResponseSchema, nn as PiGrepExecSuccessSchema, nr as SwitchModeRequestResponseSchema, o as AgentServerMessageSchema, on as PiReadExecErrorSchema, or as UserMessageActionSchema, ot as GrepErrorSchema, p as BackgroundShellSpawnResultSchema, pn as PiWriteExecSuccessSchema, pr as WriteErrorSchema, pt as InteractionResponseSchema, q as ExecClientThrowSchema, qn as SmartModeClassifierErrorSchema, qt as PiEditExecErrorSchema, r as AgentClientMessageSchema, rn as PiLsExecErrorSchema, rr as SwitchModeRequestResponse_RejectedSchema, rt as GrepContentMatchSchema, s as AgentStoreConflictErrorSchema, sn as PiReadExecResultSchema, sr as UserMessageSchema, st as GrepFileCountSchema, t as AfterAgentResponseRequestResponseSchema, tn as PiGrepExecResultSchema, tr as SubagentStopRequestResponseSchema, u as AskQuestionRejectedSchema, un as PiWriteExecErrorSchema, ur as WebFetchRequestResponse_ApprovedSchema, ut as GrepResultSchema, v as ComputerUseErrorSchema, vn as ReadErrorSchema, vr as WriteSuccessSchema, vt as ListMcpResourcesSuccessSchema, w as ConversationStepSchema, wn as ReadResultSchema, wt as LsSuccessSchema, x as ConversationSearchErrorSchema, xn as ReadMcpResourceNotFoundSchema, xt as LsErrorSchema, y as ComputerUseResultSchema, yn as ReadMcpResourceErrorSchema, yt as LsDirectoryTreeNodeSchema, z as DiagnosticsSuccessSchema, zn as ShellRejectedSchema, zt as McpToolErrorSchema } from "./cursor-proto-Cr_WEMuv.mjs";
|
|
17
|
-
import { t as deterministicUuid } from "./deterministic-id-CUEIMSqR.mjs";
|
|
18
|
-
import * as path$1 from "node:path";
|
|
19
|
-
import { createHash } from "node:crypto";
|
|
20
|
-
import http2 from "node:http2";
|
|
21
|
-
//#region node_modules/.pnpm/@oh-my-pi+pi-ai@18.1.17/node_modules/@oh-my-pi/pi-ai/src/providers/connect-error-detail.ts
|
|
22
|
-
/**
|
|
23
|
-
* Connect-protocol end-stream error formatting.
|
|
24
|
-
*
|
|
25
|
-
* A Connect end-of-stream trailer carries an error object of the shape
|
|
26
|
-
* code / message / details. Some backends collapse the useful part into a
|
|
27
|
-
* generic message (for example just "Error"), which previously surfaced
|
|
28
|
-
* verbatim as "Connect error invalid_argument: Error" — an unactionable
|
|
29
|
-
* string that hid both the failing argument and which transport produced it
|
|
30
|
-
* (Refs #4813). This module keeps the existing "Connect error code: message"
|
|
31
|
-
* prefix byte-for-byte and appends what the trailer actually contained:
|
|
32
|
-
* typed detail entries are summarized, and when the message is generic the
|
|
33
|
-
* remaining trailer fields are appended (truncated) so the server's real
|
|
34
|
-
* rejection is visible in logs and bug reports.
|
|
35
|
-
*/
|
|
36
|
-
/** Messages that carry no diagnostic content on their own. */
|
|
37
|
-
const GENERIC_CONNECT_ERROR_MESSAGES = /* @__PURE__ */ new Set([
|
|
38
|
-
"",
|
|
39
|
-
"error",
|
|
40
|
-
"unknown",
|
|
41
|
-
"unknown error",
|
|
42
|
-
"internal",
|
|
43
|
-
"internal error"
|
|
44
|
-
]);
|
|
45
|
-
/** Upper bound for appended trailer context so errors stay log-line sized. */
|
|
46
|
-
const MAX_EXTRA_DETAIL_CHARS = 400;
|
|
47
|
-
function isRecord(value) {
|
|
48
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
49
|
-
}
|
|
50
|
-
function safeJson(value) {
|
|
51
|
-
try {
|
|
52
|
-
return (typeof value === "string" ? value : JSON.stringify(value)) || void 0;
|
|
53
|
-
} catch {
|
|
54
|
-
return;
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
/**
|
|
58
|
-
* Summarizes Connect error detail entries (objects with type / value / debug
|
|
59
|
-
* fields per the Connect JSON error model). Returns undefined when nothing
|
|
60
|
-
* usable is present.
|
|
61
|
-
*/
|
|
62
|
-
function summarizeConnectErrorDetails(details) {
|
|
63
|
-
if (!Array.isArray(details) || details.length === 0) return void 0;
|
|
64
|
-
const parts = [];
|
|
65
|
-
for (const entry of details) {
|
|
66
|
-
if (!isRecord(entry)) continue;
|
|
67
|
-
const type = typeof entry.type === "string" && entry.type ? entry.type : void 0;
|
|
68
|
-
const debug = entry.debug !== void 0 ? safeJson(entry.debug) : void 0;
|
|
69
|
-
const value = entry.value !== void 0 ? safeJson(entry.value) : void 0;
|
|
70
|
-
const diagnostic = debug ?? value;
|
|
71
|
-
if (type && diagnostic) parts.push(`${type}: ${diagnostic}`);
|
|
72
|
-
else if (type) parts.push(type);
|
|
73
|
-
else if (diagnostic) parts.push(diagnostic);
|
|
74
|
-
}
|
|
75
|
-
if (parts.length === 0) return void 0;
|
|
76
|
-
return truncate(parts.join("; "), MAX_EXTRA_DETAIL_CHARS);
|
|
77
|
-
}
|
|
78
|
-
/**
|
|
79
|
-
* Formats a Connect end-stream error object into a diagnosable message.
|
|
80
|
-
* The "Connect error code: message" prefix is preserved exactly; detail
|
|
81
|
-
* entries are appended when present, and when the message itself is generic
|
|
82
|
-
* the remaining trailer fields are inlined so the error names what the
|
|
83
|
-
* server actually sent instead of a bare "Error".
|
|
84
|
-
*/
|
|
85
|
-
function formatConnectEndStreamError(error) {
|
|
86
|
-
const record = isRecord(error) ? error : {};
|
|
87
|
-
const code = typeof record.code === "string" && record.code ? record.code : "unknown";
|
|
88
|
-
const message = typeof record.message === "string" ? record.message : "";
|
|
89
|
-
const detail = summarizeConnectErrorDetails(record.details);
|
|
90
|
-
const parts = [`Connect error ${code}: ${message || "Unknown error"}`];
|
|
91
|
-
if (detail) parts.push(`[details: ${detail}]`);
|
|
92
|
-
else if (GENERIC_CONNECT_ERROR_MESSAGES.has(message.trim().toLowerCase())) {
|
|
93
|
-
const extras = {};
|
|
94
|
-
for (const [key, value] of Object.entries(record)) {
|
|
95
|
-
if (key === "code" || key === "message") continue;
|
|
96
|
-
extras[key] = value;
|
|
97
|
-
}
|
|
98
|
-
const raw = Object.keys(extras).length > 0 ? safeJson(extras) : void 0;
|
|
99
|
-
if (raw && raw !== "{}") parts.push(`[trailer: ${truncate(raw, MAX_EXTRA_DETAIL_CHARS)}]`);
|
|
100
|
-
}
|
|
101
|
-
return parts.join(" ");
|
|
102
|
-
}
|
|
103
|
-
//#endregion
|
|
104
|
-
//#region node_modules/.pnpm/@oh-my-pi+pi-ai@18.1.17/node_modules/@oh-my-pi/pi-ai/src/providers/cursor-external-tool-handoff.md
|
|
105
|
-
var cursor_external_tool_handoff_default = "Tool call received and handed off to the external client for execution. Do not retry or call it again; end the turn. The result will be provided in the next request.\n";
|
|
106
|
-
//#endregion
|
|
107
|
-
//#region node_modules/.pnpm/@oh-my-pi+pi-ai@18.1.17/node_modules/@oh-my-pi/pi-ai/src/providers/cursor-pi-args.ts
|
|
108
|
-
/**
|
|
109
|
-
* Translate a Pi frame's args into the local tool kwargs that run it.
|
|
110
|
-
*
|
|
111
|
-
* Shared deliberately by three consumers: the provider synthesizes a display
|
|
112
|
-
* block from these, the coding-agent bridge executes with them, and the legacy
|
|
113
|
-
* pi shim performs the identical translation for the old wire. Separate
|
|
114
|
-
* hand-rolled copies drift, and the drift is invisible — the transcript shows
|
|
115
|
-
* one operation while a different one runs.
|
|
116
|
-
*
|
|
117
|
-
* Kept apart from `cursor/exec-modern.ts` on purpose: these are pure
|
|
118
|
-
* string/path functions with no protobuf coupling, while that module pulls in
|
|
119
|
-
* the generated cursor protobuf graph. The legacy shim is
|
|
120
|
-
* compiled into the bundled virtual module registry, so importing it from a
|
|
121
|
-
* nested path would drag the whole exec implementation in with it — and
|
|
122
|
-
* `./providers/*` is a single-segment wildcard export that cannot serve a
|
|
123
|
-
* nested specifier under bunfs (issue #3442).
|
|
124
|
-
*
|
|
125
|
-
* Every `optional int32` here is presence-sensitive: `0` is a supplied value,
|
|
126
|
-
* not "unset", so it must never be folded into a default.
|
|
127
|
-
*/
|
|
128
|
-
/**
|
|
129
|
-
* A `pi_read` range composed onto the path as `read`'s inline `:raw:N+K`
|
|
130
|
-
* selector.
|
|
131
|
-
*
|
|
132
|
-
* `read` exposes no range kwargs, so an uncomposed range reads the whole file.
|
|
133
|
-
* `offset` is a 1-indexed start clamped like the reference's
|
|
134
|
-
* `Math.max(0, offset - 1)` over 0-indexed lines; `limit` is a line count.
|
|
135
|
-
* `null` marks a present `limit: 0` — zero lines, which no selector expresses
|
|
136
|
-
* and which must not degrade into a whole-file read.
|
|
137
|
-
*
|
|
138
|
-
* The range is `raw` because a plain `:N+K` deliberately pads with one leading
|
|
139
|
-
* and three trailing context lines: helpful for a human reading a snippet,
|
|
140
|
-
* wrong for a caller that asked for exactly `limit` lines from `offset`. The
|
|
141
|
-
* wire result is an opaque `output` string, so the hashline and line-number
|
|
142
|
-
* gutter that `raw` also drops carry nothing the frame's contract needs.
|
|
143
|
-
* A range-free read keeps the ordinary form — whole-file reads want them.
|
|
144
|
-
*/
|
|
145
|
-
function piReadPath(readPath, offset, limit) {
|
|
146
|
-
if (limit !== void 0 && Math.floor(limit) <= 0) return null;
|
|
147
|
-
const start = offset !== void 0 ? Math.max(1, Math.floor(offset)) : void 0;
|
|
148
|
-
const count = limit !== void 0 ? Math.floor(limit) : void 0;
|
|
149
|
-
if (start === void 0 && count === void 0) return readPath;
|
|
150
|
-
const base = readPath.split(":").some((chunk) => chunk.toLowerCase() === "raw") ? readPath : `${readPath}:raw`;
|
|
151
|
-
if (start === void 0) return `${base}:1+${count}`;
|
|
152
|
-
return count === void 0 ? `${base}:${start}-` : `${base}:${start}+${count}`;
|
|
153
|
-
}
|
|
154
|
-
const READ_RANGE_CHUNK_RE = /^L?(\d+)(?:(\.\.|[-+])L?(\d+)?)?$/i;
|
|
155
|
-
function isReadRangeList(value) {
|
|
156
|
-
return value.split(",").every((chunk) => {
|
|
157
|
-
const match = READ_RANGE_CHUNK_RE.exec(chunk);
|
|
158
|
-
if (!match) return false;
|
|
159
|
-
const start = Number.parseInt(match[1], 10);
|
|
160
|
-
if (start < 1) return false;
|
|
161
|
-
const separator = match[2];
|
|
162
|
-
if (!separator) return true;
|
|
163
|
-
const end = match[3] ? Number.parseInt(match[3], 10) : void 0;
|
|
164
|
-
if (separator === "+") return end !== void 0 && end >= 1;
|
|
165
|
-
return end === void 0 || end >= start;
|
|
166
|
-
});
|
|
167
|
-
}
|
|
168
|
-
/**
|
|
169
|
-
* Whether a read path ends in an OMP line selector, including compound `raw`
|
|
170
|
-
* forms. Cursor uses this only to describe the operation already executed by
|
|
171
|
-
* the coding-agent read tool; the selector remains embedded in the path.
|
|
172
|
-
*/
|
|
173
|
-
function piReadPathHasRange(readPath) {
|
|
174
|
-
const chunks = readPath.split(":");
|
|
175
|
-
const last = chunks.at(-1);
|
|
176
|
-
if (last && isReadRangeList(last)) return true;
|
|
177
|
-
if (last?.toLowerCase() !== "raw") return false;
|
|
178
|
-
const preceding = chunks.at(-2);
|
|
179
|
-
return preceding !== void 0 && isReadRangeList(preceding);
|
|
180
|
-
}
|
|
181
|
-
/**
|
|
182
|
-
* Force a Cursor exec read onto `read`'s verbatim `:raw` selector.
|
|
183
|
-
*
|
|
184
|
-
* Native `editToolCall` (StrReplace) materializes via `readArgs` then
|
|
185
|
-
* `writeArgs`. The server treats the read result as file bytes and writes
|
|
186
|
-
* them back. A hashline-formatted native read (`[path#TAG]` + `LINE:`
|
|
187
|
-
* prefixes) poisons that cycle: the write would persist the markup.
|
|
188
|
-
* `:raw` is the existing selector that drops both. A path that already
|
|
189
|
-
* carries `raw` is left alone; a range-only selector gets `raw` inserted
|
|
190
|
-
* so the range still applies without the gutter.
|
|
191
|
-
*/
|
|
192
|
-
function cursorRawReadPath(readPath) {
|
|
193
|
-
const chunks = readPath.split(":");
|
|
194
|
-
if (chunks.some((chunk) => chunk.toLowerCase() === "raw")) return readPath;
|
|
195
|
-
if (piReadPathHasRange(readPath)) {
|
|
196
|
-
const last = chunks.pop();
|
|
197
|
-
return `${chunks.join(":")}:raw:${last}`;
|
|
198
|
-
}
|
|
199
|
-
return `${readPath}:raw`;
|
|
200
|
-
}
|
|
201
|
-
/**
|
|
202
|
-
* Path the edit-owned materialization read should execute.
|
|
203
|
-
*
|
|
204
|
-
* Range is composed first (`piReadPath` already uses `:raw` for a range),
|
|
205
|
-
* then a whole-file path is forced onto `:raw`. The caller must drop
|
|
206
|
-
* `offset`/`limit` after this so the bridge's `piReadPath` cannot append a
|
|
207
|
-
* second `:raw` onto the already-composed selector.
|
|
208
|
-
*/
|
|
209
|
-
function cursorEditOwnedReadPath(readPath, offset, limit) {
|
|
210
|
-
const ranged = piReadPath(readPath, offset, limit);
|
|
211
|
-
if (ranged === null) return null;
|
|
212
|
-
return cursorRawReadPath(ranged);
|
|
213
|
-
}
|
|
214
|
-
/**
|
|
215
|
-
* The same range as {@link piReadPath}, rendered for a transcript block rather
|
|
216
|
-
* than for execution.
|
|
217
|
-
*
|
|
218
|
-
* Differs only at `limit: 0`, where `piReadPath` returns `null` because no
|
|
219
|
-
* selector reads zero lines and the frame is answered with empty output
|
|
220
|
-
* directly. The block still has to say so: falling back to the bare path there
|
|
221
|
-
* would record a whole-file read whose result is empty, which is the widest
|
|
222
|
-
* possible gap between what a rebuilt transcript shows and what happened.
|
|
223
|
-
* `+0` is never executed — it exists to be read.
|
|
224
|
-
*/
|
|
225
|
-
function piReadDisplayPath(readPath, offset, limit) {
|
|
226
|
-
const composed = piReadPath(readPath, offset, limit);
|
|
227
|
-
if (composed !== null) return composed;
|
|
228
|
-
return `${readPath}:raw:${offset !== void 0 ? Math.max(1, Math.floor(offset)) : 1}+0`;
|
|
229
|
-
}
|
|
230
|
-
/**
|
|
231
|
-
* A legacy `grep` frame's pagination `offset` as the local tool's file `skip`.
|
|
232
|
-
*
|
|
233
|
-
* `grep` paginates by file and reports "use skip=N for the next page" in that
|
|
234
|
-
* same unit, so the offset maps across directly. A present `0` means "start at
|
|
235
|
-
* the beginning", which is the unskipped search rather than a skip of zero.
|
|
236
|
-
*
|
|
237
|
-
* Shared because both the executing bridge and the provider's transcript
|
|
238
|
-
* synthesis need it: a block showing an unskipped search beside a result from
|
|
239
|
-
* a later file window misreports what was searched.
|
|
240
|
-
*/
|
|
241
|
-
function piGrepSkip(offset) {
|
|
242
|
-
return offset !== void 0 && offset > 0 ? Math.floor(offset) : void 0;
|
|
243
|
-
}
|
|
244
|
-
/**
|
|
245
|
-
* Join a Pi frame's optional `path` with the `glob`/`pattern` it scopes.
|
|
246
|
-
*
|
|
247
|
-
* The local `grep`/`glob` tools take one combined path spec. An absolute
|
|
248
|
-
* pattern ignores the path, and an absent or `.` path leaves the pattern
|
|
249
|
-
* standing alone rather than building a `./`- or `//`-prefixed spec.
|
|
250
|
-
*
|
|
251
|
-
* Uses `node:path` rather than string surgery so Windows absolutes (`C:\…`,
|
|
252
|
-
* UNC) are recognised and separators stay normalized.
|
|
253
|
-
*/
|
|
254
|
-
function piJoinPath(basePath, pattern) {
|
|
255
|
-
if (path$1.isAbsolute(pattern)) return pattern;
|
|
256
|
-
if (!basePath || basePath === ".") return pattern;
|
|
257
|
-
return path$1.join(basePath, pattern);
|
|
258
|
-
}
|
|
259
|
-
/**
|
|
260
|
-
* The path a `pi_ls` frame lists.
|
|
261
|
-
*
|
|
262
|
-
* The frame's `limit` is deliberately NOT mapped. It caps directory *entries*
|
|
263
|
-
* (the reference does a flat `readdir` and slices the entry array), while the
|
|
264
|
-
* local `read` tool renders a depth-2 tree with per-directory caps and elision
|
|
265
|
-
* summaries and applies a selector as a *rendered line* slice. Nested rows,
|
|
266
|
-
* headers and "N more" lines all count toward that slice, so `:1+K` would cap
|
|
267
|
-
* a different unit while looking honored — worse than leaving it unset, which
|
|
268
|
-
* at least reports the local listing's own truncation faithfully.
|
|
269
|
-
*/
|
|
270
|
-
function piLsPath(basePath) {
|
|
271
|
-
return basePath || ".";
|
|
272
|
-
}
|
|
273
|
-
/** Escape a literal string so the regex-only local `grep` tool matches it verbatim. */
|
|
274
|
-
function piEscapeRegexLiteral(value) {
|
|
275
|
-
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
276
|
-
}
|
|
277
|
-
/** Clamp a present `optional int32` result cap the way the reference does; `undefined` stays unset. */
|
|
278
|
-
function piLimit(limit) {
|
|
279
|
-
return limit === void 0 ? void 0 : Math.max(1, Math.floor(limit));
|
|
280
|
-
}
|
|
281
|
-
/**
|
|
282
|
-
* A `pi_bash` frame's timeout as the local `bash` tool's kwarg.
|
|
283
|
-
*
|
|
284
|
-
* Presence-sensitive like every other `optional int32` here, and unusually
|
|
285
|
-
* load-bearing: `bash` documents `timeout: 0` as "disables the command
|
|
286
|
-
* deadline", so folding a supplied `0` into `undefined` applies the 300s
|
|
287
|
-
* default and kills exactly the long-running command that asked not to be.
|
|
288
|
-
* Negative values have no local meaning and fall back to the default.
|
|
289
|
-
*/
|
|
290
|
-
function piTimeout(timeout) {
|
|
291
|
-
return timeout !== void 0 && timeout >= 0 ? timeout : void 0;
|
|
292
|
-
}
|
|
293
|
-
/**
|
|
294
|
-
* Drop keys whose value is `undefined` so optional local-tool kwargs stay
|
|
295
|
-
* absent rather than present-as-undefined.
|
|
296
|
-
*
|
|
297
|
-
* The Cursor exec bridge historically wrote forms like
|
|
298
|
-
* `cwd: workingDirectory || undefined` and
|
|
299
|
-
* `case: caseInsensitive === true ? false : undefined`. ArkType rejects a
|
|
300
|
-
* present `undefined` on an optional field (`was undefined`) even though
|
|
301
|
-
* omitting the key is valid — which flooded Cursor sessions with bash/grep
|
|
302
|
-
* validation errors for otherwise fine frames.
|
|
303
|
-
*/
|
|
304
|
-
function omitUndefinedArgs(args) {
|
|
305
|
-
const out = {};
|
|
306
|
-
for (const key of Object.keys(args)) {
|
|
307
|
-
const value = args[key];
|
|
308
|
-
if (value !== void 0) out[key] = value;
|
|
309
|
-
}
|
|
310
|
-
return out;
|
|
311
|
-
}
|
|
312
|
-
//#endregion
|
|
313
|
-
//#region node_modules/.pnpm/@oh-my-pi+pi-ai@18.1.17/node_modules/@oh-my-pi/pi-ai/src/providers/cursor/exec-modern.ts
|
|
314
|
-
/**
|
|
315
|
-
* Proto builders for the modern Cursor CLI exec frames (`ExecServerMessage`
|
|
316
|
-
* 27-31, 36-38, 40-55).
|
|
317
|
-
*
|
|
318
|
-
* Split out of `cursor.ts` because these are pure `create(...)` shapes with no
|
|
319
|
-
* transport, stream, or block-state coupling: the dispatcher decides *which*
|
|
320
|
-
* answer a frame gets, this module knows *what* that answer looks like on the
|
|
321
|
-
* wire. Every builder returns a result whose oneof case is set — an
|
|
322
|
-
* `ExecClientMessage` carrying a result with an unset oneof is a fake success
|
|
323
|
-
* the server reads as "the tool ran and produced nothing".
|
|
324
|
-
*/
|
|
325
|
-
/** Flatten a tool result's content into the single `output` string the Pi frames carry. */
|
|
326
|
-
function piOutputText(toolResult) {
|
|
327
|
-
return toolResult.content.map((item) => item.type === "text" ? item.text : `[${item.mimeType} image]`).join("\n");
|
|
328
|
-
}
|
|
329
|
-
/**
|
|
330
|
-
* Read one field off a tool result's `details` bag, or off a nested object
|
|
331
|
-
* inside it.
|
|
332
|
-
*
|
|
333
|
-
* `details` is `unknown` by design — every tool ships its own shape — so each
|
|
334
|
-
* read is narrowed rather than asserted, and the caller decides what a value of
|
|
335
|
-
* the wrong type means.
|
|
336
|
-
*/
|
|
337
|
-
function bagValue(bag, key) {
|
|
338
|
-
if (!bag || typeof bag !== "object" || !(key in bag)) return void 0;
|
|
339
|
-
return Reflect.get(bag, key);
|
|
340
|
-
}
|
|
341
|
-
/**
|
|
342
|
-
* A positive integer count from `details`, or `undefined`.
|
|
343
|
-
*
|
|
344
|
-
* The Pi frames model their limit counters as `optional uint32`: zero means
|
|
345
|
-
* "the limit was reached at zero results", so a missing, non-numeric, or
|
|
346
|
-
* non-positive value must stay unset rather than be sent as 0.
|
|
347
|
-
*/
|
|
348
|
-
function detailCount(toolResult, key) {
|
|
349
|
-
return positiveCount(bagValue(toolResult.details, key));
|
|
350
|
-
}
|
|
351
|
-
function positiveCount(value) {
|
|
352
|
-
return typeof value === "number" && Number.isFinite(value) && value > 0 ? Math.floor(value) : void 0;
|
|
353
|
-
}
|
|
354
|
-
/**
|
|
355
|
-
* The entry cap a listing hit, from either shape a local tool records it in.
|
|
356
|
-
*
|
|
357
|
-
* `glob` sets a flat `details.resultLimitReached` alongside the structured
|
|
358
|
-
* meta; `read` — which serves `pi_ls` — records the cap only through
|
|
359
|
-
* `OutputMeta` at `details.meta.limits.resultLimit.reached`. Reading just the
|
|
360
|
-
* flat field dropped `entry_limit_reached` for every real listing, so Cursor
|
|
361
|
-
* received clipped output with no incompleteness signal.
|
|
362
|
-
*/
|
|
363
|
-
function resultLimitReached(toolResult) {
|
|
364
|
-
const flat = detailCount(toolResult, "resultLimitReached");
|
|
365
|
-
if (flat !== void 0) return flat;
|
|
366
|
-
return positiveCount(bagValue(bagValue(bagValue(bagValue(toolResult.details, "meta"), "limits"), "resultLimit"), "reached"));
|
|
367
|
-
}
|
|
368
|
-
/**
|
|
369
|
-
* Translate a local tool's truncation summary into `PiTruncation`.
|
|
370
|
-
*
|
|
371
|
-
* Two shapes reach here. `read`/`grep` set `details.truncation`
|
|
372
|
-
* (`TruncationResult`), which carries an explicit `truncated` boolean. `bash`
|
|
373
|
-
* sets `details.meta.truncation` (`TruncationMeta`), which has **no** such
|
|
374
|
-
* flag — its presence *is* the signal, and requiring the boolean silently
|
|
375
|
-
* dropped every Bash truncation, handing Cursor clipped output with no notice
|
|
376
|
-
* that it was clipped.
|
|
377
|
-
*
|
|
378
|
-
* Returns `undefined` when nothing was truncated: the field is `optional` on
|
|
379
|
-
* every Pi success message, and emitting a zeroed `PiTruncation` would tell the
|
|
380
|
-
* server the output was trimmed to nothing.
|
|
381
|
-
*/
|
|
382
|
-
function piTruncation(toolResult) {
|
|
383
|
-
const direct = bagValue(toolResult.details, "truncation");
|
|
384
|
-
const truncation = direct !== void 0 ? direct : bagValue(bagValue(toolResult.details, "meta"), "truncation");
|
|
385
|
-
if (truncation === void 0 || truncation === null) return void 0;
|
|
386
|
-
const flag = bagValue(truncation, "truncated");
|
|
387
|
-
if (flag !== void 0 && flag !== true) return void 0;
|
|
388
|
-
const truncatedBy = bagValue(truncation, "truncatedBy");
|
|
389
|
-
const totalLines = bagValue(truncation, "totalLines");
|
|
390
|
-
const outputLines = bagValue(truncation, "outputLines");
|
|
391
|
-
const outputBytes = bagValue(truncation, "outputBytes");
|
|
392
|
-
return create(PiTruncationSchema, {
|
|
393
|
-
truncated: true,
|
|
394
|
-
truncatedBy: typeof truncatedBy === "string" ? truncatedBy : "",
|
|
395
|
-
totalLines: typeof totalLines === "number" ? totalLines : 0,
|
|
396
|
-
outputLines: typeof outputLines === "number" ? outputLines : 0,
|
|
397
|
-
outputBytes: typeof outputBytes === "number" ? outputBytes : 0,
|
|
398
|
-
firstLineExceedsLimit: bagValue(truncation, "firstLineExceedsLimit") === true,
|
|
399
|
-
lastLinePartial: bagValue(truncation, "lastLinePartial") === true
|
|
400
|
-
});
|
|
401
|
-
}
|
|
402
|
-
function buildPiReadResult(toolResult) {
|
|
403
|
-
const text = piOutputText(toolResult);
|
|
404
|
-
if (toolResult.isError) return buildPiReadError(text || "Read failed");
|
|
405
|
-
return create(PiReadExecResultSchema, { result: {
|
|
406
|
-
case: "success",
|
|
407
|
-
value: create(PiReadExecSuccessSchema, {
|
|
408
|
-
output: text,
|
|
409
|
-
truncation: piTruncation(toolResult)
|
|
410
|
-
})
|
|
411
|
-
} });
|
|
412
|
-
}
|
|
413
|
-
function buildPiReadError(error) {
|
|
414
|
-
return create(PiReadExecResultSchema, { result: {
|
|
415
|
-
case: "error",
|
|
416
|
-
value: create(PiReadExecErrorSchema, { error })
|
|
417
|
-
} });
|
|
418
|
-
}
|
|
419
|
-
function buildPiBashResult(toolResult) {
|
|
420
|
-
const text = piOutputText(toolResult);
|
|
421
|
-
const truncation = piTruncation(toolResult);
|
|
422
|
-
if (toolResult.isError) return create(PiBashExecResultSchema, { result: {
|
|
423
|
-
case: "error",
|
|
424
|
-
value: create(PiBashExecErrorSchema, {
|
|
425
|
-
error: text || "Command failed",
|
|
426
|
-
truncation
|
|
427
|
-
})
|
|
428
|
-
} });
|
|
429
|
-
return create(PiBashExecResultSchema, { result: {
|
|
430
|
-
case: "success",
|
|
431
|
-
value: create(PiBashExecSuccessSchema, {
|
|
432
|
-
output: text,
|
|
433
|
-
truncation
|
|
434
|
-
})
|
|
435
|
-
} });
|
|
436
|
-
}
|
|
437
|
-
function buildPiBashError(error) {
|
|
438
|
-
return create(PiBashExecResultSchema, { result: {
|
|
439
|
-
case: "error",
|
|
440
|
-
value: create(PiBashExecErrorSchema, { error })
|
|
441
|
-
} });
|
|
442
|
-
}
|
|
443
|
-
/**
|
|
444
|
-
* `PiEditExecSuccess` requires `diff` and `patch` alongside `output`. The local
|
|
445
|
-
* `edit` tool reports them under `details`; when it does not, the strings stay
|
|
446
|
-
* empty rather than being faked from the output text.
|
|
447
|
-
*/
|
|
448
|
-
function buildPiEditResult(toolResult) {
|
|
449
|
-
const text = piOutputText(toolResult);
|
|
450
|
-
if (toolResult.isError) return buildPiEditError(text || "Edit failed");
|
|
451
|
-
const diff = bagValue(toolResult.details, "diff");
|
|
452
|
-
const patch = bagValue(toolResult.details, "patch");
|
|
453
|
-
return create(PiEditExecResultSchema, { result: {
|
|
454
|
-
case: "success",
|
|
455
|
-
value: create(PiEditExecSuccessSchema, {
|
|
456
|
-
output: text,
|
|
457
|
-
diff: typeof diff === "string" ? diff : "",
|
|
458
|
-
patch: typeof patch === "string" ? patch : "",
|
|
459
|
-
firstChangedLine: detailCount(toolResult, "firstChangedLine")
|
|
460
|
-
})
|
|
461
|
-
} });
|
|
462
|
-
}
|
|
463
|
-
function buildPiEditError(error) {
|
|
464
|
-
return create(PiEditExecResultSchema, { result: {
|
|
465
|
-
case: "error",
|
|
466
|
-
value: create(PiEditExecErrorSchema, { error })
|
|
467
|
-
} });
|
|
468
|
-
}
|
|
469
|
-
/**
|
|
470
|
-
* A refusal is not an execution failure: `PiEditExecResult` models them as
|
|
471
|
-
* separate variants, and answering a denied call with `error` reads as "the
|
|
472
|
-
* edit ran and broke", which invites a retry of an operation that was never
|
|
473
|
-
* permitted.
|
|
474
|
-
*/
|
|
475
|
-
function buildPiEditRejected(reason) {
|
|
476
|
-
return create(PiEditExecResultSchema, { result: {
|
|
477
|
-
case: "rejected",
|
|
478
|
-
value: create(PiEditExecRejectedSchema, { reason })
|
|
479
|
-
} });
|
|
480
|
-
}
|
|
481
|
-
function buildPiWriteResult(toolResult) {
|
|
482
|
-
const text = piOutputText(toolResult);
|
|
483
|
-
if (toolResult.isError) return buildPiWriteError(text || "Write failed");
|
|
484
|
-
return create(PiWriteExecResultSchema, { result: {
|
|
485
|
-
case: "success",
|
|
486
|
-
value: create(PiWriteExecSuccessSchema, { output: text })
|
|
487
|
-
} });
|
|
488
|
-
}
|
|
489
|
-
function buildPiWriteError(error) {
|
|
490
|
-
return create(PiWriteExecResultSchema, { result: {
|
|
491
|
-
case: "error",
|
|
492
|
-
value: create(PiWriteExecErrorSchema, { error })
|
|
493
|
-
} });
|
|
494
|
-
}
|
|
495
|
-
/** Same variant split as {@link buildPiEditRejected}. */
|
|
496
|
-
function buildPiWriteRejected(reason) {
|
|
497
|
-
return create(PiWriteExecResultSchema, { result: {
|
|
498
|
-
case: "rejected",
|
|
499
|
-
value: create(PiWriteExecRejectedSchema, { reason })
|
|
500
|
-
} });
|
|
501
|
-
}
|
|
502
|
-
function buildPiGrepResult(toolResult) {
|
|
503
|
-
const text = piOutputText(toolResult);
|
|
504
|
-
if (toolResult.isError) return buildPiGrepError(text || "Grep failed");
|
|
505
|
-
const matchLimitReached = detailCount(toolResult, "perFileLimitReached");
|
|
506
|
-
return create(PiGrepExecResultSchema, { result: {
|
|
507
|
-
case: "success",
|
|
508
|
-
value: create(PiGrepExecSuccessSchema, {
|
|
509
|
-
output: text,
|
|
510
|
-
truncation: piTruncation(toolResult) ?? grepInternalCapTruncation(toolResult, text, matchLimitReached),
|
|
511
|
-
matchLimitReached,
|
|
512
|
-
linesTruncated: bagValue(toolResult.details, "linesTruncated") === true
|
|
513
|
-
})
|
|
514
|
-
} });
|
|
515
|
-
}
|
|
516
|
-
/**
|
|
517
|
-
* The one grep cap that reaches Cursor through no other field.
|
|
518
|
-
*
|
|
519
|
-
* `GrepTool` folds every cap into the flat `details.truncated`, but only
|
|
520
|
-
* populates `details.truncation` when the output text itself was trimmed and
|
|
521
|
-
* `perFileLimitReached` when a per-file or explicit total cap fired. Its native
|
|
522
|
-
* backend's own ceiling (`INTERNAL_TOTAL_CAP`) sets neither, so a search that
|
|
523
|
-
* hit it answered as an unqualified success over incomplete results — the one
|
|
524
|
-
* failure mode a caller cannot detect and cannot retry around.
|
|
525
|
-
*
|
|
526
|
-
* Only consulted once the specific signals came back empty, so a cap that did
|
|
527
|
-
* report itself is never restated. `totalLines` stays 0: the flat flag says
|
|
528
|
-
* that results were dropped, not how many there were.
|
|
529
|
-
*/
|
|
530
|
-
function grepInternalCapTruncation(toolResult, text, matchLimitReached) {
|
|
531
|
-
if (matchLimitReached !== void 0) return void 0;
|
|
532
|
-
if (bagValue(toolResult.details, "truncated") !== true) return void 0;
|
|
533
|
-
return create(PiTruncationSchema, {
|
|
534
|
-
truncated: true,
|
|
535
|
-
truncatedBy: "matches",
|
|
536
|
-
totalLines: 0,
|
|
537
|
-
outputLines: text ? text.split("\n").length : 0,
|
|
538
|
-
outputBytes: Buffer.byteLength(text, "utf-8"),
|
|
539
|
-
firstLineExceedsLimit: false,
|
|
540
|
-
lastLinePartial: false
|
|
541
|
-
});
|
|
542
|
-
}
|
|
543
|
-
function buildPiGrepError(error) {
|
|
544
|
-
return create(PiGrepExecResultSchema, { result: {
|
|
545
|
-
case: "error",
|
|
546
|
-
value: create(PiGrepExecErrorSchema, { error })
|
|
547
|
-
} });
|
|
548
|
-
}
|
|
549
|
-
function buildPiFindResult(toolResult) {
|
|
550
|
-
const text = piOutputText(toolResult);
|
|
551
|
-
if (toolResult.isError) return buildPiFindError(text || "Find failed");
|
|
552
|
-
return create(PiFindExecResultSchema, { result: {
|
|
553
|
-
case: "success",
|
|
554
|
-
value: create(PiFindExecSuccessSchema, {
|
|
555
|
-
output: text,
|
|
556
|
-
truncation: piTruncation(toolResult),
|
|
557
|
-
resultLimitReached: resultLimitReached(toolResult)
|
|
558
|
-
})
|
|
559
|
-
} });
|
|
560
|
-
}
|
|
561
|
-
function buildPiFindError(error) {
|
|
562
|
-
return create(PiFindExecResultSchema, { result: {
|
|
563
|
-
case: "error",
|
|
564
|
-
value: create(PiFindExecErrorSchema, { error })
|
|
565
|
-
} });
|
|
566
|
-
}
|
|
567
|
-
function buildPiLsResult(toolResult) {
|
|
568
|
-
const text = piOutputText(toolResult);
|
|
569
|
-
if (toolResult.isError) return buildPiLsError(text || "Ls failed");
|
|
570
|
-
return create(PiLsExecResultSchema, { result: {
|
|
571
|
-
case: "success",
|
|
572
|
-
value: create(PiLsExecSuccessSchema, {
|
|
573
|
-
output: text,
|
|
574
|
-
truncation: piTruncation(toolResult),
|
|
575
|
-
entryLimitReached: resultLimitReached(toolResult)
|
|
576
|
-
})
|
|
577
|
-
} });
|
|
578
|
-
}
|
|
579
|
-
function buildPiLsError(error) {
|
|
580
|
-
return create(PiLsExecResultSchema, { result: {
|
|
581
|
-
case: "error",
|
|
582
|
-
value: create(PiLsExecErrorSchema, { error })
|
|
583
|
-
} });
|
|
584
|
-
}
|
|
585
|
-
/**
|
|
586
|
-
* Answer `mcpStateExecArgs` (frame 36) from the catalog already advertised in
|
|
587
|
-
* `RequestContext.tools`.
|
|
588
|
-
*
|
|
589
|
-
* This client hosts no MCP servers of its own: every forwarded tool is a local
|
|
590
|
-
* pi-agent tool published under a synthetic `providerIdentifier`. Regrouping
|
|
591
|
-
* the same list keeps the server's view of "which servers exist and what do
|
|
592
|
-
* they expose" consistent with what it was told at context time, instead of
|
|
593
|
-
* claiming zero servers while tool calls for them keep arriving.
|
|
594
|
-
*
|
|
595
|
-
* `serverIdentifiers` filters the answer when the server asks about specific
|
|
596
|
-
* servers. `kickOnly` is a restart request — there is nothing to restart, so it
|
|
597
|
-
* is answered with the same state rather than an error.
|
|
598
|
-
*/
|
|
599
|
-
function buildMcpStateResult(tools, serverIdentifiers) {
|
|
600
|
-
const byProvider = /* @__PURE__ */ new Map();
|
|
601
|
-
for (const tool of tools) {
|
|
602
|
-
const identifier = tool.providerIdentifier;
|
|
603
|
-
const existing = byProvider.get(identifier);
|
|
604
|
-
if (existing) existing.push(tool);
|
|
605
|
-
else byProvider.set(identifier, [tool]);
|
|
606
|
-
}
|
|
607
|
-
const wanted = serverIdentifiers.length > 0 ? new Set(serverIdentifiers) : void 0;
|
|
608
|
-
const servers = [];
|
|
609
|
-
for (const [identifier, serverTools] of byProvider) {
|
|
610
|
-
if (wanted && !wanted.has(identifier)) continue;
|
|
611
|
-
servers.push(create(McpStateServerSchema, {
|
|
612
|
-
serverName: identifier,
|
|
613
|
-
serverIdentifier: identifier,
|
|
614
|
-
tools: serverTools,
|
|
615
|
-
status: "connected"
|
|
616
|
-
}));
|
|
617
|
-
}
|
|
618
|
-
return create(McpStateExecResultSchema, { result: {
|
|
619
|
-
case: "success",
|
|
620
|
-
value: create(McpStateSuccessSchema, { servers })
|
|
621
|
-
} });
|
|
622
|
-
}
|
|
623
|
-
/**
|
|
624
|
-
* Build the neutral response for a hook query: the matching response case with
|
|
625
|
-
* every field unset.
|
|
626
|
-
*
|
|
627
|
-
* This client runs no Cursor hooks, and every field of every response variant
|
|
628
|
-
* is `optional` — so an empty response of the right case means "no hook had
|
|
629
|
-
* anything to say", which is exactly true. It is NOT the unset-oneof fake
|
|
630
|
-
* success: the case itself is set, only the payload is empty.
|
|
631
|
-
*
|
|
632
|
-
* `ExecuteHookRequest` and `ExecuteHookResponse` are parallel oneofs whose case
|
|
633
|
-
* names line up, but the two unions are unrelated to the compiler: a `switch`
|
|
634
|
-
* is what makes each pairing individually type-checked, and it forces a
|
|
635
|
-
* deliberate branch when a future regen adds a request case.
|
|
636
|
-
*
|
|
637
|
-
* Returns `null` for a request case this build does not model, which the
|
|
638
|
-
* dispatcher answers with `ExecClientThrow` rather than guessing a case.
|
|
639
|
-
*/
|
|
640
|
-
function buildNeutralHookResult(request) {
|
|
641
|
-
let response;
|
|
642
|
-
switch (request?.request.case) {
|
|
643
|
-
case "preCompact":
|
|
644
|
-
response = create(ExecuteHookResponseSchema, { response: {
|
|
645
|
-
case: "preCompact",
|
|
646
|
-
value: create(PreCompactRequestResponseSchema, {})
|
|
647
|
-
} });
|
|
648
|
-
break;
|
|
649
|
-
case "subagentStart":
|
|
650
|
-
response = create(ExecuteHookResponseSchema, { response: {
|
|
651
|
-
case: "subagentStart",
|
|
652
|
-
value: create(SubagentStartRequestResponseSchema, {})
|
|
653
|
-
} });
|
|
654
|
-
break;
|
|
655
|
-
case "subagentStop":
|
|
656
|
-
response = create(ExecuteHookResponseSchema, { response: {
|
|
657
|
-
case: "subagentStop",
|
|
658
|
-
value: create(SubagentStopRequestResponseSchema, {})
|
|
659
|
-
} });
|
|
660
|
-
break;
|
|
661
|
-
case "preToolUse":
|
|
662
|
-
response = create(ExecuteHookResponseSchema, { response: {
|
|
663
|
-
case: "preToolUse",
|
|
664
|
-
value: create(PreToolUseRequestResponseSchema, {})
|
|
665
|
-
} });
|
|
666
|
-
break;
|
|
667
|
-
case "postToolUse":
|
|
668
|
-
response = create(ExecuteHookResponseSchema, { response: {
|
|
669
|
-
case: "postToolUse",
|
|
670
|
-
value: create(PostToolUseRequestResponseSchema, {})
|
|
671
|
-
} });
|
|
672
|
-
break;
|
|
673
|
-
case "postToolUseFailure":
|
|
674
|
-
response = create(ExecuteHookResponseSchema, { response: {
|
|
675
|
-
case: "postToolUseFailure",
|
|
676
|
-
value: create(PostToolUseFailureRequestResponseSchema, {})
|
|
677
|
-
} });
|
|
678
|
-
break;
|
|
679
|
-
case "beforeSubmitPrompt":
|
|
680
|
-
response = create(ExecuteHookResponseSchema, { response: {
|
|
681
|
-
case: "beforeSubmitPrompt",
|
|
682
|
-
value: create(BeforeSubmitPromptRequestResponseSchema, {})
|
|
683
|
-
} });
|
|
684
|
-
break;
|
|
685
|
-
case "afterAgentResponse":
|
|
686
|
-
response = create(ExecuteHookResponseSchema, { response: {
|
|
687
|
-
case: "afterAgentResponse",
|
|
688
|
-
value: create(AfterAgentResponseRequestResponseSchema, {})
|
|
689
|
-
} });
|
|
690
|
-
break;
|
|
691
|
-
case "afterAgentThought":
|
|
692
|
-
response = create(ExecuteHookResponseSchema, { response: {
|
|
693
|
-
case: "afterAgentThought",
|
|
694
|
-
value: create(AfterAgentThoughtRequestResponseSchema, {})
|
|
695
|
-
} });
|
|
696
|
-
break;
|
|
697
|
-
case "stop":
|
|
698
|
-
response = create(ExecuteHookResponseSchema, { response: {
|
|
699
|
-
case: "stop",
|
|
700
|
-
value: create(StopRequestResponseSchema, {})
|
|
701
|
-
} });
|
|
702
|
-
break;
|
|
703
|
-
default: return null;
|
|
704
|
-
}
|
|
705
|
-
return create(ExecuteHookResultSchema, { response });
|
|
706
|
-
}
|
|
707
|
-
//#endregion
|
|
708
|
-
//#region node_modules/.pnpm/@oh-my-pi+pi-ai@18.1.17/node_modules/@oh-my-pi/pi-ai/src/providers/cursor/interaction-query.ts
|
|
709
|
-
const NOT_IMPLEMENTED_SUFFIX$1 = "not implemented by this client";
|
|
710
|
-
function frameConnectMessage$1(data, flags = 0) {
|
|
711
|
-
const frame = Buffer.alloc(5 + data.length);
|
|
712
|
-
frame[0] = flags;
|
|
713
|
-
frame.writeUInt32BE(data.length, 1);
|
|
714
|
-
frame.set(data, 5);
|
|
715
|
-
return frame;
|
|
716
|
-
}
|
|
717
|
-
function isProtoUnknownField(value) {
|
|
718
|
-
if (!value || typeof value !== "object") return false;
|
|
719
|
-
if (!("no" in value) || !("wireType" in value) || !("data" in value)) return false;
|
|
720
|
-
return typeof value.no === "number" && typeof value.wireType === "number" && value.data instanceof Uint8Array;
|
|
721
|
-
}
|
|
722
|
-
function protoUnknownFields$1(message) {
|
|
723
|
-
if (!("$unknown" in message) || !Array.isArray(message.$unknown)) return [];
|
|
724
|
-
return message.$unknown.filter(isProtoUnknownField);
|
|
725
|
-
}
|
|
726
|
-
function attachUnknownApprovedField(response, fieldNo) {
|
|
727
|
-
const bag = response;
|
|
728
|
-
const field = {
|
|
729
|
-
no: fieldNo,
|
|
730
|
-
wireType: 2,
|
|
731
|
-
data: new Uint8Array([
|
|
732
|
-
2,
|
|
733
|
-
10,
|
|
734
|
-
0
|
|
735
|
-
])
|
|
736
|
-
};
|
|
737
|
-
const existing = bag.$unknown;
|
|
738
|
-
if (Array.isArray(existing)) {
|
|
739
|
-
existing.push(field);
|
|
740
|
-
return;
|
|
741
|
-
}
|
|
742
|
-
bag.$unknown = [field];
|
|
743
|
-
}
|
|
744
|
-
function log$1(type, subtype, data) {
|
|
745
|
-
if (!$env.DEBUG_CURSOR) return;
|
|
746
|
-
const dataStr = ($env.DEBUG_CURSOR === "2" || $env.DEBUG_CURSOR === "verbose") && data ? ` ${JSON.stringify(data)?.slice(0, 500)}` : "";
|
|
747
|
-
console.error(`[CURSOR] ${type}${subtype ? `: ${subtype}` : ""}${dataStr}`);
|
|
748
|
-
}
|
|
749
|
-
function sendInteractionResponse(h2Request, queryId, result) {
|
|
750
|
-
const response = create(InteractionResponseSchema, {
|
|
751
|
-
id: queryId,
|
|
752
|
-
result
|
|
753
|
-
});
|
|
754
|
-
const clientMessage = create(AgentClientMessageSchema, { message: {
|
|
755
|
-
case: "interactionResponse",
|
|
756
|
-
value: response
|
|
757
|
-
} });
|
|
758
|
-
h2Request.write(frameConnectMessage$1(toBinary(AgentClientMessageSchema, clientMessage)));
|
|
759
|
-
log$1("interactionResponse", result.case, { id: queryId });
|
|
760
|
-
}
|
|
761
|
-
function sendUnknownApprovedInteractionResponse(h2Request, queryId, fieldNo) {
|
|
762
|
-
const response = create(InteractionResponseSchema, { id: queryId });
|
|
763
|
-
attachUnknownApprovedField(response, fieldNo);
|
|
764
|
-
const clientMessage = create(AgentClientMessageSchema, { message: {
|
|
765
|
-
case: "interactionResponse",
|
|
766
|
-
value: response
|
|
767
|
-
} });
|
|
768
|
-
h2Request.write(frameConnectMessage$1(toBinary(AgentClientMessageSchema, clientMessage)));
|
|
769
|
-
log$1("interactionResponse", "unknownApproved", {
|
|
770
|
-
id: queryId,
|
|
771
|
-
field: fieldNo
|
|
772
|
-
});
|
|
773
|
-
}
|
|
774
|
-
/**
|
|
775
|
-
* Answer a Cursor `interaction_query` so the Run RPC can continue.
|
|
776
|
-
*
|
|
777
|
-
* Hosted web search / Exa / unnamed permission gates (field 9 = WebFetch on
|
|
778
|
-
* current Cursor builds) block the turn until the client writes an
|
|
779
|
-
* `interaction_response`. Dropping the frame leaves the HTTP/2 stream alive
|
|
780
|
-
* on heartbeats that are not semantic progress; the lazy idle watchdog then
|
|
781
|
-
* aborts with "Provider stream stalled while waiting for the next event".
|
|
782
|
-
*
|
|
783
|
-
* Unsupported interactive queries are rejected so the server is not stranded.
|
|
784
|
-
* VM setup is left unanswered rather than reporting a fake success.
|
|
785
|
-
*/
|
|
786
|
-
function handleInteractionQuery(query, h2Request) {
|
|
787
|
-
const queryCase = query.query.case;
|
|
788
|
-
log$1("interactionQuery", queryCase, query.query.value);
|
|
789
|
-
if (!queryCase) {
|
|
790
|
-
const unknown = protoUnknownFields$1(query).find((field) => field.wireType === 2 && field.no >= 2);
|
|
791
|
-
if (unknown) {
|
|
792
|
-
log$1("warn", "unknownInteractionQueryApproved", {
|
|
793
|
-
id: query.id,
|
|
794
|
-
field: unknown.no
|
|
795
|
-
});
|
|
796
|
-
sendUnknownApprovedInteractionResponse(h2Request, query.id, unknown.no);
|
|
797
|
-
return;
|
|
798
|
-
}
|
|
799
|
-
log$1("warn", "unknownInteractionQuery", { id: query.id });
|
|
800
|
-
return;
|
|
801
|
-
}
|
|
802
|
-
switch (queryCase) {
|
|
803
|
-
case "webSearchRequestQuery":
|
|
804
|
-
sendInteractionResponse(h2Request, query.id, {
|
|
805
|
-
case: "webSearchRequestResponse",
|
|
806
|
-
value: create(WebSearchRequestResponseSchema, { result: {
|
|
807
|
-
case: "approved",
|
|
808
|
-
value: create(WebSearchRequestResponse_ApprovedSchema, {})
|
|
809
|
-
} })
|
|
810
|
-
});
|
|
811
|
-
return;
|
|
812
|
-
case "exaSearchRequestQuery":
|
|
813
|
-
sendInteractionResponse(h2Request, query.id, {
|
|
814
|
-
case: "exaSearchRequestResponse",
|
|
815
|
-
value: create(ExaSearchRequestResponseSchema, { result: {
|
|
816
|
-
case: "approved",
|
|
817
|
-
value: create(ExaSearchRequestResponse_ApprovedSchema, {})
|
|
818
|
-
} })
|
|
819
|
-
});
|
|
820
|
-
return;
|
|
821
|
-
case "exaFetchRequestQuery":
|
|
822
|
-
sendInteractionResponse(h2Request, query.id, {
|
|
823
|
-
case: "exaFetchRequestResponse",
|
|
824
|
-
value: create(ExaFetchRequestResponseSchema, { result: {
|
|
825
|
-
case: "approved",
|
|
826
|
-
value: create(ExaFetchRequestResponse_ApprovedSchema, {})
|
|
827
|
-
} })
|
|
828
|
-
});
|
|
829
|
-
return;
|
|
830
|
-
case "webFetchRequestQuery":
|
|
831
|
-
sendInteractionResponse(h2Request, query.id, {
|
|
832
|
-
case: "webFetchRequestResponse",
|
|
833
|
-
value: create(WebFetchRequestResponseSchema, { result: {
|
|
834
|
-
case: "approved",
|
|
835
|
-
value: create(WebFetchRequestResponse_ApprovedSchema, {})
|
|
836
|
-
} })
|
|
837
|
-
});
|
|
838
|
-
return;
|
|
839
|
-
case "askQuestionInteractionQuery":
|
|
840
|
-
sendInteractionResponse(h2Request, query.id, {
|
|
841
|
-
case: "askQuestionInteractionResponse",
|
|
842
|
-
value: create(AskQuestionInteractionResponseSchema, { result: create(AskQuestionResultSchema, { result: {
|
|
843
|
-
case: "rejected",
|
|
844
|
-
value: create(AskQuestionRejectedSchema, { reason: `Interactive questions are ${NOT_IMPLEMENTED_SUFFIX$1}` })
|
|
845
|
-
} }) })
|
|
846
|
-
});
|
|
847
|
-
return;
|
|
848
|
-
case "switchModeRequestQuery":
|
|
849
|
-
sendInteractionResponse(h2Request, query.id, {
|
|
850
|
-
case: "switchModeRequestResponse",
|
|
851
|
-
value: create(SwitchModeRequestResponseSchema, { result: {
|
|
852
|
-
case: "rejected",
|
|
853
|
-
value: create(SwitchModeRequestResponse_RejectedSchema, { reason: `Mode switches are ${NOT_IMPLEMENTED_SUFFIX$1}` })
|
|
854
|
-
} })
|
|
855
|
-
});
|
|
856
|
-
return;
|
|
857
|
-
case "createPlanRequestQuery":
|
|
858
|
-
sendInteractionResponse(h2Request, query.id, {
|
|
859
|
-
case: "createPlanRequestResponse",
|
|
860
|
-
value: create(CreatePlanRequestResponseSchema, { result: create(CreatePlanResultSchema, { result: {
|
|
861
|
-
case: "error",
|
|
862
|
-
value: create(CreatePlanErrorSchema, { error: `Plan files are ${NOT_IMPLEMENTED_SUFFIX$1}` })
|
|
863
|
-
} }) })
|
|
864
|
-
});
|
|
865
|
-
return;
|
|
866
|
-
case "setupVmEnvironmentArgs":
|
|
867
|
-
log$1("warn", "unhandledInteractionQuery", {
|
|
868
|
-
queryCase,
|
|
869
|
-
id: query.id
|
|
870
|
-
});
|
|
871
|
-
return;
|
|
872
|
-
default: log$1("warn", "unhandledInteractionQuery", {
|
|
873
|
-
queryCase,
|
|
874
|
-
id: query.id
|
|
875
|
-
});
|
|
876
|
-
}
|
|
877
|
-
}
|
|
878
|
-
const CURSOR_CLIENT_VERSION = "cli-2026.07.23-e383d2b";
|
|
879
|
-
/**
|
|
880
|
-
* HTTP/1 connection-specific headers that HTTP/2 forbids. Node's `http2.request()`
|
|
881
|
-
* throws `ERR_HTTP2_INVALID_CONNECTION_HEADERS` on these rather than dropping
|
|
882
|
-
* them, so a caller sending one would kill the request outright.
|
|
883
|
-
*/
|
|
884
|
-
const HTTP2_FORBIDDEN_HEADERS = /* @__PURE__ */ new Set([
|
|
885
|
-
"connection",
|
|
886
|
-
"keep-alive",
|
|
887
|
-
"proxy-connection",
|
|
888
|
-
"transfer-encoding",
|
|
889
|
-
"upgrade",
|
|
890
|
-
"http2-settings"
|
|
891
|
-
]);
|
|
892
|
-
/**
|
|
893
|
-
* Header names the Cursor request sets for itself. A caller copy in ANY casing
|
|
894
|
-
* has to go: the spread below adds the fixed lower-case name regardless, and two
|
|
895
|
-
* spellings of one field are a duplicate rather than an override.
|
|
896
|
-
*/
|
|
897
|
-
const CURSOR_RESERVED_HEADERS = /* @__PURE__ */ new Set([
|
|
898
|
-
"content-type",
|
|
899
|
-
"connect-protocol-version",
|
|
900
|
-
"te",
|
|
901
|
-
"authorization",
|
|
902
|
-
"x-ghost-mode",
|
|
903
|
-
"x-cursor-client-version",
|
|
904
|
-
"x-cursor-client-type",
|
|
905
|
-
"x-request-id",
|
|
906
|
-
"host",
|
|
907
|
-
"content-length"
|
|
908
|
-
]);
|
|
909
|
-
/**
|
|
910
|
-
* Reduce caller-supplied headers to what this HTTP/2 request can legally carry.
|
|
911
|
-
*
|
|
912
|
-
* Everything is lower-cased, because HTTP/2 field names are lower-case and node
|
|
913
|
-
* compares them that way. A caller `Authorization` next to the fixed
|
|
914
|
-
* `authorization` does not lose to it, it DUPLICATES it, and node throws
|
|
915
|
-
* `ERR_HTTP2_HEADER_SINGLE_VALUE` before the request goes out. Same for a `TE`
|
|
916
|
-
* that is not `trailers`. Node throws on all three classes here rather than
|
|
917
|
-
* ignoring them, so a miss turns a harmless header into a dead request.
|
|
918
|
-
*/
|
|
919
|
-
function sanitizeCursorCallerHeaders(headers) {
|
|
920
|
-
const sanitized = {};
|
|
921
|
-
for (const [name, value] of Object.entries(headers ?? {})) {
|
|
922
|
-
const field = name.toLowerCase();
|
|
923
|
-
if (field.startsWith(":")) continue;
|
|
924
|
-
if (HTTP2_FORBIDDEN_HEADERS.has(field)) continue;
|
|
925
|
-
if (CURSOR_RESERVED_HEADERS.has(field)) continue;
|
|
926
|
-
sanitized[field] = value;
|
|
927
|
-
}
|
|
928
|
-
return sanitized;
|
|
929
|
-
}
|
|
930
|
-
const CURSOR_PROXY_TUNNEL_TIMEOUT_MS = 3e4;
|
|
931
|
-
/**
|
|
932
|
-
* Text for a recognised frame this client answers with its own typed error
|
|
933
|
-
* variant. Phrased as a client capability statement, not a tool failure: the
|
|
934
|
-
* model reads it and should route around the capability, not retry the call.
|
|
935
|
-
*/
|
|
936
|
-
const NOT_IMPLEMENTED_SUFFIX = "not implemented by this client";
|
|
937
|
-
/** Bare gRPC `resource_exhausted` end-streams (also inside a Connect error message). */
|
|
938
|
-
const RESOURCE_EXHAUSTED_PATTERN = /resource.?exhausted/i;
|
|
939
|
-
/** Model-resolution failures that can safely retry the exact discovery id before any server output. */
|
|
940
|
-
const CURSOR_MODEL_NOT_FOUND_PATTERN = /^(?:Connect error not_found:|gRPC error 5:)/i;
|
|
941
|
-
const NOT_IMPLEMENTED = `Not implemented by this client`;
|
|
942
|
-
const conversationStateCache = /* @__PURE__ */ new Map();
|
|
943
|
-
const conversationBlobStores = /* @__PURE__ */ new Map();
|
|
944
|
-
const warnedCursorKimiK3ReplayMessages = /* @__PURE__ */ new Set();
|
|
945
|
-
/**
|
|
946
|
-
* Base conversation id → rotated wire id (#8345). Cursor's backend can pin a
|
|
947
|
-
* per-conversation rejection (bare `resource_exhausted`, zero tokens) to one
|
|
948
|
-
* conversationId forever. On such a failure the id is rotated and the next
|
|
949
|
-
* attempt rebuilds a fresh conversation from `context` (no cached-state
|
|
950
|
-
* migration). A failed rotation is not repeated, so real account exhaustion
|
|
951
|
-
* is not hidden. After the rotated id completes a turn, a later poison of
|
|
952
|
-
* that id is allowed to rotate again.
|
|
953
|
-
*/
|
|
954
|
-
const rotatedConversationIds = /* @__PURE__ */ new Map();
|
|
955
|
-
const successfulRotatedConversationIds = /* @__PURE__ */ new Set();
|
|
956
|
-
const freshRotatedConversationIds = /* @__PURE__ */ new Set();
|
|
957
|
-
const CONNECT_END_STREAM_FLAG = 2;
|
|
958
|
-
async function appendCursorDebugLog(entry) {
|
|
959
|
-
const logPath = $env.DEBUG_CURSOR_LOG;
|
|
960
|
-
if (!logPath) return;
|
|
961
|
-
try {
|
|
962
|
-
await fs_promises_shim_exports.appendFile(logPath, `${JSON.stringify(entry, debugReplacer)}\n`);
|
|
963
|
-
} catch {}
|
|
964
|
-
}
|
|
965
|
-
function log(type, subtype, data) {
|
|
966
|
-
if (!$env.DEBUG_CURSOR) return;
|
|
967
|
-
const normalizedData = data ? decodeLogData(data) : data;
|
|
968
|
-
const entry = {
|
|
969
|
-
ts: Date.now(),
|
|
970
|
-
type,
|
|
971
|
-
subtype,
|
|
972
|
-
data: normalizedData
|
|
973
|
-
};
|
|
974
|
-
const dataStr = ($env.DEBUG_CURSOR === "2" || $env.DEBUG_CURSOR === "verbose") && normalizedData ? ` ${JSON.stringify(normalizedData, debugReplacer)?.slice(0, 500)}` : "";
|
|
975
|
-
console.error(`[CURSOR] ${type}${subtype ? `: ${subtype}` : ""}${dataStr}`);
|
|
976
|
-
appendCursorDebugLog(entry);
|
|
977
|
-
}
|
|
978
|
-
function frameConnectMessage(data, flags = 0) {
|
|
979
|
-
const frame = Buffer.alloc(5 + data.length);
|
|
980
|
-
frame[0] = flags;
|
|
981
|
-
frame.writeUInt32BE(data.length, 1);
|
|
982
|
-
frame.set(data, 5);
|
|
983
|
-
return frame;
|
|
984
|
-
}
|
|
985
|
-
var ConnectEndStreamError = class extends ProviderResponseError {
|
|
986
|
-
diagnosticMessage;
|
|
987
|
-
constructor(classificationMessage, diagnosticMessage) {
|
|
988
|
-
super(classificationMessage, { kind: "envelope" });
|
|
989
|
-
this.diagnosticMessage = diagnosticMessage;
|
|
990
|
-
}
|
|
991
|
-
};
|
|
992
|
-
function parseConnectEndStream(data) {
|
|
993
|
-
try {
|
|
994
|
-
const error = JSON.parse(new TextDecoder().decode(data))?.error;
|
|
995
|
-
if (error) return new ConnectEndStreamError(`Connect error ${typeof error.code === "string" ? error.code : "unknown"}: ${typeof error.message === "string" ? error.message : "Unknown error"}`, formatConnectEndStreamError(error));
|
|
996
|
-
return null;
|
|
997
|
-
} catch {
|
|
998
|
-
return new ProviderResponseError("Failed to parse Connect end stream", { kind: "envelope" });
|
|
999
|
-
}
|
|
1000
|
-
}
|
|
1001
|
-
/**
|
|
1002
|
-
* Maps an opaque HTTP/2 negotiation failure into an actionable error.
|
|
1003
|
-
*
|
|
1004
|
-
* bun only opens an HTTP/2 session when TLS-ALPN negotiates `h2`. Behind a
|
|
1005
|
-
* TLS-intercepting proxy that strips ALPN (e.g. Zscaler), the handshake yields
|
|
1006
|
-
* no `h2` protocol and bun throws `ERR_HTTP2_ERROR: h2 is not supported`. The
|
|
1007
|
-
* Cursor run RPC is HTTP/2-only (the ALB rejects HTTP/1.1 with 464), so there
|
|
1008
|
-
* is no h1 fallback the way model discovery has one — the run simply cannot
|
|
1009
|
-
* proceed. Replace the opaque message with one that names the cause and points
|
|
1010
|
-
* at the `providers.cursor.baseUrl` workaround.
|
|
1011
|
-
*
|
|
1012
|
-
* Non-ALPN errors pass through untouched.
|
|
1013
|
-
*/
|
|
1014
|
-
function mapH2TransportError(error, baseUrl) {
|
|
1015
|
-
const code = error?.code;
|
|
1016
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
1017
|
-
if (code === "ERR_HTTP2_ERROR" && /h2 is not supported/i.test(message)) return new ProviderResponseError(`Cursor run transport could not negotiate HTTP/2 with ${baseUrl}: "h2 is not supported". This host serves the run RPC over HTTP/2 only, and the TLS handshake did not negotiate h2 via ALPN — typically an ALPN-stripping TLS-intercepting proxy (e.g. Zscaler). Front the provider with a local HTTP/2 bridge and set providers.cursor.baseUrl to it.`, {
|
|
1018
|
-
provider: "cursor",
|
|
1019
|
-
kind: "runtime",
|
|
1020
|
-
cause: error
|
|
1021
|
-
});
|
|
1022
|
-
return error;
|
|
1023
|
-
}
|
|
1024
|
-
function debugBytes(bytes, asHex) {
|
|
1025
|
-
if (asHex) return Buffer.from(bytes).toString("hex");
|
|
1026
|
-
try {
|
|
1027
|
-
const text = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
|
|
1028
|
-
if (/^[\x20-\x7E\s]*$/.test(text)) return text;
|
|
1029
|
-
} catch {}
|
|
1030
|
-
return Buffer.from(bytes).toString("hex");
|
|
1031
|
-
}
|
|
1032
|
-
function debugReplacer(key, value) {
|
|
1033
|
-
if (value instanceof Uint8Array || value && typeof value === "object" && "type" in value && value.type === "Buffer") return debugBytes(value instanceof Uint8Array ? value : new Uint8Array(value.data), key === "blobId" || key === "blob_id" || key.endsWith("Id") || key.endsWith("_id"));
|
|
1034
|
-
if (typeof value === "bigint") return value.toString();
|
|
1035
|
-
return value;
|
|
1036
|
-
}
|
|
1037
|
-
function extractLogBytes(value) {
|
|
1038
|
-
if (value instanceof Uint8Array) return value;
|
|
1039
|
-
if (value && typeof value === "object" && "type" in value && value.type === "Buffer") {
|
|
1040
|
-
const data = value.data;
|
|
1041
|
-
if (Array.isArray(data)) return new Uint8Array(data);
|
|
1042
|
-
}
|
|
1043
|
-
return null;
|
|
1044
|
-
}
|
|
1045
|
-
function decodeMcpArgsForLog(args) {
|
|
1046
|
-
if (!args) return;
|
|
1047
|
-
let mutated = false;
|
|
1048
|
-
const decoded = {};
|
|
1049
|
-
for (const [key, value] of Object.entries(args)) {
|
|
1050
|
-
const bytes = extractLogBytes(value);
|
|
1051
|
-
if (bytes) {
|
|
1052
|
-
decoded[key] = decodeMcpArgValue(bytes);
|
|
1053
|
-
mutated = true;
|
|
1054
|
-
continue;
|
|
1055
|
-
}
|
|
1056
|
-
const normalizedValue = decodeLogData(value);
|
|
1057
|
-
decoded[key] = normalizedValue;
|
|
1058
|
-
if (normalizedValue !== value) mutated = true;
|
|
1059
|
-
}
|
|
1060
|
-
return mutated ? decoded : args;
|
|
1061
|
-
}
|
|
1062
|
-
function decodeLogData(value) {
|
|
1063
|
-
if (!value || typeof value !== "object") return value;
|
|
1064
|
-
if (Array.isArray(value)) return value.map((entry) => decodeLogData(entry));
|
|
1065
|
-
const record = value;
|
|
1066
|
-
const typeName = record.$typeName;
|
|
1067
|
-
const stripTypeName = typeof typeName === "string" && typeName.startsWith("agent.v1.");
|
|
1068
|
-
if (typeName === "agent.v1.McpArgs") {
|
|
1069
|
-
const decodedArgs = decodeMcpArgsForLog(record.args);
|
|
1070
|
-
const base = stripTypeName ? omitTypeName(record) : record;
|
|
1071
|
-
return decodedArgs ? {
|
|
1072
|
-
...base,
|
|
1073
|
-
args: decodedArgs
|
|
1074
|
-
} : base;
|
|
1075
|
-
}
|
|
1076
|
-
if (typeName === "agent.v1.McpToolCall") {
|
|
1077
|
-
const argsRecord = record.args;
|
|
1078
|
-
const decodedArgs = decodeMcpArgsForLog(argsRecord?.args);
|
|
1079
|
-
const base = stripTypeName ? omitTypeName(record) : record;
|
|
1080
|
-
if (decodedArgs && argsRecord) return {
|
|
1081
|
-
...base,
|
|
1082
|
-
args: {
|
|
1083
|
-
...argsRecord,
|
|
1084
|
-
args: decodedArgs
|
|
1085
|
-
}
|
|
1086
|
-
};
|
|
1087
|
-
return base;
|
|
1088
|
-
}
|
|
1089
|
-
let mutated = stripTypeName;
|
|
1090
|
-
const decoded = {};
|
|
1091
|
-
for (const [key, entry] of Object.entries(record)) {
|
|
1092
|
-
if (stripTypeName && key === "$typeName") continue;
|
|
1093
|
-
const normalizedEntry = decodeLogData(entry);
|
|
1094
|
-
decoded[key] = normalizedEntry;
|
|
1095
|
-
if (normalizedEntry !== entry) mutated = true;
|
|
1096
|
-
}
|
|
1097
|
-
return mutated ? decoded : record;
|
|
1098
|
-
}
|
|
1099
|
-
function omitTypeName(record) {
|
|
1100
|
-
const { $typeName: _, ...rest } = record;
|
|
1101
|
-
return rest;
|
|
1102
|
-
}
|
|
1103
|
-
function streamCursorWithWireMode(model, context, options, wireMode, timing) {
|
|
1104
|
-
const stream = new AssistantMessageEventStream();
|
|
1105
|
-
(async () => {
|
|
1106
|
-
const startTime = timing?.startTime ?? performance.now();
|
|
1107
|
-
let firstTokenTime;
|
|
1108
|
-
const output = {
|
|
1109
|
-
role: "assistant",
|
|
1110
|
-
content: [],
|
|
1111
|
-
api: "cursor-agent",
|
|
1112
|
-
provider: model.provider,
|
|
1113
|
-
model: model.id,
|
|
1114
|
-
usage: {
|
|
1115
|
-
input: 0,
|
|
1116
|
-
output: 0,
|
|
1117
|
-
cacheRead: 0,
|
|
1118
|
-
cacheWrite: 0,
|
|
1119
|
-
totalTokens: 0,
|
|
1120
|
-
cost: {
|
|
1121
|
-
input: 0,
|
|
1122
|
-
output: 0,
|
|
1123
|
-
cacheRead: 0,
|
|
1124
|
-
cacheWrite: 0,
|
|
1125
|
-
total: 0
|
|
1126
|
-
}
|
|
1127
|
-
},
|
|
1128
|
-
stopReason: "stop",
|
|
1129
|
-
timestamp: timing?.timestamp ?? Date.now()
|
|
1130
|
-
};
|
|
1131
|
-
const inFlightDispatches = /* @__PURE__ */ new Set();
|
|
1132
|
-
let abortSettled;
|
|
1133
|
-
const drainInFlightDispatches = async () => {
|
|
1134
|
-
const signal = options?.signal;
|
|
1135
|
-
while (inFlightDispatches.size > 0) {
|
|
1136
|
-
if (signal?.aborted) return;
|
|
1137
|
-
const settled = Promise.all(inFlightDispatches);
|
|
1138
|
-
if (!signal) {
|
|
1139
|
-
await settled;
|
|
1140
|
-
continue;
|
|
1141
|
-
}
|
|
1142
|
-
abortSettled ??= new Promise((resolve) => signal.addEventListener("abort", () => resolve(), { once: true }));
|
|
1143
|
-
await Promise.race([settled, abortSettled]);
|
|
1144
|
-
}
|
|
1145
|
-
};
|
|
1146
|
-
let h2Client = null;
|
|
1147
|
-
let h2Request = null;
|
|
1148
|
-
let heartbeatTimer = null;
|
|
1149
|
-
let debugResponseLogPromise;
|
|
1150
|
-
const h2Completion = Promise.withResolvers();
|
|
1151
|
-
let h2Settled = false;
|
|
1152
|
-
let sawTurnEnded = false;
|
|
1153
|
-
let endStreamError = null;
|
|
1154
|
-
let sawProgressOrSideEffect = false;
|
|
1155
|
-
let openBlockState;
|
|
1156
|
-
const settleH2 = (error) => {
|
|
1157
|
-
if (h2Settled) return;
|
|
1158
|
-
h2Settled = true;
|
|
1159
|
-
if (error !== void 0) {
|
|
1160
|
-
h2Completion.reject(error);
|
|
1161
|
-
return;
|
|
1162
|
-
}
|
|
1163
|
-
if (endStreamError) {
|
|
1164
|
-
h2Completion.reject(endStreamError);
|
|
1165
|
-
return;
|
|
1166
|
-
}
|
|
1167
|
-
if (!sawTurnEnded) {
|
|
1168
|
-
h2Completion.reject(new ProviderResponseError("Cursor stream ended before turnEnded", { kind: "incomplete-stream" }));
|
|
1169
|
-
return;
|
|
1170
|
-
}
|
|
1171
|
-
h2Completion.resolve();
|
|
1172
|
-
};
|
|
1173
|
-
let baseConversationId;
|
|
1174
|
-
let conversationId;
|
|
1175
|
-
let usageState;
|
|
1176
|
-
let serializedFallbackWireModelId;
|
|
1177
|
-
try {
|
|
1178
|
-
const apiKey = options?.apiKey;
|
|
1179
|
-
if (!apiKey) throw new MissingApiKeyError(void 0, "Cursor API key (access token) is required");
|
|
1180
|
-
baseConversationId = options?.conversationId ?? options?.sessionId ?? crypto.randomUUID();
|
|
1181
|
-
conversationId = rotatedConversationIds.get(baseConversationId) ?? baseConversationId;
|
|
1182
|
-
const rotatedFresh = freshRotatedConversationIds.has(conversationId);
|
|
1183
|
-
const blobStore = conversationBlobStores.get(conversationId) ?? /* @__PURE__ */ new Map();
|
|
1184
|
-
conversationBlobStores.set(conversationId, blobStore);
|
|
1185
|
-
const cachedState = rotatedFresh ? void 0 : conversationStateCache.get(conversationId);
|
|
1186
|
-
const builtRequest = await buildGrpcRequestForWireMode(model, context, options, {
|
|
1187
|
-
conversationId,
|
|
1188
|
-
blobStore,
|
|
1189
|
-
conversationState: cachedState,
|
|
1190
|
-
rotatedFresh
|
|
1191
|
-
}, wireMode);
|
|
1192
|
-
const { requestBytes, conversationState } = builtRequest;
|
|
1193
|
-
serializedFallbackWireModelId = builtRequest.fallbackWireModelId;
|
|
1194
|
-
conversationStateCache.set(conversationId, conversationState);
|
|
1195
|
-
const requestContextTools = buildMcpToolDefinitions(context.tools, model.requiresCursorToolSchemaProjection === true);
|
|
1196
|
-
const requestContextRules = buildCursorRequestContextRules(context.systemPrompt);
|
|
1197
|
-
const baseUrl = model.baseUrl || "https://api2.cursor.sh";
|
|
1198
|
-
const requestPath = "/agent.v1.AgentService/Run";
|
|
1199
|
-
const requestHeaders = {
|
|
1200
|
-
...sanitizeCursorCallerHeaders(options?.headers),
|
|
1201
|
-
":method": "POST",
|
|
1202
|
-
":path": requestPath,
|
|
1203
|
-
"content-type": "application/connect+proto",
|
|
1204
|
-
"connect-protocol-version": "1",
|
|
1205
|
-
te: "trailers",
|
|
1206
|
-
authorization: `Bearer ${apiKey}`,
|
|
1207
|
-
"x-ghost-mode": "true",
|
|
1208
|
-
"x-cursor-client-version": CURSOR_CLIENT_VERSION,
|
|
1209
|
-
"x-cursor-client-type": "cli",
|
|
1210
|
-
"x-request-id": crypto.randomUUID()
|
|
1211
|
-
};
|
|
1212
|
-
const debugSession = isRequestDebugEnabled() ? await createRequestDebugSession({
|
|
1213
|
-
protocol: "http2",
|
|
1214
|
-
method: "POST",
|
|
1215
|
-
url: new URL(requestPath, baseUrl).toString(),
|
|
1216
|
-
headers: requestHeaders,
|
|
1217
|
-
bodyBase64: Buffer.from(requestBytes).toString("base64")
|
|
1218
|
-
}) : void 0;
|
|
1219
|
-
const proxyUrl = getProxyForUrl(model.provider, new URL(baseUrl));
|
|
1220
|
-
if (proxyUrl) {
|
|
1221
|
-
const tlsSocket = await connectProxiedSocket(proxyUrl, baseUrl, {
|
|
1222
|
-
signal: options?.signal,
|
|
1223
|
-
timeoutMs: CURSOR_PROXY_TUNNEL_TIMEOUT_MS
|
|
1224
|
-
});
|
|
1225
|
-
h2Client = http2.connect(baseUrl, { createConnection: () => tlsSocket });
|
|
1226
|
-
} else h2Client = http2.connect(baseUrl);
|
|
1227
|
-
h2Client.on("error", (error) => settleH2(mapH2TransportError(error, baseUrl)));
|
|
1228
|
-
h2Request = h2Client.request(requestHeaders);
|
|
1229
|
-
stream.push({
|
|
1230
|
-
type: "start",
|
|
1231
|
-
partial: output
|
|
1232
|
-
});
|
|
1233
|
-
let pendingBuffer = Buffer.alloc(0);
|
|
1234
|
-
let currentTextBlock = null;
|
|
1235
|
-
let currentThinkingBlock = null;
|
|
1236
|
-
let currentToolCall = null;
|
|
1237
|
-
const resolvedMcpToolCallIds = /* @__PURE__ */ new Set();
|
|
1238
|
-
usageState = { sawTokenDelta: false };
|
|
1239
|
-
const state = {
|
|
1240
|
-
get currentTextBlock() {
|
|
1241
|
-
return currentTextBlock;
|
|
1242
|
-
},
|
|
1243
|
-
get currentThinkingBlock() {
|
|
1244
|
-
return currentThinkingBlock;
|
|
1245
|
-
},
|
|
1246
|
-
get currentToolCall() {
|
|
1247
|
-
return currentToolCall;
|
|
1248
|
-
},
|
|
1249
|
-
openToolCalls: /* @__PURE__ */ new Map(),
|
|
1250
|
-
resolvedMcpToolCallIds,
|
|
1251
|
-
get firstTokenTime() {
|
|
1252
|
-
return firstTokenTime;
|
|
1253
|
-
},
|
|
1254
|
-
setTextBlock: (b) => {
|
|
1255
|
-
currentTextBlock = b;
|
|
1256
|
-
},
|
|
1257
|
-
setThinkingBlock: (b) => {
|
|
1258
|
-
currentThinkingBlock = b;
|
|
1259
|
-
},
|
|
1260
|
-
setToolCall: (t) => {
|
|
1261
|
-
currentToolCall = t;
|
|
1262
|
-
},
|
|
1263
|
-
setFirstTokenTime: () => {
|
|
1264
|
-
if (!firstTokenTime) firstTokenTime = performance.now();
|
|
1265
|
-
},
|
|
1266
|
-
onTodoSnapshot: options?.execHandlers?.todoSync?.bind(options.execHandlers),
|
|
1267
|
-
onToolResult: options?.onToolResult
|
|
1268
|
-
};
|
|
1269
|
-
openBlockState = state;
|
|
1270
|
-
const onConversationCheckpoint = (checkpoint) => {
|
|
1271
|
-
conversationStateCache.set(conversationId, checkpoint);
|
|
1272
|
-
};
|
|
1273
|
-
h2Request.on("response", (headers) => {
|
|
1274
|
-
debugResponseLogPromise = debugSession?.openResponseLog(`HTTP/2 ${headers[":status"] ?? ""}`.trim(), headers);
|
|
1275
|
-
});
|
|
1276
|
-
h2Request.on("data", (chunk) => {
|
|
1277
|
-
if (debugResponseLogPromise) debugResponseLogPromise.then((log) => {
|
|
1278
|
-
log?.write(chunk);
|
|
1279
|
-
});
|
|
1280
|
-
pendingBuffer = pendingBuffer.length === 0 ? chunk : Buffer.concat([pendingBuffer, chunk]);
|
|
1281
|
-
while (pendingBuffer.length >= 5) {
|
|
1282
|
-
const flags = pendingBuffer[0];
|
|
1283
|
-
const msgLen = pendingBuffer.readUInt32BE(1);
|
|
1284
|
-
if (pendingBuffer.length < 5 + msgLen) break;
|
|
1285
|
-
const messageBytes = pendingBuffer.subarray(5, 5 + msgLen);
|
|
1286
|
-
pendingBuffer = pendingBuffer.subarray(5 + msgLen);
|
|
1287
|
-
if (flags & CONNECT_END_STREAM_FLAG) {
|
|
1288
|
-
const endError = parseConnectEndStream(messageBytes);
|
|
1289
|
-
if (endError) {
|
|
1290
|
-
endStreamError = endError;
|
|
1291
|
-
h2Request?.close();
|
|
1292
|
-
}
|
|
1293
|
-
continue;
|
|
1294
|
-
}
|
|
1295
|
-
try {
|
|
1296
|
-
const serverMessage = fromBinary(AgentServerMessageSchema, messageBytes);
|
|
1297
|
-
const interactionCase = (serverMessage.message.case === "interactionUpdate" ? serverMessage.message.value : void 0)?.message?.case;
|
|
1298
|
-
if (interactionCase !== "heartbeat") sawProgressOrSideEffect = true;
|
|
1299
|
-
const isTurnEnded = interactionCase === "turnEnded";
|
|
1300
|
-
const dispatch = handleServerMessage(serverMessage, output, stream, state, blobStore, h2Request, options?.execHandlers, options?.onToolResult, usageState, requestContextTools, requestContextRules, onConversationCheckpoint, options?.externalToolExecutor).catch((error) => {
|
|
1301
|
-
log("error", "handleServerMessage", { error: String(error) });
|
|
1302
|
-
});
|
|
1303
|
-
inFlightDispatches.add(dispatch);
|
|
1304
|
-
dispatch.finally(() => inFlightDispatches.delete(dispatch));
|
|
1305
|
-
if (isTurnEnded) sawTurnEnded = true;
|
|
1306
|
-
} catch (e) {
|
|
1307
|
-
log("error", "parseServerMessage", { error: String(e) });
|
|
1308
|
-
}
|
|
1309
|
-
}
|
|
1310
|
-
});
|
|
1311
|
-
const sendHeartbeat = () => {
|
|
1312
|
-
if (!h2Request || h2Request.closed) return;
|
|
1313
|
-
const heartbeatMessage = create(AgentClientMessageSchema, { message: {
|
|
1314
|
-
case: "clientHeartbeat",
|
|
1315
|
-
value: create(ClientHeartbeatSchema, {})
|
|
1316
|
-
} });
|
|
1317
|
-
const heartbeatBytes = toBinary(AgentClientMessageSchema, heartbeatMessage);
|
|
1318
|
-
h2Request.write(frameConnectMessage(heartbeatBytes));
|
|
1319
|
-
};
|
|
1320
|
-
const closeDebugLog = async () => {
|
|
1321
|
-
await (await debugResponseLogPromise)?.close();
|
|
1322
|
-
};
|
|
1323
|
-
h2Request.on("trailers", (trailers) => {
|
|
1324
|
-
const status = trailers["grpc-status"];
|
|
1325
|
-
const msg = trailers["grpc-message"];
|
|
1326
|
-
if (status && status !== "0" && !endStreamError) endStreamError = new ProviderResponseError(`gRPC error ${status}: ${decodeURIComponent(String(msg || ""))}`, { kind: "envelope" });
|
|
1327
|
-
});
|
|
1328
|
-
h2Request.on("end", () => {
|
|
1329
|
-
closeDebugLog().then(() => settleH2()).catch((error) => settleH2(error));
|
|
1330
|
-
});
|
|
1331
|
-
h2Request.on("error", (error) => {
|
|
1332
|
-
const mapped = mapH2TransportError(error, baseUrl);
|
|
1333
|
-
closeDebugLog().finally(() => settleH2(mapped));
|
|
1334
|
-
});
|
|
1335
|
-
if (options?.signal) options.signal.addEventListener("abort", () => {
|
|
1336
|
-
h2Request?.close();
|
|
1337
|
-
closeDebugLog().finally(() => {
|
|
1338
|
-
settleH2(new AbortError());
|
|
1339
|
-
});
|
|
1340
|
-
});
|
|
1341
|
-
h2Request.write(frameConnectMessage(requestBytes));
|
|
1342
|
-
heartbeatTimer = setInterval(sendHeartbeat, 5e3);
|
|
1343
|
-
await h2Completion.promise;
|
|
1344
|
-
if (conversationId && baseConversationId && conversationId !== baseConversationId) {
|
|
1345
|
-
successfulRotatedConversationIds.add(conversationId);
|
|
1346
|
-
freshRotatedConversationIds.delete(conversationId);
|
|
1347
|
-
}
|
|
1348
|
-
await drainInFlightDispatches();
|
|
1349
|
-
endCurrentTextBlock(output, stream, state);
|
|
1350
|
-
endCurrentThinkingBlock(output, stream, state);
|
|
1351
|
-
flushOpenToolCalls(output, stream, state);
|
|
1352
|
-
calculateCost(model, output.usage, output.timestamp);
|
|
1353
|
-
output.duration = performance.now() - startTime;
|
|
1354
|
-
if (firstTokenTime) output.ttft = firstTokenTime - startTime;
|
|
1355
|
-
stream.push({
|
|
1356
|
-
type: "done",
|
|
1357
|
-
reason: output.stopReason,
|
|
1358
|
-
message: output
|
|
1359
|
-
});
|
|
1360
|
-
stream.end();
|
|
1361
|
-
} catch (error) {
|
|
1362
|
-
if ((wireMode === "normalized" && !sawProgressOrSideEffect && !options?.signal?.aborted && error instanceof Error && CURSOR_MODEL_NOT_FOUND_PATTERN.test(error.message) ? serializedFallbackWireModelId : void 0) !== void 0) {
|
|
1363
|
-
if (heartbeatTimer) {
|
|
1364
|
-
clearInterval(heartbeatTimer);
|
|
1365
|
-
heartbeatTimer = null;
|
|
1366
|
-
}
|
|
1367
|
-
h2Request?.close();
|
|
1368
|
-
h2Client?.close();
|
|
1369
|
-
const fallbackStream = streamCursorWithWireMode(model, context, options, "discovered", {
|
|
1370
|
-
startTime,
|
|
1371
|
-
timestamp: output.timestamp
|
|
1372
|
-
});
|
|
1373
|
-
stream.forwardLocalWorkFrom(fallbackStream);
|
|
1374
|
-
try {
|
|
1375
|
-
for await (const event of fallbackStream) {
|
|
1376
|
-
if (event.type === "start") continue;
|
|
1377
|
-
stream.push(event);
|
|
1378
|
-
}
|
|
1379
|
-
const fallbackResult = await fallbackStream.result();
|
|
1380
|
-
if (!stream.resultSettled) stream.end(fallbackResult);
|
|
1381
|
-
} finally {
|
|
1382
|
-
stream.forwardLocalWorkFrom(void 0);
|
|
1383
|
-
}
|
|
1384
|
-
return;
|
|
1385
|
-
}
|
|
1386
|
-
await drainInFlightDispatches();
|
|
1387
|
-
if (openBlockState) {
|
|
1388
|
-
endCurrentTextBlock(output, stream, openBlockState);
|
|
1389
|
-
endCurrentThinkingBlock(output, stream, openBlockState);
|
|
1390
|
-
flushOpenToolCalls(output, stream, openBlockState);
|
|
1391
|
-
}
|
|
1392
|
-
const result = await finalize(error, {
|
|
1393
|
-
api: model.api,
|
|
1394
|
-
signal: options?.signal
|
|
1395
|
-
});
|
|
1396
|
-
const currentRotated = baseConversationId === void 0 ? void 0 : rotatedConversationIds.get(baseConversationId);
|
|
1397
|
-
const canRotate = currentRotated === void 0 || successfulRotatedConversationIds.has(currentRotated);
|
|
1398
|
-
if (conversationId !== void 0 && baseConversationId !== void 0 && usageState !== void 0 && !usageState.sawTokenDelta && RESOURCE_EXHAUSTED_PATTERN.test(result.message) && canRotate) {
|
|
1399
|
-
const rotated = crypto.randomUUID();
|
|
1400
|
-
if (currentRotated) successfulRotatedConversationIds.delete(currentRotated);
|
|
1401
|
-
rotatedConversationIds.set(baseConversationId, rotated);
|
|
1402
|
-
freshRotatedConversationIds.add(rotated);
|
|
1403
|
-
debug("cursor conversation rotated", {
|
|
1404
|
-
base: baseConversationId,
|
|
1405
|
-
from: conversationId,
|
|
1406
|
-
to: rotated
|
|
1407
|
-
});
|
|
1408
|
-
}
|
|
1409
|
-
output.stopReason = result.stopReason;
|
|
1410
|
-
output.errorStatus = result.status;
|
|
1411
|
-
output.errorId = result.id;
|
|
1412
|
-
if (error instanceof ConnectEndStreamError) {
|
|
1413
|
-
output.errorClassificationMessage = result.message;
|
|
1414
|
-
output.errorMessage = error.diagnosticMessage;
|
|
1415
|
-
} else output.errorMessage = result.message;
|
|
1416
|
-
output.duration = performance.now() - startTime;
|
|
1417
|
-
if (firstTokenTime) output.ttft = firstTokenTime - startTime;
|
|
1418
|
-
stream.push({
|
|
1419
|
-
type: "error",
|
|
1420
|
-
reason: output.stopReason,
|
|
1421
|
-
error: output
|
|
1422
|
-
});
|
|
1423
|
-
stream.end();
|
|
1424
|
-
} finally {
|
|
1425
|
-
await (await debugResponseLogPromise)?.close();
|
|
1426
|
-
if (heartbeatTimer) {
|
|
1427
|
-
clearInterval(heartbeatTimer);
|
|
1428
|
-
heartbeatTimer = null;
|
|
1429
|
-
}
|
|
1430
|
-
h2Request?.close();
|
|
1431
|
-
h2Client?.close();
|
|
1432
|
-
}
|
|
1433
|
-
})();
|
|
1434
|
-
return stream;
|
|
1435
|
-
}
|
|
1436
|
-
/** Streams a Cursor Agent turn, retrying a discovered effort id when its normalized wire id is unavailable. */
|
|
1437
|
-
const streamCursor = (model, context, options) => streamCursorWithWireMode(model, context, options, "normalized");
|
|
1438
|
-
function markCursorExecResolved(block) {
|
|
1439
|
-
block[kCursorExecResolved] = true;
|
|
1440
|
-
}
|
|
1441
|
-
/** Exported for tests: drives one Cursor server message through the stream (exec waits mark the stream busy). */
|
|
1442
|
-
async function handleServerMessage(msg, output, stream, state, blobStore, h2Request, execHandlers, onToolResult, usageState, requestContextTools, requestContextRules = [], onConversationCheckpoint, externalToolExecutor = false) {
|
|
1443
|
-
const msgCase = msg.message.case;
|
|
1444
|
-
log("serverMessage", msgCase, msg.message.value);
|
|
1445
|
-
if (msgCase === "interactionUpdate") processInteractionUpdate(msg.message.value, output, stream, state, usageState);
|
|
1446
|
-
else if (msgCase === "kvServerMessage") handleKvServerMessage(msg.message.value, blobStore, h2Request);
|
|
1447
|
-
else if (msgCase === "execServerMessage") await stream.trackLocalWork(handleExecServerMessage(msg.message.value, h2Request, execHandlers, onToolResult, requestContextTools, requestContextRules, output, stream, state, externalToolExecutor));
|
|
1448
|
-
else if (msgCase === "interactionQuery") handleInteractionQuery(msg.message.value, h2Request);
|
|
1449
|
-
else if (msgCase === "conversationCheckpointUpdate") handleConversationCheckpointUpdate(msg.message.value, output, usageState, onConversationCheckpoint);
|
|
1450
|
-
}
|
|
1451
|
-
function selectHostedFetchCall(toolCall) {
|
|
1452
|
-
const oneof = toolCall?.tool;
|
|
1453
|
-
if (oneof?.case === "fetchToolCall" || oneof?.case === "webFetchToolCall") return oneof.value;
|
|
1454
|
-
}
|
|
1455
|
-
function hostedFetchUnknown(toolCall) {
|
|
1456
|
-
if (!toolCall) return false;
|
|
1457
|
-
return protoUnknownFields(toolCall).some((field) => field.no === 37) || protoUnknownFields(toolCall.tool ?? {}).some((field) => field.no === 37);
|
|
1458
|
-
}
|
|
1459
|
-
function extractHttpUrlFromUnknown(message) {
|
|
1460
|
-
for (const field of protoUnknownFields(message)) {
|
|
1461
|
-
const match = new TextDecoder().decode(field.data).match(/https?:\/\/[^\x00-\x1f]+/);
|
|
1462
|
-
if (match) return match[0];
|
|
1463
|
-
}
|
|
1464
|
-
const nested = message.tool;
|
|
1465
|
-
return nested ? extractHttpUrlFromUnknown(nested) : void 0;
|
|
1466
|
-
}
|
|
1467
|
-
function describeHostedFetchResult(call) {
|
|
1468
|
-
const result = call?.result?.result;
|
|
1469
|
-
if (result?.case === "success") return {
|
|
1470
|
-
text: result.value?.content || result.value?.url || "Fetched",
|
|
1471
|
-
isError: false
|
|
1472
|
-
};
|
|
1473
|
-
if (result?.case === "error") return {
|
|
1474
|
-
text: result.value?.error || "Fetch failed",
|
|
1475
|
-
isError: true
|
|
1476
|
-
};
|
|
1477
|
-
return {
|
|
1478
|
-
text: "Fetch completed",
|
|
1479
|
-
isError: false
|
|
1480
|
-
};
|
|
1481
|
-
}
|
|
1482
|
-
function protoUnknownFields(message) {
|
|
1483
|
-
const raw = message.$unknown;
|
|
1484
|
-
return Array.isArray(raw) ? raw : [];
|
|
1485
|
-
}
|
|
1486
|
-
function handleKvServerMessage(kvMsg, blobStore, h2Request) {
|
|
1487
|
-
const kvCase = kvMsg.message.case;
|
|
1488
|
-
if (kvCase === "getBlobArgs") {
|
|
1489
|
-
const blobId = kvMsg.message.value.blobId;
|
|
1490
|
-
const blobIdKey = Buffer.from(blobId).toString("hex");
|
|
1491
|
-
const blobData = blobStore.get(blobIdKey);
|
|
1492
|
-
const response = create(KvClientMessageSchema, {
|
|
1493
|
-
id: kvMsg.id,
|
|
1494
|
-
message: {
|
|
1495
|
-
case: "getBlobResult",
|
|
1496
|
-
value: create(GetBlobResultSchema, blobData ? { blobData } : {})
|
|
1497
|
-
}
|
|
1498
|
-
});
|
|
1499
|
-
const kvClientMessage = create(AgentClientMessageSchema, { message: {
|
|
1500
|
-
case: "kvClientMessage",
|
|
1501
|
-
value: response
|
|
1502
|
-
} });
|
|
1503
|
-
const responseBytes = toBinary(AgentClientMessageSchema, kvClientMessage);
|
|
1504
|
-
h2Request.write(frameConnectMessage(responseBytes));
|
|
1505
|
-
log("kvClient", "getBlobResult", { blobId: blobIdKey.slice(0, 40) });
|
|
1506
|
-
} else if (kvCase === "setBlobArgs") {
|
|
1507
|
-
const { blobId, blobData } = kvMsg.message.value;
|
|
1508
|
-
const blobIdKey = Buffer.from(blobId).toString("hex");
|
|
1509
|
-
blobStore.set(blobIdKey, blobData);
|
|
1510
|
-
const response = create(KvClientMessageSchema, {
|
|
1511
|
-
id: kvMsg.id,
|
|
1512
|
-
message: {
|
|
1513
|
-
case: "setBlobResult",
|
|
1514
|
-
value: create(SetBlobResultSchema, {})
|
|
1515
|
-
}
|
|
1516
|
-
});
|
|
1517
|
-
const kvClientMessage = create(AgentClientMessageSchema, { message: {
|
|
1518
|
-
case: "kvClientMessage",
|
|
1519
|
-
value: response
|
|
1520
|
-
} });
|
|
1521
|
-
const responseBytes = toBinary(AgentClientMessageSchema, kvClientMessage);
|
|
1522
|
-
h2Request.write(frameConnectMessage(responseBytes));
|
|
1523
|
-
log("kvClient", "setBlobResult", { blobId: blobIdKey.slice(0, 40) });
|
|
1524
|
-
}
|
|
1525
|
-
}
|
|
1526
|
-
function sendShellStreamEvent(h2Request, execMsg, event) {
|
|
1527
|
-
sendExecClientMessage(h2Request, execMsg, "shellStream", create(ShellStreamSchema, { event }));
|
|
1528
|
-
}
|
|
1529
|
-
function sanitizeShellExecResult(execResult) {
|
|
1530
|
-
const result = execResult.result;
|
|
1531
|
-
if (!result) return execResult;
|
|
1532
|
-
switch (result.case) {
|
|
1533
|
-
case "success":
|
|
1534
|
-
case "failure": {
|
|
1535
|
-
const value = result.value;
|
|
1536
|
-
return {
|
|
1537
|
-
...execResult,
|
|
1538
|
-
result: {
|
|
1539
|
-
case: result.case,
|
|
1540
|
-
value: {
|
|
1541
|
-
...value,
|
|
1542
|
-
stdout: value.stdout ? sanitizeText(value.stdout) : value.stdout,
|
|
1543
|
-
stderr: value.stderr ? sanitizeText(value.stderr) : value.stderr
|
|
1544
|
-
}
|
|
1545
|
-
}
|
|
1546
|
-
};
|
|
1547
|
-
}
|
|
1548
|
-
default: return execResult;
|
|
1549
|
-
}
|
|
1550
|
-
}
|
|
1551
|
-
async function handleShellStreamArgs(args, execMsg, h2Request, execHandlers, onToolResult) {
|
|
1552
|
-
const normalizedWorkingDirectory = args.workingDirectory || process.cwd();
|
|
1553
|
-
const normalizedArgs = {
|
|
1554
|
-
...args,
|
|
1555
|
-
workingDirectory: normalizedWorkingDirectory
|
|
1556
|
-
};
|
|
1557
|
-
const startTs = performance.now();
|
|
1558
|
-
log("shellStream", "start", {
|
|
1559
|
-
command: args.command,
|
|
1560
|
-
workingDirectory: normalizedWorkingDirectory,
|
|
1561
|
-
execId: execMsg.execId,
|
|
1562
|
-
hasExecHandlers: !!execHandlers,
|
|
1563
|
-
hasShell: !!execHandlers?.shell,
|
|
1564
|
-
hasShellStream: !!execHandlers?.shellStream
|
|
1565
|
-
});
|
|
1566
|
-
sendShellStreamEvent(h2Request, execMsg, {
|
|
1567
|
-
case: "start",
|
|
1568
|
-
value: create(ShellStreamStartSchema, {})
|
|
1569
|
-
});
|
|
1570
|
-
let stdoutBuffer = "";
|
|
1571
|
-
let stderrBuffer = "";
|
|
1572
|
-
const incompleteEscapeRegex = /\x1b(|\[|\[\d*|\[\?|\[\?\d*|\]\d*;?)$/;
|
|
1573
|
-
const flushStdout = () => {
|
|
1574
|
-
if (stdoutBuffer) {
|
|
1575
|
-
let safeEnd = stdoutBuffer.length;
|
|
1576
|
-
const match = stdoutBuffer.match(incompleteEscapeRegex);
|
|
1577
|
-
if (match && match[0].length > 0) safeEnd = stdoutBuffer.length - match[0].length;
|
|
1578
|
-
const toSend = stdoutBuffer.slice(0, safeEnd);
|
|
1579
|
-
const remaining = stdoutBuffer.slice(safeEnd);
|
|
1580
|
-
if (toSend) sendShellStreamEvent(h2Request, execMsg, {
|
|
1581
|
-
case: "stdout",
|
|
1582
|
-
value: create(ShellStreamStdoutSchema, { data: sanitizeText(toSend) })
|
|
1583
|
-
});
|
|
1584
|
-
stdoutBuffer = remaining;
|
|
1585
|
-
}
|
|
1586
|
-
};
|
|
1587
|
-
const flushStderr = () => {
|
|
1588
|
-
if (stderrBuffer) {
|
|
1589
|
-
let safeEnd = stderrBuffer.length;
|
|
1590
|
-
const match = stderrBuffer.match(incompleteEscapeRegex);
|
|
1591
|
-
if (match && match[0].length > 0) safeEnd = stderrBuffer.length - match[0].length;
|
|
1592
|
-
const toSend = stderrBuffer.slice(0, safeEnd);
|
|
1593
|
-
const remaining = stderrBuffer.slice(safeEnd);
|
|
1594
|
-
if (toSend) sendShellStreamEvent(h2Request, execMsg, {
|
|
1595
|
-
case: "stderr",
|
|
1596
|
-
value: create(ShellStreamStderrSchema, { data: sanitizeText(toSend) })
|
|
1597
|
-
});
|
|
1598
|
-
stderrBuffer = remaining;
|
|
1599
|
-
}
|
|
1600
|
-
};
|
|
1601
|
-
let stdoutFlushTimer = null;
|
|
1602
|
-
let stderrFlushTimer = null;
|
|
1603
|
-
const scheduleStdoutFlush = () => {
|
|
1604
|
-
if (!stdoutFlushTimer) stdoutFlushTimer = setTimeout(() => {
|
|
1605
|
-
stdoutFlushTimer = null;
|
|
1606
|
-
flushStdout();
|
|
1607
|
-
}, 100);
|
|
1608
|
-
};
|
|
1609
|
-
const scheduleStderrFlush = () => {
|
|
1610
|
-
if (!stderrFlushTimer) stderrFlushTimer = setTimeout(() => {
|
|
1611
|
-
stderrFlushTimer = null;
|
|
1612
|
-
flushStderr();
|
|
1613
|
-
}, 100);
|
|
1614
|
-
};
|
|
1615
|
-
const streamCallbacks = {
|
|
1616
|
-
onStdout(data) {
|
|
1617
|
-
stdoutBuffer += data;
|
|
1618
|
-
if (stdoutBuffer.includes("\n") || stdoutBuffer.length > 4096) {
|
|
1619
|
-
if (stdoutFlushTimer) {
|
|
1620
|
-
clearTimeout(stdoutFlushTimer);
|
|
1621
|
-
stdoutFlushTimer = null;
|
|
1622
|
-
}
|
|
1623
|
-
flushStdout();
|
|
1624
|
-
} else scheduleStdoutFlush();
|
|
1625
|
-
},
|
|
1626
|
-
onStderr(data) {
|
|
1627
|
-
stderrBuffer += data;
|
|
1628
|
-
if (stderrBuffer.includes("\n") || stderrBuffer.length > 4096) {
|
|
1629
|
-
if (stderrFlushTimer) {
|
|
1630
|
-
clearTimeout(stderrFlushTimer);
|
|
1631
|
-
stderrFlushTimer = null;
|
|
1632
|
-
}
|
|
1633
|
-
flushStderr();
|
|
1634
|
-
} else scheduleStderrFlush();
|
|
1635
|
-
}
|
|
1636
|
-
};
|
|
1637
|
-
const streamHandler = execHandlers?.shellStream?.bind(execHandlers);
|
|
1638
|
-
const batchHandler = execHandlers?.shell?.bind(execHandlers);
|
|
1639
|
-
const { execResult } = await resolveExecHandler(args, streamHandler ? (shellArgs) => streamHandler(shellArgs, streamCallbacks) : batchHandler, onToolResult, (toolResult) => buildShellResultFromToolResult(normalizedArgs, toolResult), (reason) => buildShellRejectedResult(normalizedArgs.command, normalizedArgs.workingDirectory, reason), (error) => buildShellFailureResult(normalizedArgs.command, normalizedArgs.workingDirectory, error), {
|
|
1640
|
-
toolCallId: args.toolCallId,
|
|
1641
|
-
toolName: "bash"
|
|
1642
|
-
});
|
|
1643
|
-
const sendBufferedOutput = !streamHandler;
|
|
1644
|
-
const sanitizedExecResult = sanitizeShellExecResult(execResult);
|
|
1645
|
-
if (stdoutFlushTimer) clearTimeout(stdoutFlushTimer);
|
|
1646
|
-
if (stderrFlushTimer) clearTimeout(stderrFlushTimer);
|
|
1647
|
-
flushStdout();
|
|
1648
|
-
flushStderr();
|
|
1649
|
-
sendShellStreamExitFromResult(h2Request, execMsg, sanitizedExecResult, sendBufferedOutput);
|
|
1650
|
-
sendExecClientMessage(h2Request, execMsg, "shellResult", sanitizedExecResult);
|
|
1651
|
-
sendExecClientStreamClose(h2Request, execMsg);
|
|
1652
|
-
log("shellStream", "done", { elapsed: performance.now() - startTs });
|
|
1653
|
-
}
|
|
1654
|
-
function sendShellStreamExitFromResult(h2Request, execMsg, execResult, sendBufferedOutput) {
|
|
1655
|
-
const result = execResult.result;
|
|
1656
|
-
switch (result.case) {
|
|
1657
|
-
case "success": {
|
|
1658
|
-
const value = result.value;
|
|
1659
|
-
if (sendBufferedOutput) {
|
|
1660
|
-
if (value.stdout) sendShellStreamEvent(h2Request, execMsg, {
|
|
1661
|
-
case: "stdout",
|
|
1662
|
-
value: create(ShellStreamStdoutSchema, { data: sanitizeText(value.stdout) })
|
|
1663
|
-
});
|
|
1664
|
-
if (value.stderr) sendShellStreamEvent(h2Request, execMsg, {
|
|
1665
|
-
case: "stderr",
|
|
1666
|
-
value: create(ShellStreamStderrSchema, { data: sanitizeText(value.stderr) })
|
|
1667
|
-
});
|
|
1668
|
-
}
|
|
1669
|
-
sendShellStreamEvent(h2Request, execMsg, {
|
|
1670
|
-
case: "exit",
|
|
1671
|
-
value: create(ShellStreamExitSchema, {
|
|
1672
|
-
code: value.exitCode,
|
|
1673
|
-
cwd: value.workingDirectory,
|
|
1674
|
-
aborted: false
|
|
1675
|
-
})
|
|
1676
|
-
});
|
|
1677
|
-
return;
|
|
1678
|
-
}
|
|
1679
|
-
case "failure": {
|
|
1680
|
-
const value = result.value;
|
|
1681
|
-
if (sendBufferedOutput) {
|
|
1682
|
-
if (value.stdout) sendShellStreamEvent(h2Request, execMsg, {
|
|
1683
|
-
case: "stdout",
|
|
1684
|
-
value: create(ShellStreamStdoutSchema, { data: sanitizeText(value.stdout) })
|
|
1685
|
-
});
|
|
1686
|
-
if (value.stderr) sendShellStreamEvent(h2Request, execMsg, {
|
|
1687
|
-
case: "stderr",
|
|
1688
|
-
value: create(ShellStreamStderrSchema, { data: sanitizeText(value.stderr) })
|
|
1689
|
-
});
|
|
1690
|
-
}
|
|
1691
|
-
sendShellStreamEvent(h2Request, execMsg, {
|
|
1692
|
-
case: "exit",
|
|
1693
|
-
value: create(ShellStreamExitSchema, {
|
|
1694
|
-
code: value.exitCode,
|
|
1695
|
-
cwd: value.workingDirectory,
|
|
1696
|
-
aborted: value.aborted,
|
|
1697
|
-
abortReason: value.abortReason
|
|
1698
|
-
})
|
|
1699
|
-
});
|
|
1700
|
-
return;
|
|
1701
|
-
}
|
|
1702
|
-
case "rejected":
|
|
1703
|
-
sendShellStreamEvent(h2Request, execMsg, {
|
|
1704
|
-
case: "rejected",
|
|
1705
|
-
value: result.value
|
|
1706
|
-
});
|
|
1707
|
-
sendShellStreamEvent(h2Request, execMsg, {
|
|
1708
|
-
case: "exit",
|
|
1709
|
-
value: create(ShellStreamExitSchema, {
|
|
1710
|
-
code: 1,
|
|
1711
|
-
cwd: result.value.workingDirectory,
|
|
1712
|
-
aborted: false
|
|
1713
|
-
})
|
|
1714
|
-
});
|
|
1715
|
-
return;
|
|
1716
|
-
case "timeout": {
|
|
1717
|
-
const value = result.value;
|
|
1718
|
-
sendShellStreamEvent(h2Request, execMsg, {
|
|
1719
|
-
case: "stderr",
|
|
1720
|
-
value: create(ShellStreamStderrSchema, { data: `Command timed out after ${value.timeoutMs}ms` })
|
|
1721
|
-
});
|
|
1722
|
-
sendShellStreamEvent(h2Request, execMsg, {
|
|
1723
|
-
case: "exit",
|
|
1724
|
-
value: create(ShellStreamExitSchema, {
|
|
1725
|
-
code: 1,
|
|
1726
|
-
cwd: value.workingDirectory,
|
|
1727
|
-
aborted: true
|
|
1728
|
-
})
|
|
1729
|
-
});
|
|
1730
|
-
return;
|
|
1731
|
-
}
|
|
1732
|
-
case "permissionDenied":
|
|
1733
|
-
sendShellStreamEvent(h2Request, execMsg, {
|
|
1734
|
-
case: "permissionDenied",
|
|
1735
|
-
value: result.value
|
|
1736
|
-
});
|
|
1737
|
-
sendShellStreamEvent(h2Request, execMsg, {
|
|
1738
|
-
case: "exit",
|
|
1739
|
-
value: create(ShellStreamExitSchema, {
|
|
1740
|
-
code: 1,
|
|
1741
|
-
cwd: result.value.workingDirectory,
|
|
1742
|
-
aborted: false
|
|
1743
|
-
})
|
|
1744
|
-
});
|
|
1745
|
-
return;
|
|
1746
|
-
default: return;
|
|
1747
|
-
}
|
|
1748
|
-
}
|
|
1749
|
-
async function handleExecServerMessage(execMsg, h2Request, execHandlers, onToolResult, requestContextTools, requestContextRules, output, stream, state, externalToolExecutor) {
|
|
1750
|
-
const execCase = execMsg.message.case;
|
|
1751
|
-
log("exec", "dispatch", {
|
|
1752
|
-
execCase,
|
|
1753
|
-
execId: execMsg.execId,
|
|
1754
|
-
hasHandlers: !!execHandlers
|
|
1755
|
-
});
|
|
1756
|
-
if (execCase === "requestContextArgs") {
|
|
1757
|
-
const requestContext = create(RequestContextSchema, {
|
|
1758
|
-
rules: requestContextRules,
|
|
1759
|
-
repositoryInfo: [],
|
|
1760
|
-
tools: requestContextTools,
|
|
1761
|
-
gitRepos: [],
|
|
1762
|
-
projectLayouts: [],
|
|
1763
|
-
mcpInstructions: [],
|
|
1764
|
-
fileContents: {},
|
|
1765
|
-
customSubagents: []
|
|
1766
|
-
});
|
|
1767
|
-
sendExecClientMessage(h2Request, execMsg, "requestContextResult", create(RequestContextResultSchema, { result: {
|
|
1768
|
-
case: "success",
|
|
1769
|
-
value: create(RequestContextSuccessSchema, { requestContext })
|
|
1770
|
-
} }));
|
|
1771
|
-
log("execClient", "requestContextResult");
|
|
1772
|
-
return;
|
|
1773
|
-
}
|
|
1774
|
-
if (!execCase) {
|
|
1775
|
-
log("warn", "unknownExecVariant", {
|
|
1776
|
-
id: execMsg.id,
|
|
1777
|
-
execId: execMsg.execId
|
|
1778
|
-
});
|
|
1779
|
-
sendExecClientThrow(h2Request, execMsg, "Unknown exec message variant", "unknown_exec_variant");
|
|
1780
|
-
return;
|
|
1781
|
-
}
|
|
1782
|
-
switch (execCase) {
|
|
1783
|
-
case "readArgs": {
|
|
1784
|
-
const args = execMsg.message.value;
|
|
1785
|
-
if (!args.toolCallId) args.toolCallId = crypto.randomUUID();
|
|
1786
|
-
const editOwned = isEditOwnedToolCallId(state, output, args.toolCallId);
|
|
1787
|
-
const composed = editOwned ? cursorEditOwnedReadPath(args.path, args.offset, args.limit) : args.path;
|
|
1788
|
-
const handlerArgs = editOwned ? composed === null ? args : {
|
|
1789
|
-
...args,
|
|
1790
|
-
path: composed,
|
|
1791
|
-
offset: void 0,
|
|
1792
|
-
limit: void 0
|
|
1793
|
-
} : args;
|
|
1794
|
-
if (!editOwned) synthesizeCursorExecToolCall(output, stream, state, args.toolCallId, "read", { path: piReadDisplayPath(args.path, args.offset, args.limit) });
|
|
1795
|
-
const { execResult } = await resolveExecHandler(handlerArgs, execHandlers?.read?.bind(execHandlers), editOwned ? void 0 : onToolResult, (toolResult) => buildReadResultFromToolResult(args.path, toolResult, args.offset !== void 0 || args.limit !== void 0 || piReadPathHasRange(args.path)), (reason) => buildReadRejectedResult(args.path, reason), (error) => buildReadErrorResult(args.path, error), editOwned ? null : {
|
|
1796
|
-
toolCallId: args.toolCallId,
|
|
1797
|
-
toolName: "read"
|
|
1798
|
-
});
|
|
1799
|
-
sendExecClientMessage(h2Request, execMsg, "readResult", execResult);
|
|
1800
|
-
return;
|
|
1801
|
-
}
|
|
1802
|
-
case "lsArgs": {
|
|
1803
|
-
const args = execMsg.message.value;
|
|
1804
|
-
if (!args.toolCallId) args.toolCallId = crypto.randomUUID();
|
|
1805
|
-
synthesizeCursorExecToolCall(output, stream, state, args.toolCallId, "read", { path: args.path });
|
|
1806
|
-
const { execResult } = await resolveExecHandler(args, execHandlers?.ls?.bind(execHandlers), onToolResult, (toolResult) => buildLsResultFromToolResult(args.path, toolResult), (reason) => buildLsRejectedResult(args.path, reason), (error) => buildLsErrorResult(args.path, error), {
|
|
1807
|
-
toolCallId: args.toolCallId,
|
|
1808
|
-
toolName: "read"
|
|
1809
|
-
});
|
|
1810
|
-
sendExecClientMessage(h2Request, execMsg, "lsResult", execResult);
|
|
1811
|
-
return;
|
|
1812
|
-
}
|
|
1813
|
-
case "grepArgs": {
|
|
1814
|
-
const args = execMsg.message.value;
|
|
1815
|
-
if (!args.toolCallId) args.toolCallId = crypto.randomUUID();
|
|
1816
|
-
const emptyPatternError = emptyGrepPatternRejection(args.pattern, args.glob);
|
|
1817
|
-
if (emptyPatternError !== null) {
|
|
1818
|
-
sendExecClientMessage(h2Request, execMsg, "grepResult", buildGrepErrorResult(emptyPatternError));
|
|
1819
|
-
return;
|
|
1820
|
-
}
|
|
1821
|
-
const searchPath = args.glob ? `${args.path || "."}/${args.glob}` : args.path || ".";
|
|
1822
|
-
synthesizeCursorExecToolCall(output, stream, state, args.toolCallId, "grep", {
|
|
1823
|
-
pattern: args.pattern,
|
|
1824
|
-
path: searchPath,
|
|
1825
|
-
case: args.caseInsensitive === true ? false : void 0,
|
|
1826
|
-
skip: piGrepSkip(args.offset)
|
|
1827
|
-
});
|
|
1828
|
-
const { execResult } = await resolveExecHandler(args, execHandlers?.grep?.bind(execHandlers), onToolResult, (toolResult) => buildGrepResultFromToolResult(args, toolResult), (reason) => buildGrepErrorResult(reason), (error) => buildGrepErrorResult(error), {
|
|
1829
|
-
toolCallId: args.toolCallId,
|
|
1830
|
-
toolName: "grep"
|
|
1831
|
-
});
|
|
1832
|
-
sendExecClientMessage(h2Request, execMsg, "grepResult", execResult);
|
|
1833
|
-
return;
|
|
1834
|
-
}
|
|
1835
|
-
case "writeArgs": {
|
|
1836
|
-
const args = execMsg.message.value;
|
|
1837
|
-
if (!args.toolCallId) args.toolCallId = crypto.randomUUID();
|
|
1838
|
-
const editOwned = isEditOwnedToolCallId(state, output, args.toolCallId);
|
|
1839
|
-
const content = args.fileText ?? new TextDecoder().decode(args.fileBytes ?? /* @__PURE__ */ new Uint8Array());
|
|
1840
|
-
if (!editOwned) synthesizeCursorExecToolCall(output, stream, state, args.toolCallId, "write", {
|
|
1841
|
-
path: args.path,
|
|
1842
|
-
content
|
|
1843
|
-
});
|
|
1844
|
-
const write = execHandlers?.write?.bind(execHandlers);
|
|
1845
|
-
const { execResult } = await resolveExecHandler(args, write ? async (writeArgs) => {
|
|
1846
|
-
const result = await write(writeArgs);
|
|
1847
|
-
return editOwned ? remapExecHandlerToolName(result, "edit") : result;
|
|
1848
|
-
} : void 0, onToolResult, (toolResult) => buildWriteResultFromToolResult({
|
|
1849
|
-
path: args.path,
|
|
1850
|
-
fileText: args.fileText,
|
|
1851
|
-
fileBytes: args.fileBytes,
|
|
1852
|
-
returnFileContentAfterWrite: args.returnFileContentAfterWrite
|
|
1853
|
-
}, toolResult), (reason) => buildWriteRejectedResult(args.path, reason), (error) => buildWriteErrorResult(args.path, error), {
|
|
1854
|
-
toolCallId: args.toolCallId,
|
|
1855
|
-
toolName: editOwned ? "edit" : "write"
|
|
1856
|
-
});
|
|
1857
|
-
if (editOwned) markEditToolCallPaired(state, args.toolCallId);
|
|
1858
|
-
sendExecClientMessage(h2Request, execMsg, "writeResult", execResult);
|
|
1859
|
-
return;
|
|
1860
|
-
}
|
|
1861
|
-
case "deleteArgs": {
|
|
1862
|
-
const args = execMsg.message.value;
|
|
1863
|
-
if (!args.toolCallId) args.toolCallId = crypto.randomUUID();
|
|
1864
|
-
synthesizeCursorExecToolCall(output, stream, state, args.toolCallId, "delete", { path: args.path });
|
|
1865
|
-
const { execResult } = await resolveExecHandler(args, execHandlers?.delete?.bind(execHandlers), onToolResult, (toolResult) => buildDeleteResultFromToolResult(args.path, toolResult), (reason) => buildDeleteRejectedResult(args.path, reason), (error) => buildDeleteErrorResult(args.path, error), {
|
|
1866
|
-
toolCallId: args.toolCallId,
|
|
1867
|
-
toolName: "delete"
|
|
1868
|
-
});
|
|
1869
|
-
sendExecClientMessage(h2Request, execMsg, "deleteResult", execResult);
|
|
1870
|
-
return;
|
|
1871
|
-
}
|
|
1872
|
-
case "shellArgs": {
|
|
1873
|
-
const args = execMsg.message.value;
|
|
1874
|
-
if (!args.toolCallId) args.toolCallId = crypto.randomUUID();
|
|
1875
|
-
const normalizedArgs = {
|
|
1876
|
-
...args,
|
|
1877
|
-
workingDirectory: args.workingDirectory || process.cwd()
|
|
1878
|
-
};
|
|
1879
|
-
const shellTimeout = args.timeout && args.timeout > 0 ? args.timeout : void 0;
|
|
1880
|
-
synthesizeCursorExecToolCall(output, stream, state, args.toolCallId, "bash", {
|
|
1881
|
-
command: args.command,
|
|
1882
|
-
cwd: args.workingDirectory || void 0,
|
|
1883
|
-
timeout: shellTimeout
|
|
1884
|
-
});
|
|
1885
|
-
const { execResult } = await resolveExecHandler(args, execHandlers?.shell?.bind(execHandlers), onToolResult, (toolResult) => buildShellResultFromToolResult(normalizedArgs, toolResult), (reason) => buildShellRejectedResult(normalizedArgs.command, normalizedArgs.workingDirectory, reason), (error) => buildShellFailureResult(normalizedArgs.command, normalizedArgs.workingDirectory, error), {
|
|
1886
|
-
toolCallId: args.toolCallId,
|
|
1887
|
-
toolName: "bash"
|
|
1888
|
-
});
|
|
1889
|
-
sendExecClientMessage(h2Request, execMsg, "shellResult", sanitizeShellExecResult(execResult));
|
|
1890
|
-
return;
|
|
1891
|
-
}
|
|
1892
|
-
case "shellStreamArgs": {
|
|
1893
|
-
const args = execMsg.message.value;
|
|
1894
|
-
if (!args.toolCallId) args.toolCallId = crypto.randomUUID();
|
|
1895
|
-
const shellStreamTimeout = args.timeout && args.timeout > 0 ? args.timeout : void 0;
|
|
1896
|
-
synthesizeCursorExecToolCall(output, stream, state, args.toolCallId, "bash", {
|
|
1897
|
-
command: args.command,
|
|
1898
|
-
cwd: args.workingDirectory || void 0,
|
|
1899
|
-
timeout: shellStreamTimeout
|
|
1900
|
-
});
|
|
1901
|
-
await handleShellStreamArgs(args, execMsg, h2Request, execHandlers, onToolResult);
|
|
1902
|
-
return;
|
|
1903
|
-
}
|
|
1904
|
-
case "backgroundShellSpawnArgs": {
|
|
1905
|
-
const args = execMsg.message.value;
|
|
1906
|
-
sendExecClientMessage(h2Request, execMsg, "backgroundShellSpawnResult", create(BackgroundShellSpawnResultSchema, { result: {
|
|
1907
|
-
case: "rejected",
|
|
1908
|
-
value: create(ShellRejectedSchema, {
|
|
1909
|
-
command: args.command,
|
|
1910
|
-
workingDirectory: args.workingDirectory,
|
|
1911
|
-
reason: "Not implemented",
|
|
1912
|
-
isReadonly: false
|
|
1913
|
-
})
|
|
1914
|
-
} }));
|
|
1915
|
-
return;
|
|
1916
|
-
}
|
|
1917
|
-
case "writeShellStdinArgs":
|
|
1918
|
-
sendExecClientMessage(h2Request, execMsg, "writeShellStdinResult", create(WriteShellStdinResultSchema, { result: {
|
|
1919
|
-
case: "error",
|
|
1920
|
-
value: create(WriteShellStdinErrorSchema, { error: "Not implemented" })
|
|
1921
|
-
} }));
|
|
1922
|
-
return;
|
|
1923
|
-
case "fetchArgs": {
|
|
1924
|
-
const args = execMsg.message.value;
|
|
1925
|
-
sendExecClientMessage(h2Request, execMsg, "fetchResult", create(FetchResultSchema, { result: {
|
|
1926
|
-
case: "error",
|
|
1927
|
-
value: create(FetchErrorSchema, {
|
|
1928
|
-
url: args.url,
|
|
1929
|
-
error: "Not implemented"
|
|
1930
|
-
})
|
|
1931
|
-
} }));
|
|
1932
|
-
return;
|
|
1933
|
-
}
|
|
1934
|
-
case "diagnosticsArgs": {
|
|
1935
|
-
const args = execMsg.message.value;
|
|
1936
|
-
if (!args.toolCallId) args.toolCallId = crypto.randomUUID();
|
|
1937
|
-
synthesizeCursorExecToolCall(output, stream, state, args.toolCallId, "lsp", {
|
|
1938
|
-
action: "diagnostics",
|
|
1939
|
-
file: args.path
|
|
1940
|
-
});
|
|
1941
|
-
const { execResult } = await resolveExecHandler(args, execHandlers?.diagnostics?.bind(execHandlers), onToolResult, (toolResult) => buildDiagnosticsResultFromToolResult(args.path, toolResult), (reason) => buildDiagnosticsRejectedResult(args.path, reason), (error) => buildDiagnosticsErrorResult(args.path, error), {
|
|
1942
|
-
toolCallId: args.toolCallId,
|
|
1943
|
-
toolName: "lsp"
|
|
1944
|
-
});
|
|
1945
|
-
sendExecClientMessage(h2Request, execMsg, "diagnosticsResult", execResult);
|
|
1946
|
-
return;
|
|
1947
|
-
}
|
|
1948
|
-
case "mcpArgs": {
|
|
1949
|
-
const args = execMsg.message.value;
|
|
1950
|
-
const mcpCall = decodeMcpCall(args);
|
|
1951
|
-
if (mcpCall.approvalOnly) {
|
|
1952
|
-
const approved = await execHandlers?.mcpApprovalPreflight?.(mcpCall) === true;
|
|
1953
|
-
sendExecClientMessage(h2Request, execMsg, "mcpResult", create(McpResultSchema, { result: approved ? {
|
|
1954
|
-
case: "approved",
|
|
1955
|
-
value: create(McpApprovedSchema, {})
|
|
1956
|
-
} : {
|
|
1957
|
-
case: "rejected",
|
|
1958
|
-
value: create(McpRejectedSchema, { reason: `Tool "${mcpCall.toolName || mcpCall.name}" is not approved to run without asking.` })
|
|
1959
|
-
} }));
|
|
1960
|
-
return;
|
|
1961
|
-
}
|
|
1962
|
-
if (execHandlers?.mcp) {
|
|
1963
|
-
const existingBlock = output.content.find((block) => block.type === "toolCall" && block.id === mcpCall.toolCallId);
|
|
1964
|
-
if (existingBlock) markCursorExecResolved(existingBlock);
|
|
1965
|
-
else {
|
|
1966
|
-
synthesizeCursorExecToolCall(output, stream, state, mcpCall.toolCallId, mcpCall.toolName || mcpCall.name, mcpCall.args);
|
|
1967
|
-
state.resolvedMcpToolCallIds.add(mcpCall.toolCallId);
|
|
1968
|
-
}
|
|
1969
|
-
}
|
|
1970
|
-
const { execResult } = await resolveExecHandler(mcpCall, execHandlers?.mcp?.bind(execHandlers), onToolResult, (toolResult) => buildMcpResultFromToolResult(mcpCall, toolResult), (_reason) => externalToolExecutor && !execHandlers?.mcp ? buildMcpExternalHandoffResult() : buildMcpToolNotFoundResult(mcpCall), (error) => buildMcpErrorResult(error), execHandlers?.mcp ? {
|
|
1971
|
-
toolCallId: mcpCall.toolCallId,
|
|
1972
|
-
toolName: mcpCall.toolName
|
|
1973
|
-
} : null);
|
|
1974
|
-
sendExecClientMessage(h2Request, execMsg, "mcpResult", execResult);
|
|
1975
|
-
return;
|
|
1976
|
-
}
|
|
1977
|
-
case "listMcpResourcesExecArgs": {
|
|
1978
|
-
const args = execMsg.message.value;
|
|
1979
|
-
let execResult;
|
|
1980
|
-
const toolCallId = execHandlers?.listMcpResources ? crypto.randomUUID() : void 0;
|
|
1981
|
-
if (toolCallId) synthesizeCursorExecToolCall(output, stream, state, toolCallId, "list_mcp_resources", { server: args.server });
|
|
1982
|
-
try {
|
|
1983
|
-
const resources = await execHandlers?.listMcpResources?.({ server: args.server }) ?? [];
|
|
1984
|
-
execResult = create(ListMcpResourcesExecResultSchema, { result: {
|
|
1985
|
-
case: "success",
|
|
1986
|
-
value: create(ListMcpResourcesSuccessSchema, { resources: resources.map((resource) => create(ListMcpResourcesExecResult_McpResourceSchema, {
|
|
1987
|
-
uri: resource.uri,
|
|
1988
|
-
name: resource.name,
|
|
1989
|
-
description: resource.description,
|
|
1990
|
-
mimeType: resource.mimeType,
|
|
1991
|
-
server: resource.server
|
|
1992
|
-
})) })
|
|
1993
|
-
} });
|
|
1994
|
-
} catch (error) {
|
|
1995
|
-
execResult = create(ListMcpResourcesExecResultSchema, { result: {
|
|
1996
|
-
case: "error",
|
|
1997
|
-
value: create(ListMcpResourcesErrorSchema, { error: error instanceof Error ? error.message : String(error) })
|
|
1998
|
-
} });
|
|
1999
|
-
}
|
|
2000
|
-
if (toolCallId) {
|
|
2001
|
-
const settled = execResult.result;
|
|
2002
|
-
await pairSynthesizedExecResult(state, onToolResult, toolCallId, "list_mcp_resources", settled.case === "success" ? formatListedMcpResources(settled.value.resources) : settled.case === "error" ? settled.value.error || "Failed to list MCP resources" : settled.value?.reason ?? "Failed to list MCP resources", settled.case !== "success");
|
|
2003
|
-
}
|
|
2004
|
-
sendExecClientMessage(h2Request, execMsg, "listMcpResourcesExecResult", execResult);
|
|
2005
|
-
return;
|
|
2006
|
-
}
|
|
2007
|
-
case "readMcpResourceExecArgs": {
|
|
2008
|
-
const args = execMsg.message.value;
|
|
2009
|
-
let execResult;
|
|
2010
|
-
const toolCallId = execHandlers?.readMcpResource ? crypto.randomUUID() : void 0;
|
|
2011
|
-
if (toolCallId) synthesizeCursorExecToolCall(output, stream, state, toolCallId, "read_mcp_resource", {
|
|
2012
|
-
server: args.server,
|
|
2013
|
-
uri: args.uri,
|
|
2014
|
-
download_path: args.downloadPath
|
|
2015
|
-
});
|
|
2016
|
-
try {
|
|
2017
|
-
const content = await execHandlers?.readMcpResource?.({
|
|
2018
|
-
server: args.server,
|
|
2019
|
-
uri: args.uri,
|
|
2020
|
-
downloadPath: args.downloadPath
|
|
2021
|
-
});
|
|
2022
|
-
execResult = content ? create(ReadMcpResourceExecResultSchema, { result: {
|
|
2023
|
-
case: "success",
|
|
2024
|
-
value: create(ReadMcpResourceSuccessSchema, {
|
|
2025
|
-
uri: content.uri,
|
|
2026
|
-
name: content.name,
|
|
2027
|
-
description: content.description,
|
|
2028
|
-
mimeType: content.mimeType,
|
|
2029
|
-
downloadPath: content.downloadPath,
|
|
2030
|
-
content: content.downloadPath !== void 0 ? { case: void 0 } : content.text !== void 0 ? {
|
|
2031
|
-
case: "text",
|
|
2032
|
-
value: content.text
|
|
2033
|
-
} : content.blob !== void 0 ? {
|
|
2034
|
-
case: "blob",
|
|
2035
|
-
value: content.blob
|
|
2036
|
-
} : { case: void 0 }
|
|
2037
|
-
})
|
|
2038
|
-
} }) : create(ReadMcpResourceExecResultSchema, { result: {
|
|
2039
|
-
case: "notFound",
|
|
2040
|
-
value: create(ReadMcpResourceNotFoundSchema, { uri: args.uri })
|
|
2041
|
-
} });
|
|
2042
|
-
} catch (error) {
|
|
2043
|
-
execResult = create(ReadMcpResourceExecResultSchema, { result: {
|
|
2044
|
-
case: "error",
|
|
2045
|
-
value: create(ReadMcpResourceErrorSchema, {
|
|
2046
|
-
uri: args.uri,
|
|
2047
|
-
error: error instanceof Error ? error.message : String(error)
|
|
2048
|
-
})
|
|
2049
|
-
} });
|
|
2050
|
-
}
|
|
2051
|
-
if (toolCallId) {
|
|
2052
|
-
const settled = execResult.result;
|
|
2053
|
-
let text;
|
|
2054
|
-
switch (settled.case) {
|
|
2055
|
-
case "success":
|
|
2056
|
-
text = settled.value.downloadPath ? `Downloaded ${args.uri} to ${settled.value.downloadPath}` : `Read ${args.uri}`;
|
|
2057
|
-
break;
|
|
2058
|
-
case "notFound":
|
|
2059
|
-
text = `No such resource: ${args.uri}`;
|
|
2060
|
-
break;
|
|
2061
|
-
case "rejected":
|
|
2062
|
-
text = `Refused: ${settled.value.reason}`;
|
|
2063
|
-
break;
|
|
2064
|
-
default: text = settled.value?.error ?? `Failed to read ${args.uri}`;
|
|
2065
|
-
}
|
|
2066
|
-
await pairSynthesizedExecResult(state, onToolResult, toolCallId, "read_mcp_resource", text, settled.case !== "success");
|
|
2067
|
-
}
|
|
2068
|
-
sendExecClientMessage(h2Request, execMsg, "readMcpResourceExecResult", execResult);
|
|
2069
|
-
return;
|
|
2070
|
-
}
|
|
2071
|
-
case "recordScreenArgs":
|
|
2072
|
-
sendExecClientMessage(h2Request, execMsg, "recordScreenResult", create(RecordScreenResultSchema, { result: {
|
|
2073
|
-
case: "failure",
|
|
2074
|
-
value: create(RecordScreenFailureSchema, { error: NOT_IMPLEMENTED })
|
|
2075
|
-
} }));
|
|
2076
|
-
return;
|
|
2077
|
-
case "computerUseArgs":
|
|
2078
|
-
sendExecClientMessage(h2Request, execMsg, "computerUseResult", create(ComputerUseResultSchema, { result: {
|
|
2079
|
-
case: "error",
|
|
2080
|
-
value: create(ComputerUseErrorSchema, { error: NOT_IMPLEMENTED })
|
|
2081
|
-
} }));
|
|
2082
|
-
return;
|
|
2083
|
-
case "piReadArgs": {
|
|
2084
|
-
const args = execMsg.message.value;
|
|
2085
|
-
const toolCallId = crypto.randomUUID();
|
|
2086
|
-
synthesizeCursorExecToolCall(output, stream, state, toolCallId, "read", { path: piReadDisplayPath(args.path, args.offset, args.limit) });
|
|
2087
|
-
const { execResult } = await resolveExecHandler({
|
|
2088
|
-
args,
|
|
2089
|
-
toolCallId
|
|
2090
|
-
}, execHandlers?.piRead?.bind(execHandlers), onToolResult, buildPiReadResult, buildPiReadError, buildPiReadError, {
|
|
2091
|
-
toolCallId,
|
|
2092
|
-
toolName: "read"
|
|
2093
|
-
});
|
|
2094
|
-
sendExecClientMessage(h2Request, execMsg, "piReadResult", execResult);
|
|
2095
|
-
return;
|
|
2096
|
-
}
|
|
2097
|
-
case "piBashArgs": {
|
|
2098
|
-
const args = execMsg.message.value;
|
|
2099
|
-
const toolCallId = crypto.randomUUID();
|
|
2100
|
-
synthesizeCursorExecToolCall(output, stream, state, toolCallId, "bash", {
|
|
2101
|
-
command: args.command,
|
|
2102
|
-
timeout: piTimeout(args.timeout)
|
|
2103
|
-
});
|
|
2104
|
-
const { execResult } = await resolveExecHandler({
|
|
2105
|
-
args,
|
|
2106
|
-
toolCallId
|
|
2107
|
-
}, execHandlers?.piBash?.bind(execHandlers), onToolResult, buildPiBashResult, buildPiBashError, buildPiBashError, {
|
|
2108
|
-
toolCallId,
|
|
2109
|
-
toolName: "bash"
|
|
2110
|
-
});
|
|
2111
|
-
sendExecClientMessage(h2Request, execMsg, "piBashResult", execResult);
|
|
2112
|
-
return;
|
|
2113
|
-
}
|
|
2114
|
-
case "piEditArgs": {
|
|
2115
|
-
const args = execMsg.message.value;
|
|
2116
|
-
const toolCallId = crypto.randomUUID();
|
|
2117
|
-
const firstEdit = args.edits[0];
|
|
2118
|
-
synthesizeCursorExecToolCall(output, stream, state, toolCallId, "edit", {
|
|
2119
|
-
path: args.path,
|
|
2120
|
-
old_string: firstEdit?.oldText ?? "",
|
|
2121
|
-
new_string: firstEdit?.newText ?? ""
|
|
2122
|
-
});
|
|
2123
|
-
const { execResult } = await resolveExecHandler({
|
|
2124
|
-
args,
|
|
2125
|
-
toolCallId
|
|
2126
|
-
}, execHandlers?.piEdit?.bind(execHandlers), onToolResult, buildPiEditResult, buildPiEditRejected, buildPiEditError, {
|
|
2127
|
-
toolCallId,
|
|
2128
|
-
toolName: "edit"
|
|
2129
|
-
});
|
|
2130
|
-
sendExecClientMessage(h2Request, execMsg, "piEditResult", execResult);
|
|
2131
|
-
return;
|
|
2132
|
-
}
|
|
2133
|
-
case "piWriteArgs": {
|
|
2134
|
-
const args = execMsg.message.value;
|
|
2135
|
-
const toolCallId = crypto.randomUUID();
|
|
2136
|
-
synthesizeCursorExecToolCall(output, stream, state, toolCallId, "write", {
|
|
2137
|
-
path: args.path,
|
|
2138
|
-
content: args.content
|
|
2139
|
-
});
|
|
2140
|
-
const { execResult } = await resolveExecHandler({
|
|
2141
|
-
args,
|
|
2142
|
-
toolCallId
|
|
2143
|
-
}, execHandlers?.piWrite?.bind(execHandlers), onToolResult, buildPiWriteResult, buildPiWriteRejected, buildPiWriteError, {
|
|
2144
|
-
toolCallId,
|
|
2145
|
-
toolName: "write"
|
|
2146
|
-
});
|
|
2147
|
-
sendExecClientMessage(h2Request, execMsg, "piWriteResult", execResult);
|
|
2148
|
-
return;
|
|
2149
|
-
}
|
|
2150
|
-
case "piGrepArgs": {
|
|
2151
|
-
const args = execMsg.message.value;
|
|
2152
|
-
const toolCallId = crypto.randomUUID();
|
|
2153
|
-
synthesizeCursorExecToolCall(output, stream, state, toolCallId, "grep", {
|
|
2154
|
-
pattern: args.literal === true ? piEscapeRegexLiteral(args.pattern) : args.pattern,
|
|
2155
|
-
path: args.glob ? piJoinPath(args.path, args.glob) : args.path || ".",
|
|
2156
|
-
case: args.ignoreCase === true ? false : void 0,
|
|
2157
|
-
context: args.context,
|
|
2158
|
-
limit: piLimit(args.limit)
|
|
2159
|
-
});
|
|
2160
|
-
const { execResult } = await resolveExecHandler({
|
|
2161
|
-
args,
|
|
2162
|
-
toolCallId
|
|
2163
|
-
}, execHandlers?.piGrep?.bind(execHandlers), onToolResult, buildPiGrepResult, buildPiGrepError, buildPiGrepError, {
|
|
2164
|
-
toolCallId,
|
|
2165
|
-
toolName: "grep"
|
|
2166
|
-
});
|
|
2167
|
-
sendExecClientMessage(h2Request, execMsg, "piGrepResult", execResult);
|
|
2168
|
-
return;
|
|
2169
|
-
}
|
|
2170
|
-
case "piFindArgs": {
|
|
2171
|
-
const args = execMsg.message.value;
|
|
2172
|
-
const toolCallId = crypto.randomUUID();
|
|
2173
|
-
synthesizeCursorExecToolCall(output, stream, state, toolCallId, "glob", {
|
|
2174
|
-
path: piJoinPath(args.path, args.pattern),
|
|
2175
|
-
limit: piLimit(args.limit)
|
|
2176
|
-
});
|
|
2177
|
-
const { execResult } = await resolveExecHandler({
|
|
2178
|
-
args,
|
|
2179
|
-
toolCallId
|
|
2180
|
-
}, execHandlers?.piFind?.bind(execHandlers), onToolResult, buildPiFindResult, buildPiFindError, buildPiFindError, {
|
|
2181
|
-
toolCallId,
|
|
2182
|
-
toolName: "glob"
|
|
2183
|
-
});
|
|
2184
|
-
sendExecClientMessage(h2Request, execMsg, "piFindResult", execResult);
|
|
2185
|
-
return;
|
|
2186
|
-
}
|
|
2187
|
-
case "piLsArgs": {
|
|
2188
|
-
const args = execMsg.message.value;
|
|
2189
|
-
const toolCallId = crypto.randomUUID();
|
|
2190
|
-
synthesizeCursorExecToolCall(output, stream, state, toolCallId, "read", { path: piLsPath(args.path) });
|
|
2191
|
-
const { execResult } = await resolveExecHandler({
|
|
2192
|
-
args,
|
|
2193
|
-
toolCallId
|
|
2194
|
-
}, execHandlers?.piLs?.bind(execHandlers), onToolResult, buildPiLsResult, buildPiLsError, buildPiLsError, {
|
|
2195
|
-
toolCallId,
|
|
2196
|
-
toolName: "read"
|
|
2197
|
-
});
|
|
2198
|
-
sendExecClientMessage(h2Request, execMsg, "piLsResult", execResult);
|
|
2199
|
-
return;
|
|
2200
|
-
}
|
|
2201
|
-
case "miniSweAgentBashArgs": {
|
|
2202
|
-
const args = execMsg.message.value;
|
|
2203
|
-
if (!args.toolCallId) args.toolCallId = crypto.randomUUID();
|
|
2204
|
-
const normalizedArgs = {
|
|
2205
|
-
...args,
|
|
2206
|
-
workingDirectory: args.workingDirectory || process.cwd()
|
|
2207
|
-
};
|
|
2208
|
-
synthesizeCursorExecToolCall(output, stream, state, args.toolCallId, "bash", {
|
|
2209
|
-
command: args.command,
|
|
2210
|
-
cwd: args.workingDirectory || void 0,
|
|
2211
|
-
timeout: args.timeout && args.timeout > 0 ? args.timeout : void 0
|
|
2212
|
-
});
|
|
2213
|
-
const { execResult } = await resolveExecHandler(normalizedArgs, execHandlers?.shell?.bind(execHandlers), onToolResult, (toolResult) => buildShellResultFromToolResult(normalizedArgs, toolResult), (reason) => buildShellRejectedResult(normalizedArgs.command, normalizedArgs.workingDirectory, reason), (error) => buildShellFailureResult(normalizedArgs.command, normalizedArgs.workingDirectory, error), {
|
|
2214
|
-
toolCallId: args.toolCallId,
|
|
2215
|
-
toolName: "bash"
|
|
2216
|
-
});
|
|
2217
|
-
sendExecClientMessage(h2Request, execMsg, "miniSweAgentBashResult", sanitizeShellExecResult(execResult));
|
|
2218
|
-
return;
|
|
2219
|
-
}
|
|
2220
|
-
case "redactedReadArgs": {
|
|
2221
|
-
const args = execMsg.message.value;
|
|
2222
|
-
sendExecClientMessage(h2Request, execMsg, "redactedReadResult", buildReadErrorResult(args.path, "Secret redaction is not implemented by this client"));
|
|
2223
|
-
return;
|
|
2224
|
-
}
|
|
2225
|
-
case "mcpStateExecArgs": {
|
|
2226
|
-
const args = execMsg.message.value;
|
|
2227
|
-
sendExecClientMessage(h2Request, execMsg, "mcpStateExecResult", buildMcpStateResult(requestContextTools, args.serverIdentifiers));
|
|
2228
|
-
return;
|
|
2229
|
-
}
|
|
2230
|
-
case "executeHookArgs": {
|
|
2231
|
-
const args = execMsg.message.value;
|
|
2232
|
-
const execResult = buildNeutralHookResult(args.request);
|
|
2233
|
-
if (!execResult) {
|
|
2234
|
-
sendExecClientThrow(h2Request, execMsg, `Unsupported hook request: ${args.request?.request.case ?? "unset"}`, "unknown_hook_request");
|
|
2235
|
-
return;
|
|
2236
|
-
}
|
|
2237
|
-
sendExecClientMessage(h2Request, execMsg, "executeHookResult", execResult);
|
|
2238
|
-
return;
|
|
2239
|
-
}
|
|
2240
|
-
case "subagentArgs": {
|
|
2241
|
-
const args = execMsg.message.value;
|
|
2242
|
-
const execResult = create(SubagentResultSchema, { result: {
|
|
2243
|
-
case: "error",
|
|
2244
|
-
value: create(SubagentErrorSchema, { error: `Subagents are ${NOT_IMPLEMENTED_SUFFIX}` })
|
|
2245
|
-
} });
|
|
2246
|
-
log("exec", "subagentRejected", { subagentType: args.subagentType });
|
|
2247
|
-
sendExecClientMessage(h2Request, execMsg, "subagentResult", execResult);
|
|
2248
|
-
return;
|
|
2249
|
-
}
|
|
2250
|
-
case "subagentAwaitArgs": {
|
|
2251
|
-
const args = execMsg.message.value;
|
|
2252
|
-
sendExecClientMessage(h2Request, execMsg, "subagentAwaitResult", create(SubagentAwaitResultSchema, { result: {
|
|
2253
|
-
case: "notFound",
|
|
2254
|
-
value: create(SubagentAwaitNotFoundSchema, { agentId: args.agentId })
|
|
2255
|
-
} }));
|
|
2256
|
-
return;
|
|
2257
|
-
}
|
|
2258
|
-
case "forceBackgroundShellArgs":
|
|
2259
|
-
sendExecClientMessage(h2Request, execMsg, "forceBackgroundShellResult", create(ForceBackgroundShellResultSchema, { status: 2 }));
|
|
2260
|
-
return;
|
|
2261
|
-
case "forceBackgroundSubagentArgs":
|
|
2262
|
-
sendExecClientMessage(h2Request, execMsg, "forceBackgroundSubagentResult", create(ForceBackgroundSubagentResultSchema, { status: 2 }));
|
|
2263
|
-
return;
|
|
2264
|
-
case "smartModeClassifierArgs":
|
|
2265
|
-
sendExecClientMessage(h2Request, execMsg, "smartModeClassifierResult", create(SmartModeClassifierResultSchema, { result: {
|
|
2266
|
-
case: "error",
|
|
2267
|
-
value: create(SmartModeClassifierErrorSchema, { error: `Smart-mode classification is ${NOT_IMPLEMENTED_SUFFIX}` })
|
|
2268
|
-
} }));
|
|
2269
|
-
return;
|
|
2270
|
-
case "canvasDiagnosticsArgs": {
|
|
2271
|
-
const args = execMsg.message.value;
|
|
2272
|
-
sendExecClientMessage(h2Request, execMsg, "canvasDiagnosticsResult", create(CanvasDiagnosticsResultSchema, { result: {
|
|
2273
|
-
case: "error",
|
|
2274
|
-
value: create(CanvasDiagnosticsErrorSchema, {
|
|
2275
|
-
path: args.path,
|
|
2276
|
-
error: `Canvas diagnostics are ${NOT_IMPLEMENTED_SUFFIX}`
|
|
2277
|
-
})
|
|
2278
|
-
} }));
|
|
2279
|
-
return;
|
|
2280
|
-
}
|
|
2281
|
-
case "shellAllowlistPrecheckArgs":
|
|
2282
|
-
sendExecClientMessage(h2Request, execMsg, "shellAllowlistPrecheckResult", create(ShellAllowlistPrecheckResultSchema, { allowlisted: false }));
|
|
2283
|
-
return;
|
|
2284
|
-
case "mcpAllowlistPrecheckArgs":
|
|
2285
|
-
sendExecClientMessage(h2Request, execMsg, "mcpAllowlistPrecheckResult", create(McpAllowlistPrecheckResultSchema, { allowlisted: false }));
|
|
2286
|
-
return;
|
|
2287
|
-
case "webFetchAllowlistPrecheckArgs":
|
|
2288
|
-
sendExecClientMessage(h2Request, execMsg, "webFetchAllowlistPrecheckResult", create(WebFetchAllowlistPrecheckResultSchema, { allowlisted: false }));
|
|
2289
|
-
return;
|
|
2290
|
-
case "conversationSearchArgs": {
|
|
2291
|
-
const args = execMsg.message.value;
|
|
2292
|
-
const toolCallId = args.toolCallId || crypto.randomUUID();
|
|
2293
|
-
const error = `Conversation search is ${NOT_IMPLEMENTED_SUFFIX}`;
|
|
2294
|
-
synthesizeCursorExecToolCall(output, stream, state, toolCallId, "search_conversations", {
|
|
2295
|
-
query: args.query,
|
|
2296
|
-
limit: args.limit
|
|
2297
|
-
});
|
|
2298
|
-
await pairSynthesizedExecResult(state, onToolResult, toolCallId, "search_conversations", error);
|
|
2299
|
-
sendExecClientMessage(h2Request, execMsg, "conversationSearchResult", create(ConversationSearchResultSchema, { result: {
|
|
2300
|
-
case: "error",
|
|
2301
|
-
value: create(ConversationSearchErrorSchema, { error })
|
|
2302
|
-
} }));
|
|
2303
|
-
return;
|
|
2304
|
-
}
|
|
2305
|
-
case "agentStoreConflictArgs":
|
|
2306
|
-
sendExecClientMessage(h2Request, execMsg, "agentStoreConflictResult", create(AgentStoreConflictResultSchema, { result: {
|
|
2307
|
-
case: "error",
|
|
2308
|
-
value: create(AgentStoreConflictErrorSchema, { error: `Agent store conflicts are ${NOT_IMPLEMENTED_SUFFIX}` })
|
|
2309
|
-
} }));
|
|
2310
|
-
return;
|
|
2311
|
-
case "gitDiffRequest":
|
|
2312
|
-
sendExecClientThrow(h2Request, execMsg, `Git diff is ${NOT_IMPLEMENTED_SUFFIX}`, "exec_variant_unsupported");
|
|
2313
|
-
return;
|
|
2314
|
-
default:
|
|
2315
|
-
log("warn", "unhandledExecMessage", { execCase });
|
|
2316
|
-
sendExecClientThrow(h2Request, execMsg, `No handler for exec message of type ${execCase}`, "exec_variant_unsupported");
|
|
2317
|
-
}
|
|
2318
|
-
}
|
|
2319
|
-
/**
|
|
2320
|
-
* Send one typed answer on the exec channel.
|
|
2321
|
-
*
|
|
2322
|
-
* `ExecClientMessage["message"]` is a discriminated union pairing each case
|
|
2323
|
-
* with its own result type, so the generic is keyed on the case: passing a
|
|
2324
|
-
* `ReadResult` under `"shellResult"` is a compile error rather than a wire
|
|
2325
|
-
* message the server rejects at runtime.
|
|
2326
|
-
*/
|
|
2327
|
-
function sendExecClientMessage(h2Request, execMsg, messageCase, value) {
|
|
2328
|
-
const execClientMessage = create(ExecClientMessageSchema, {
|
|
2329
|
-
id: execMsg.id,
|
|
2330
|
-
execId: execMsg.execId,
|
|
2331
|
-
message: {
|
|
2332
|
-
case: messageCase,
|
|
2333
|
-
value
|
|
2334
|
-
}
|
|
2335
|
-
});
|
|
2336
|
-
const clientMessage = create(AgentClientMessageSchema, { message: {
|
|
2337
|
-
case: "execClientMessage",
|
|
2338
|
-
value: execClientMessage
|
|
2339
|
-
} });
|
|
2340
|
-
const responseBytes = toBinary(AgentClientMessageSchema, clientMessage);
|
|
2341
|
-
h2Request.write(frameConnectMessage(responseBytes));
|
|
2342
|
-
log("execClientMessage", messageCase, value);
|
|
2343
|
-
}
|
|
2344
|
-
/**
|
|
2345
|
-
* Fail one exec frame in band.
|
|
2346
|
-
*
|
|
2347
|
-
* `ExecClientThrow` is the protocol's failure channel for a frame that cannot
|
|
2348
|
-
* be answered at all — as opposed to a frame answered with its own typed error
|
|
2349
|
-
* variant, which means "the tool ran and failed". Cursor's own executor sends
|
|
2350
|
-
* exactly this for a frame no handler claims
|
|
2351
|
-
* (`agent-exec/dist/index.js`: `No handler found for server message of type …`
|
|
2352
|
-
* → `case: 'throw'` then `streamClose`), so the server already knows how to
|
|
2353
|
-
* recover from it: it surfaces the error to the model instead of blocking on a
|
|
2354
|
-
* reply that never comes.
|
|
2355
|
-
*
|
|
2356
|
-
* The alternative this replaces — writing an `ExecClientMessage` whose `message`
|
|
2357
|
-
* oneof is unset — is not a valid answer: the server sees a reply carrying no
|
|
2358
|
-
* result and cannot tell it apart from a malformed frame.
|
|
2359
|
-
*/
|
|
2360
|
-
function sendExecClientThrow(h2Request, execMsg, error, errorCode) {
|
|
2361
|
-
const controlMessage = create(ExecClientControlMessageSchema, { message: {
|
|
2362
|
-
case: "throw",
|
|
2363
|
-
value: create(ExecClientThrowSchema, {
|
|
2364
|
-
id: execMsg.id,
|
|
2365
|
-
error,
|
|
2366
|
-
errorCode
|
|
2367
|
-
})
|
|
2368
|
-
} });
|
|
2369
|
-
const clientMessage = create(AgentClientMessageSchema, { message: {
|
|
2370
|
-
case: "execClientControlMessage",
|
|
2371
|
-
value: controlMessage
|
|
2372
|
-
} });
|
|
2373
|
-
h2Request.write(frameConnectMessage(toBinary(AgentClientMessageSchema, clientMessage)));
|
|
2374
|
-
log("execClientControl", "throw", {
|
|
2375
|
-
id: execMsg.id,
|
|
2376
|
-
execId: execMsg.execId,
|
|
2377
|
-
error,
|
|
2378
|
-
errorCode
|
|
2379
|
-
});
|
|
2380
|
-
sendExecClientStreamClose(h2Request, execMsg);
|
|
2381
|
-
}
|
|
2382
|
-
function sendExecClientStreamClose(h2Request, execMsg) {
|
|
2383
|
-
const closeMessage = create(ExecClientControlMessageSchema, { message: {
|
|
2384
|
-
case: "streamClose",
|
|
2385
|
-
value: create(ExecClientStreamCloseSchema, { id: execMsg.id })
|
|
2386
|
-
} });
|
|
2387
|
-
const clientMessage = create(AgentClientMessageSchema, { message: {
|
|
2388
|
-
case: "execClientControlMessage",
|
|
2389
|
-
value: closeMessage
|
|
2390
|
-
} });
|
|
2391
|
-
const responseBytes = toBinary(AgentClientMessageSchema, clientMessage);
|
|
2392
|
-
h2Request.write(frameConnectMessage(responseBytes));
|
|
2393
|
-
log("execClientControl", "streamClose", {
|
|
2394
|
-
id: execMsg.id,
|
|
2395
|
-
execId: execMsg.execId
|
|
2396
|
-
});
|
|
2397
|
-
}
|
|
2398
|
-
/**
|
|
2399
|
-
* Exported for tests: verifies handler is invoked with correct `this` when passed as bound.
|
|
2400
|
-
*
|
|
2401
|
-
* Every exit pairs a `toolResult`. The synthesized block was already marked
|
|
2402
|
-
* `kCursorExecResolved` before this runs (`synthesizeCursorExecToolCall`), so
|
|
2403
|
-
* `agent-loop.ts` emits no placeholder for it: a path that returns without a
|
|
2404
|
-
* result leaves the call unpaired and `buildSessionContext` strips the whole
|
|
2405
|
-
* interaction on replay. The three result-less paths — no handler installed, a
|
|
2406
|
-
* handler that produced nothing, and a thrown handler — therefore synthesize
|
|
2407
|
-
* one from the same text the server sees in `execResult`.
|
|
2408
|
-
*
|
|
2409
|
-
* `pairing` is required so a new callsite cannot silently recreate the orphan,
|
|
2410
|
-
* and nullable for the one caller whose block is NOT pre-resolved: MCP without
|
|
2411
|
-
* an `mcp` handler, which `agent-loop.ts` runs locally and pairs itself.
|
|
2412
|
-
*/
|
|
2413
|
-
async function resolveExecHandler(args, handler, onToolResult, buildFromToolResult, buildRejected, buildError, pairing) {
|
|
2414
|
-
const pair = async (text, isError) => {
|
|
2415
|
-
if (!pairing) return void 0;
|
|
2416
|
-
return await applyToolResultHandler({
|
|
2417
|
-
role: "toolResult",
|
|
2418
|
-
toolCallId: pairing.toolCallId,
|
|
2419
|
-
toolName: pairing.toolName,
|
|
2420
|
-
content: [{
|
|
2421
|
-
type: "text",
|
|
2422
|
-
text
|
|
2423
|
-
}],
|
|
2424
|
-
isError,
|
|
2425
|
-
timestamp: Date.now()
|
|
2426
|
-
}, onToolResult);
|
|
2427
|
-
};
|
|
2428
|
-
if (!handler) {
|
|
2429
|
-
const reason = "Tool not available";
|
|
2430
|
-
return {
|
|
2431
|
-
execResult: buildRejected(reason),
|
|
2432
|
-
toolResult: await pair(reason, true)
|
|
2433
|
-
};
|
|
2434
|
-
}
|
|
2435
|
-
try {
|
|
2436
|
-
const { execResult, toolResult } = splitExecHandlerResult(await handler(args));
|
|
2437
|
-
const finalToolResult = await applyToolResultHandler(toolResult, onToolResult);
|
|
2438
|
-
if (execResult) return {
|
|
2439
|
-
execResult,
|
|
2440
|
-
toolResult: finalToolResult ?? await pair(...describeExecResult(execResult))
|
|
2441
|
-
};
|
|
2442
|
-
if (finalToolResult) return {
|
|
2443
|
-
execResult: buildFromToolResult(finalToolResult),
|
|
2444
|
-
toolResult: finalToolResult
|
|
2445
|
-
};
|
|
2446
|
-
const reason = "Tool returned no result";
|
|
2447
|
-
return {
|
|
2448
|
-
execResult: buildRejected(reason),
|
|
2449
|
-
toolResult: await pair(reason, true)
|
|
2450
|
-
};
|
|
2451
|
-
} catch (error) {
|
|
2452
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
2453
|
-
return {
|
|
2454
|
-
execResult: buildError(message),
|
|
2455
|
-
toolResult: await pair(message, true)
|
|
2456
|
-
};
|
|
2457
|
-
}
|
|
2458
|
-
}
|
|
2459
|
-
/**
|
|
2460
|
-
* Derive the transcript state of an exec result the SDK handler returned in the
|
|
2461
|
-
* R-only form, which carries no `toolResult` to copy it from.
|
|
2462
|
-
*
|
|
2463
|
-
* Every exec result in `agent.proto` is a `oneof result` whose success variant
|
|
2464
|
-
* is named `success` — the rest (`error`, `rejected`, `file_not_found`,
|
|
2465
|
-
* `permission_denied`, `invalid_file`, ...) are failures. Recording those as a
|
|
2466
|
-
* successful call would show the user a green entry for a call Cursor was told
|
|
2467
|
-
* failed. The variant's own `error`/`reason` text is the same string the server
|
|
2468
|
-
* receives, so it is reused verbatim as the transcript body.
|
|
2469
|
-
*
|
|
2470
|
-
* MCP is the one shape where `success` is not enough: `McpSuccess.is_error`
|
|
2471
|
-
* carries an application-level tool failure inside the success variant
|
|
2472
|
-
* (`agent.proto:2058`), mirroring the MCP spec's own `isError`. The transport
|
|
2473
|
-
* succeeded, the tool did not — so the entry must be a failure, and its text
|
|
2474
|
-
* comes from the payload's own content rather than a placeholder.
|
|
2475
|
-
*/
|
|
2476
|
-
function describeExecResult(execResult) {
|
|
2477
|
-
const result = execResult?.result;
|
|
2478
|
-
const variant = result?.case;
|
|
2479
|
-
if (variant === "success") {
|
|
2480
|
-
const success = result?.value;
|
|
2481
|
-
if (!success?.isError) return ["Tool produced no transcript result", false];
|
|
2482
|
-
return [mcpContentToText(success.content) || "MCP tool reported an error", true];
|
|
2483
|
-
}
|
|
2484
|
-
if (!variant) return ["Tool produced no transcript result", false];
|
|
2485
|
-
const value = result?.value;
|
|
2486
|
-
return [value?.error || value?.reason || `Tool call ${variant}`, true];
|
|
2487
|
-
}
|
|
2488
|
-
/**
|
|
2489
|
-
* Flatten `McpSuccess.content` into transcript text. Image items carry no text
|
|
2490
|
-
* to surface, so only the text variant contributes; an all-image failure falls
|
|
2491
|
-
* back to the caller's generic message.
|
|
2492
|
-
*/
|
|
2493
|
-
function mcpContentToText(content) {
|
|
2494
|
-
if (!Array.isArray(content)) return "";
|
|
2495
|
-
const parts = [];
|
|
2496
|
-
for (const item of content) {
|
|
2497
|
-
const inner = item?.content;
|
|
2498
|
-
if (inner?.case === "text" && inner.value?.text) parts.push(inner.value.text);
|
|
2499
|
-
}
|
|
2500
|
-
return parts.join("\n");
|
|
2501
|
-
}
|
|
2502
|
-
function splitExecHandlerResult(result) {
|
|
2503
|
-
if (isToolResultMessage(result)) return { toolResult: result };
|
|
2504
|
-
if (result && typeof result === "object") {
|
|
2505
|
-
const record = result;
|
|
2506
|
-
if ("execResult" in record) {
|
|
2507
|
-
const { execResult, toolResult } = record;
|
|
2508
|
-
return {
|
|
2509
|
-
execResult,
|
|
2510
|
-
toolResult
|
|
2511
|
-
};
|
|
2512
|
-
}
|
|
2513
|
-
if ("toolResult" in record && !isToolResultMessage(record)) {
|
|
2514
|
-
const { result: execResult, toolResult } = record;
|
|
2515
|
-
return {
|
|
2516
|
-
execResult,
|
|
2517
|
-
toolResult
|
|
2518
|
-
};
|
|
2519
|
-
}
|
|
2520
|
-
if ("result" in record && !("$typeName" in record)) {
|
|
2521
|
-
const { result: execResult, toolResult } = record;
|
|
2522
|
-
return {
|
|
2523
|
-
execResult,
|
|
2524
|
-
toolResult
|
|
2525
|
-
};
|
|
2526
|
-
}
|
|
2527
|
-
}
|
|
2528
|
-
return { execResult: result };
|
|
2529
|
-
}
|
|
2530
|
-
function isToolResultMessage(value) {
|
|
2531
|
-
return !!value && typeof value === "object" && value.role === "toolResult";
|
|
2532
|
-
}
|
|
2533
|
-
async function applyToolResultHandler(toolResult, onToolResult) {
|
|
2534
|
-
if (!toolResult || !onToolResult) return toolResult;
|
|
2535
|
-
return await onToolResult(toolResult) ?? toolResult;
|
|
2536
|
-
}
|
|
2537
|
-
function toolResultToText(toolResult) {
|
|
2538
|
-
return toolResult.content.map((item) => item.type === "text" ? item.text : `[${item.mimeType} image]`).join("\n");
|
|
2539
|
-
}
|
|
2540
|
-
/**
|
|
2541
|
-
* The catalog as the paired transcript result records it.
|
|
2542
|
-
*
|
|
2543
|
-
* Cursor receives every resource's identity on the wire, but rebuilt history is
|
|
2544
|
-
* serialized from this local result — so recording only a count leaves the
|
|
2545
|
-
* model, one reload later, aware that it once saw N resources and unable to
|
|
2546
|
-
* name any of them. The URI is what a follow-up `read_mcp_resource` needs, so
|
|
2547
|
-
* it leads; name and mime type follow only when the server supplied them.
|
|
2548
|
-
*/
|
|
2549
|
-
function formatListedMcpResources(resources) {
|
|
2550
|
-
if (resources.length === 0) return "No MCP resources available";
|
|
2551
|
-
const lines = resources.map((resource) => {
|
|
2552
|
-
const qualifiers = [resource.name, resource.mimeType].filter((part) => !!part).join(", ");
|
|
2553
|
-
const server = resource.server ? `[${resource.server}] ` : "";
|
|
2554
|
-
return qualifiers ? `- ${server}${resource.uri} (${qualifiers})` : `- ${server}${resource.uri}`;
|
|
2555
|
-
});
|
|
2556
|
-
return [`Listed ${resources.length} MCP resource(s):`, ...lines].join("\n");
|
|
2557
|
-
}
|
|
2558
|
-
function toolResultWasTruncated(toolResult) {
|
|
2559
|
-
if (!toolResult.details || typeof toolResult.details !== "object") return false;
|
|
2560
|
-
return !!toolResult.details.truncation?.truncated;
|
|
2561
|
-
}
|
|
2562
|
-
function toolResultDetailBoolean(toolResult, key) {
|
|
2563
|
-
if (!toolResult.details || typeof toolResult.details !== "object") return false;
|
|
2564
|
-
const value = toolResult.details[key];
|
|
2565
|
-
return typeof value === "boolean" ? value : false;
|
|
2566
|
-
}
|
|
2567
|
-
/**
|
|
2568
|
-
* The file's own line count, when the tool recorded one.
|
|
2569
|
-
*
|
|
2570
|
-
* Read results expose the source-wide count directly when known. Older tool
|
|
2571
|
-
* results carry it at `details.meta.truncation.totalLines`; the flat
|
|
2572
|
-
* `details.truncation.totalLines` counts from a window's start and is
|
|
2573
|
-
* deliberately not consulted here.
|
|
2574
|
-
*/
|
|
2575
|
-
function readTotalLinesFromDetails(toolResult) {
|
|
2576
|
-
const details = toolResult.details;
|
|
2577
|
-
if (!details || typeof details !== "object") return void 0;
|
|
2578
|
-
const direct = "totalLines" in details ? details.totalLines : void 0;
|
|
2579
|
-
if (typeof direct === "number" && Number.isFinite(direct)) return direct;
|
|
2580
|
-
const meta = "meta" in details ? details.meta : void 0;
|
|
2581
|
-
if (!meta || typeof meta !== "object") return void 0;
|
|
2582
|
-
const truncation = "truncation" in meta ? meta.truncation : void 0;
|
|
2583
|
-
if (!truncation || typeof truncation !== "object") return void 0;
|
|
2584
|
-
const totalLines = "totalLines" in truncation ? truncation.totalLines : void 0;
|
|
2585
|
-
return typeof totalLines === "number" && Number.isFinite(totalLines) ? totalLines : void 0;
|
|
2586
|
-
}
|
|
2587
|
-
function readFileSizeFromDetails(toolResult) {
|
|
2588
|
-
const details = toolResult.details;
|
|
2589
|
-
if (!details || typeof details !== "object" || !("fileSize" in details)) return void 0;
|
|
2590
|
-
const { fileSize } = details;
|
|
2591
|
-
return typeof fileSize === "number" && Number.isSafeInteger(fileSize) && fileSize >= 0 ? fileSize : void 0;
|
|
2592
|
-
}
|
|
2593
|
-
function buildReadResultFromToolResult(path, toolResult, rangeApplied = false) {
|
|
2594
|
-
const text = toolResultToText(toolResult);
|
|
2595
|
-
if (toolResult.isError) return buildReadErrorResult(path, text || "Read failed");
|
|
2596
|
-
const totalLines = readTotalLinesFromDetails(toolResult) ?? (rangeApplied ? 0 : text ? text.split("\n").length : 0);
|
|
2597
|
-
return create(ReadResultSchema, { result: {
|
|
2598
|
-
case: "success",
|
|
2599
|
-
value: create(ReadSuccessSchema, {
|
|
2600
|
-
path,
|
|
2601
|
-
totalLines,
|
|
2602
|
-
fileSize: BigInt(readFileSizeFromDetails(toolResult) ?? Buffer.byteLength(text, "utf-8")),
|
|
2603
|
-
truncated: toolResultWasTruncated(toolResult),
|
|
2604
|
-
output: {
|
|
2605
|
-
case: "content",
|
|
2606
|
-
value: text
|
|
2607
|
-
},
|
|
2608
|
-
rangeApplied
|
|
2609
|
-
})
|
|
2610
|
-
} });
|
|
2611
|
-
}
|
|
2612
|
-
function buildReadErrorResult(path, error) {
|
|
2613
|
-
return create(ReadResultSchema, { result: {
|
|
2614
|
-
case: "error",
|
|
2615
|
-
value: create(ReadErrorSchema, {
|
|
2616
|
-
path,
|
|
2617
|
-
error
|
|
2618
|
-
})
|
|
2619
|
-
} });
|
|
2620
|
-
}
|
|
2621
|
-
function buildReadRejectedResult(path, reason) {
|
|
2622
|
-
return create(ReadResultSchema, { result: {
|
|
2623
|
-
case: "rejected",
|
|
2624
|
-
value: create(ReadRejectedSchema, {
|
|
2625
|
-
path,
|
|
2626
|
-
reason
|
|
2627
|
-
})
|
|
2628
|
-
} });
|
|
2629
|
-
}
|
|
2630
|
-
function buildWriteResultFromToolResult(args, toolResult) {
|
|
2631
|
-
const text = toolResultToText(toolResult);
|
|
2632
|
-
if (toolResult.isError) return buildWriteErrorResult(args.path, text || "Write failed");
|
|
2633
|
-
const fileText = args.fileText ?? "";
|
|
2634
|
-
const fileSize = args.fileBytes?.length ?? Buffer.byteLength(fileText, "utf-8");
|
|
2635
|
-
const linesCreated = fileText ? fileText.split("\n").length : 0;
|
|
2636
|
-
return create(WriteResultSchema, { result: {
|
|
2637
|
-
case: "success",
|
|
2638
|
-
value: create(WriteSuccessSchema, {
|
|
2639
|
-
path: args.path,
|
|
2640
|
-
linesCreated,
|
|
2641
|
-
fileSize,
|
|
2642
|
-
fileContentAfterWrite: args.returnFileContentAfterWrite ? fileText : void 0
|
|
2643
|
-
})
|
|
2644
|
-
} });
|
|
2645
|
-
}
|
|
2646
|
-
function buildWriteErrorResult(path, error) {
|
|
2647
|
-
return create(WriteResultSchema, { result: {
|
|
2648
|
-
case: "error",
|
|
2649
|
-
value: create(WriteErrorSchema, {
|
|
2650
|
-
path,
|
|
2651
|
-
error
|
|
2652
|
-
})
|
|
2653
|
-
} });
|
|
2654
|
-
}
|
|
2655
|
-
function buildWriteRejectedResult(path, reason) {
|
|
2656
|
-
return create(WriteResultSchema, { result: {
|
|
2657
|
-
case: "rejected",
|
|
2658
|
-
value: create(WriteRejectedSchema, {
|
|
2659
|
-
path,
|
|
2660
|
-
reason
|
|
2661
|
-
})
|
|
2662
|
-
} });
|
|
2663
|
-
}
|
|
2664
|
-
function buildDeleteResultFromToolResult(path, toolResult) {
|
|
2665
|
-
const text = toolResultToText(toolResult);
|
|
2666
|
-
if (toolResult.isError) return buildDeleteErrorResult(path, text || "Delete failed");
|
|
2667
|
-
return create(DeleteResultSchema, { result: {
|
|
2668
|
-
case: "success",
|
|
2669
|
-
value: create(DeleteSuccessSchema, {
|
|
2670
|
-
path,
|
|
2671
|
-
deletedFile: path,
|
|
2672
|
-
fileSize: BigInt(0),
|
|
2673
|
-
prevContent: ""
|
|
2674
|
-
})
|
|
2675
|
-
} });
|
|
2676
|
-
}
|
|
2677
|
-
function buildDeleteErrorResult(path, error) {
|
|
2678
|
-
return create(DeleteResultSchema, { result: {
|
|
2679
|
-
case: "error",
|
|
2680
|
-
value: create(DeleteErrorSchema, {
|
|
2681
|
-
path,
|
|
2682
|
-
error
|
|
2683
|
-
})
|
|
2684
|
-
} });
|
|
2685
|
-
}
|
|
2686
|
-
function buildDeleteRejectedResult(path, reason) {
|
|
2687
|
-
return create(DeleteResultSchema, { result: {
|
|
2688
|
-
case: "rejected",
|
|
2689
|
-
value: create(DeleteRejectedSchema, {
|
|
2690
|
-
path,
|
|
2691
|
-
reason
|
|
2692
|
-
})
|
|
2693
|
-
} });
|
|
2694
|
-
}
|
|
2695
|
-
function buildShellResultFromToolResult(args, toolResult) {
|
|
2696
|
-
const output = toolResultToText(toolResult);
|
|
2697
|
-
if (toolResult.isError) return buildShellFailureResult(args.command, args.workingDirectory, output || "Shell failed");
|
|
2698
|
-
return create(ShellResultSchema, { result: {
|
|
2699
|
-
case: "success",
|
|
2700
|
-
value: create(ShellSuccessSchema, {
|
|
2701
|
-
command: args.command,
|
|
2702
|
-
workingDirectory: args.workingDirectory,
|
|
2703
|
-
exitCode: 0,
|
|
2704
|
-
signal: "",
|
|
2705
|
-
stdout: output,
|
|
2706
|
-
stderr: "",
|
|
2707
|
-
executionTime: 0
|
|
2708
|
-
})
|
|
2709
|
-
} });
|
|
2710
|
-
}
|
|
2711
|
-
function buildShellFailureResult(command, workingDirectory, error) {
|
|
2712
|
-
return create(ShellResultSchema, { result: {
|
|
2713
|
-
case: "failure",
|
|
2714
|
-
value: create(ShellFailureSchema, {
|
|
2715
|
-
command,
|
|
2716
|
-
workingDirectory,
|
|
2717
|
-
exitCode: 1,
|
|
2718
|
-
signal: "",
|
|
2719
|
-
stdout: "",
|
|
2720
|
-
stderr: error,
|
|
2721
|
-
executionTime: 0,
|
|
2722
|
-
aborted: false
|
|
2723
|
-
})
|
|
2724
|
-
} });
|
|
2725
|
-
}
|
|
2726
|
-
function buildShellRejectedResult(command, workingDirectory, reason) {
|
|
2727
|
-
return create(ShellResultSchema, { result: {
|
|
2728
|
-
case: "rejected",
|
|
2729
|
-
value: create(ShellRejectedSchema, {
|
|
2730
|
-
command,
|
|
2731
|
-
workingDirectory,
|
|
2732
|
-
reason,
|
|
2733
|
-
isReadonly: false
|
|
2734
|
-
})
|
|
2735
|
-
} });
|
|
2736
|
-
}
|
|
2737
|
-
function buildLsResultFromToolResult(path, toolResult) {
|
|
2738
|
-
const text = toolResultToText(toolResult);
|
|
2739
|
-
if (toolResult.isError) return buildLsErrorResult(path, text || "Ls failed");
|
|
2740
|
-
const rootPath = path || ".";
|
|
2741
|
-
const entries = text.split("\n").map((line) => line.trim()).filter((line) => line.length > 0 && !line.startsWith("["));
|
|
2742
|
-
const childrenDirs = [];
|
|
2743
|
-
const childrenFiles = [];
|
|
2744
|
-
for (const entry of entries) {
|
|
2745
|
-
const name = entry.split(" (")[0];
|
|
2746
|
-
if (name.endsWith("/")) {
|
|
2747
|
-
const dirName = name.slice(0, -1);
|
|
2748
|
-
childrenDirs.push(create(LsDirectoryTreeNodeSchema, {
|
|
2749
|
-
absPath: `${rootPath.replace(/\/$/, "")}/${dirName}`,
|
|
2750
|
-
childrenDirs: [],
|
|
2751
|
-
childrenFiles: [],
|
|
2752
|
-
childrenWereProcessed: false,
|
|
2753
|
-
fullSubtreeExtensionCounts: {},
|
|
2754
|
-
numFiles: 0
|
|
2755
|
-
}));
|
|
2756
|
-
} else childrenFiles.push(create(LsDirectoryTreeNode_FileSchema, { name }));
|
|
2757
|
-
}
|
|
2758
|
-
const root = create(LsDirectoryTreeNodeSchema, {
|
|
2759
|
-
absPath: rootPath,
|
|
2760
|
-
childrenDirs,
|
|
2761
|
-
childrenFiles,
|
|
2762
|
-
childrenWereProcessed: true,
|
|
2763
|
-
fullSubtreeExtensionCounts: {},
|
|
2764
|
-
numFiles: childrenFiles.length
|
|
2765
|
-
});
|
|
2766
|
-
return create(LsResultSchema, { result: {
|
|
2767
|
-
case: "success",
|
|
2768
|
-
value: create(LsSuccessSchema, { directoryTreeRoot: root })
|
|
2769
|
-
} });
|
|
2770
|
-
}
|
|
2771
|
-
function buildLsErrorResult(path, error) {
|
|
2772
|
-
return create(LsResultSchema, { result: {
|
|
2773
|
-
case: "error",
|
|
2774
|
-
value: create(LsErrorSchema, {
|
|
2775
|
-
path,
|
|
2776
|
-
error
|
|
2777
|
-
})
|
|
2778
|
-
} });
|
|
2779
|
-
}
|
|
2780
|
-
function buildLsRejectedResult(path, reason) {
|
|
2781
|
-
return create(LsResultSchema, { result: {
|
|
2782
|
-
case: "rejected",
|
|
2783
|
-
value: create(LsRejectedSchema, {
|
|
2784
|
-
path,
|
|
2785
|
-
reason
|
|
2786
|
-
})
|
|
2787
|
-
} });
|
|
2788
|
-
}
|
|
2789
|
-
function buildGrepResultFromToolResult(args, toolResult) {
|
|
2790
|
-
const text = toolResultToText(toolResult);
|
|
2791
|
-
if (toolResult.isError) return buildGrepErrorResult(text || "Grep failed");
|
|
2792
|
-
const outputMode = args.outputMode || "content";
|
|
2793
|
-
const clientTruncated = toolResultDetailBoolean(toolResult, "truncated");
|
|
2794
|
-
const lines = text.split("\n").map((line) => line.trimEnd()).filter((line) => line.length > 0 && !line.startsWith("[") && !line.toLowerCase().startsWith("no matches"));
|
|
2795
|
-
const workspaceKey = args.path || ".";
|
|
2796
|
-
let unionResult;
|
|
2797
|
-
if (outputMode === "files_with_matches") {
|
|
2798
|
-
const files = lines;
|
|
2799
|
-
unionResult = create(GrepUnionResultSchema, { result: {
|
|
2800
|
-
case: "files",
|
|
2801
|
-
value: create(GrepFilesResultSchema, {
|
|
2802
|
-
files,
|
|
2803
|
-
totalFiles: files.length,
|
|
2804
|
-
clientTruncated,
|
|
2805
|
-
ripgrepTruncated: false,
|
|
2806
|
-
offsetApplied: args.offset
|
|
2807
|
-
})
|
|
2808
|
-
} });
|
|
2809
|
-
} else if (outputMode === "count") {
|
|
2810
|
-
const counts = lines.map((line) => {
|
|
2811
|
-
const separatorIndex = line.lastIndexOf(":");
|
|
2812
|
-
if (separatorIndex === -1) return null;
|
|
2813
|
-
const file = line.slice(0, separatorIndex);
|
|
2814
|
-
const count = Number.parseInt(line.slice(separatorIndex + 1), 10);
|
|
2815
|
-
if (!file || Number.isNaN(count)) return null;
|
|
2816
|
-
return create(GrepFileCountSchema, {
|
|
2817
|
-
file,
|
|
2818
|
-
count
|
|
2819
|
-
});
|
|
2820
|
-
}).filter((entry) => entry !== null);
|
|
2821
|
-
const totalMatches = counts.reduce((sum, entry) => sum + entry.count, 0);
|
|
2822
|
-
unionResult = create(GrepUnionResultSchema, { result: {
|
|
2823
|
-
case: "count",
|
|
2824
|
-
value: create(GrepCountResultSchema, {
|
|
2825
|
-
counts,
|
|
2826
|
-
totalFiles: counts.length,
|
|
2827
|
-
totalMatches,
|
|
2828
|
-
clientTruncated,
|
|
2829
|
-
ripgrepTruncated: false,
|
|
2830
|
-
offsetApplied: args.offset
|
|
2831
|
-
})
|
|
2832
|
-
} });
|
|
2833
|
-
} else {
|
|
2834
|
-
const matchMap = /* @__PURE__ */ new Map();
|
|
2835
|
-
let totalMatchedLines = 0;
|
|
2836
|
-
for (const line of lines) {
|
|
2837
|
-
const matchLine = line.match(/^(.+?):(\d+):\s?(.*)$/);
|
|
2838
|
-
const contextLine = line.match(/^(.+?)-(\d+)-\s?(.*)$/);
|
|
2839
|
-
const match = matchLine ?? contextLine;
|
|
2840
|
-
if (!match) continue;
|
|
2841
|
-
const [, file, lineNumber, content] = match;
|
|
2842
|
-
const isContextLine = Boolean(contextLine);
|
|
2843
|
-
const list = matchMap.get(file) ?? [];
|
|
2844
|
-
list.push({
|
|
2845
|
-
line: Number(lineNumber),
|
|
2846
|
-
content,
|
|
2847
|
-
isContextLine
|
|
2848
|
-
});
|
|
2849
|
-
matchMap.set(file, list);
|
|
2850
|
-
if (!isContextLine) totalMatchedLines += 1;
|
|
2851
|
-
}
|
|
2852
|
-
const matches = Array.from(matchMap.entries()).map(([file, matches]) => create(GrepFileMatchSchema, {
|
|
2853
|
-
file,
|
|
2854
|
-
matches: matches.map((entry) => create(GrepContentMatchSchema, {
|
|
2855
|
-
lineNumber: entry.line,
|
|
2856
|
-
content: entry.content,
|
|
2857
|
-
contentTruncated: false,
|
|
2858
|
-
isContextLine: entry.isContextLine
|
|
2859
|
-
}))
|
|
2860
|
-
}));
|
|
2861
|
-
const totalLines = matches.reduce((sum, entry) => sum + entry.matches.length, 0);
|
|
2862
|
-
unionResult = create(GrepUnionResultSchema, { result: {
|
|
2863
|
-
case: "content",
|
|
2864
|
-
value: create(GrepContentResultSchema, {
|
|
2865
|
-
matches,
|
|
2866
|
-
totalLines,
|
|
2867
|
-
totalMatchedLines,
|
|
2868
|
-
clientTruncated,
|
|
2869
|
-
ripgrepTruncated: false,
|
|
2870
|
-
offsetApplied: args.offset
|
|
2871
|
-
})
|
|
2872
|
-
} });
|
|
2873
|
-
}
|
|
2874
|
-
return create(GrepResultSchema, { result: {
|
|
2875
|
-
case: "success",
|
|
2876
|
-
value: create(GrepSuccessSchema, {
|
|
2877
|
-
pattern: args.pattern,
|
|
2878
|
-
path: args.path || "",
|
|
2879
|
-
outputMode,
|
|
2880
|
-
workspaceResults: { [workspaceKey]: unionResult }
|
|
2881
|
-
})
|
|
2882
|
-
} });
|
|
2883
|
-
}
|
|
2884
|
-
function buildGrepErrorResult(error) {
|
|
2885
|
-
return create(GrepResultSchema, { result: {
|
|
2886
|
-
case: "error",
|
|
2887
|
-
value: create(GrepErrorSchema, { error })
|
|
2888
|
-
} });
|
|
2889
|
-
}
|
|
2890
|
-
/**
|
|
2891
|
-
* Reject a Cursor exec-channel `grepArgs` frame whose `pattern` is empty or
|
|
2892
|
-
* whitespace-only. Returns an actionable error message when the pattern is
|
|
2893
|
-
* unusable (with a `glob`-aware hint when the model likely meant to list
|
|
2894
|
-
* files), or `null` when the pattern is valid and grep should run.
|
|
2895
|
-
*
|
|
2896
|
-
* Exported for tests. Cursor's model sometimes sends `pattern=""` together
|
|
2897
|
-
* with a non-empty `glob`, expecting grep to enumerate matching files; the
|
|
2898
|
-
* downstream coding-agent `grep` tool rejects that with a bare "Pattern must
|
|
2899
|
-
* not be empty", which the TUI renders as `?` in the tool preview (issue
|
|
2900
|
-
* #4574). Handling it at the Cursor exec dispatch keeps the synthesized
|
|
2901
|
-
* `toolCall` block off the persisted assistant message and gives the model a
|
|
2902
|
-
* specific recovery hint.
|
|
2903
|
-
*/
|
|
2904
|
-
function emptyGrepPatternRejection(pattern, glob) {
|
|
2905
|
-
if (pattern && pattern.trim().length > 0) return null;
|
|
2906
|
-
if (glob && glob.length > 0) return `grep pattern is required (received an empty pattern). To list files matching "${glob}", pass a non-empty regex (e.g. ".") and set path to that glob, or use the ls/read tool instead.`;
|
|
2907
|
-
return "grep pattern is required (received an empty pattern).";
|
|
2908
|
-
}
|
|
2909
|
-
function buildDiagnosticsResultFromToolResult(path, toolResult) {
|
|
2910
|
-
const text = toolResultToText(toolResult);
|
|
2911
|
-
if (toolResult.isError) return buildDiagnosticsErrorResult(path, text || "Diagnostics failed");
|
|
2912
|
-
return create(DiagnosticsResultSchema, { result: {
|
|
2913
|
-
case: "success",
|
|
2914
|
-
value: create(DiagnosticsSuccessSchema, {
|
|
2915
|
-
path,
|
|
2916
|
-
diagnostics: [],
|
|
2917
|
-
totalDiagnostics: 0
|
|
2918
|
-
})
|
|
2919
|
-
} });
|
|
2920
|
-
}
|
|
2921
|
-
function buildDiagnosticsErrorResult(_path, error) {
|
|
2922
|
-
return create(DiagnosticsResultSchema, { result: {
|
|
2923
|
-
case: "error",
|
|
2924
|
-
value: create(DiagnosticsErrorSchema, { error })
|
|
2925
|
-
} });
|
|
2926
|
-
}
|
|
2927
|
-
function buildDiagnosticsRejectedResult(path, reason) {
|
|
2928
|
-
return create(DiagnosticsResultSchema, { result: {
|
|
2929
|
-
case: "rejected",
|
|
2930
|
-
value: create(DiagnosticsRejectedSchema, {
|
|
2931
|
-
path,
|
|
2932
|
-
reason
|
|
2933
|
-
})
|
|
2934
|
-
} });
|
|
2935
|
-
}
|
|
2936
|
-
function parseToolArgsJson(text) {
|
|
2937
|
-
const trimmed = text.trim();
|
|
2938
|
-
if (!trimmed) return text;
|
|
2939
|
-
try {
|
|
2940
|
-
return parseJsonWithRepair(trimmed);
|
|
2941
|
-
} catch {
|
|
2942
|
-
return text;
|
|
2943
|
-
}
|
|
2944
|
-
}
|
|
2945
|
-
function decodeMcpArgValue(value) {
|
|
2946
|
-
try {
|
|
2947
|
-
const jsonValue = decodeJsonValue(value);
|
|
2948
|
-
if (typeof jsonValue === "string") {
|
|
2949
|
-
const first = jsonValue.trimStart()[0];
|
|
2950
|
-
return first === "{" || first === "[" || first === "\"" ? parseToolArgsJson(jsonValue) : jsonValue;
|
|
2951
|
-
}
|
|
2952
|
-
return jsonValue;
|
|
2953
|
-
} catch {}
|
|
2954
|
-
return parseToolArgsJson(new TextDecoder().decode(value));
|
|
2955
|
-
}
|
|
2956
|
-
function decodeMcpArgsMap(args) {
|
|
2957
|
-
if (!args) return;
|
|
2958
|
-
const decoded = {};
|
|
2959
|
-
for (const [key, value] of Object.entries(args)) decoded[key] = decodeMcpArgValue(value);
|
|
2960
|
-
return decoded;
|
|
2961
|
-
}
|
|
2962
|
-
function decodeMcpCall(args) {
|
|
2963
|
-
const decodedArgs = {};
|
|
2964
|
-
for (const [key, value] of Object.entries(args.args ?? {})) decodedArgs[key] = decodeMcpArgValue(value);
|
|
2965
|
-
return {
|
|
2966
|
-
name: args.name,
|
|
2967
|
-
providerIdentifier: args.providerIdentifier,
|
|
2968
|
-
toolName: args.toolName || args.name,
|
|
2969
|
-
toolCallId: args.toolCallId,
|
|
2970
|
-
args: decodedArgs,
|
|
2971
|
-
rawArgs: args.args ?? {},
|
|
2972
|
-
approvalOnly: args.smartModeApprovalOnly === true
|
|
2973
|
-
};
|
|
2974
|
-
}
|
|
2975
|
-
/**
|
|
2976
|
-
* Map Cursor's `TodoStatus` enum (agent.proto) onto the local todo statuses.
|
|
2977
|
-
*
|
|
2978
|
-
* `TODO_STATUS_CANCELLED` (4) maps to `abandoned` rather than collapsing to
|
|
2979
|
-
* `pending`, which would resurrect a task the model explicitly cancelled.
|
|
2980
|
-
*/
|
|
2981
|
-
function mapTodoStatusValue(status) {
|
|
2982
|
-
switch (status) {
|
|
2983
|
-
case 2: return "in_progress";
|
|
2984
|
-
case 3: return "completed";
|
|
2985
|
-
case 4: return "abandoned";
|
|
2986
|
-
default: return "pending";
|
|
2987
|
-
}
|
|
2988
|
-
}
|
|
2989
|
-
function selectTodoCalls(toolCall) {
|
|
2990
|
-
const oneof = toolCall.tool;
|
|
2991
|
-
if (oneof?.case === "updateTodosToolCall") return { update: oneof.value };
|
|
2992
|
-
if (oneof?.case === "readTodosToolCall") return { read: oneof.value };
|
|
2993
|
-
return {
|
|
2994
|
-
update: toolCall.updateTodosToolCall,
|
|
2995
|
-
read: toolCall.readTodosToolCall
|
|
2996
|
-
};
|
|
2997
|
-
}
|
|
2998
|
-
function mapTodoSnapshot(todos) {
|
|
2999
|
-
return todos.map((todo) => ({
|
|
3000
|
-
content: typeof todo.content === "string" ? todo.content : "",
|
|
3001
|
-
status: mapTodoStatusValue(typeof todo.status === "number" ? todo.status : void 0)
|
|
3002
|
-
}));
|
|
3003
|
-
}
|
|
3004
|
-
/**
|
|
3005
|
-
* `ToolCall.tool` is a protobuf oneof: a wire-decoded message exposes the
|
|
3006
|
-
* variant as `{ case, value }` and NEVER as a flattened `mcpToolCall`
|
|
3007
|
-
* property. Reading the flat property alone is what made native todo calls
|
|
3008
|
-
* invisible on the wire while hand-shaped test fixtures kept passing, so MCP
|
|
3009
|
-
* goes through the same selector. The flat fallback is kept for those fixtures.
|
|
3010
|
-
*/
|
|
3011
|
-
function selectMcpCall(toolCall) {
|
|
3012
|
-
const oneof = toolCall?.tool;
|
|
3013
|
-
if (oneof?.case === "mcpToolCall") return oneof.value;
|
|
3014
|
-
return toolCall?.mcpToolCall;
|
|
3015
|
-
}
|
|
3016
|
-
/**
|
|
3017
|
-
* Same oneof-first selector as MCP: a wire-decoded `ToolCall` exposes
|
|
3018
|
-
* `editToolCall` only as `{ case: "editToolCall", value }`.
|
|
3019
|
-
*/
|
|
3020
|
-
function selectEditCall(toolCall) {
|
|
3021
|
-
const oneof = toolCall?.tool;
|
|
3022
|
-
if (oneof?.case === "editToolCall") return oneof.value;
|
|
3023
|
-
return toolCall?.editToolCall;
|
|
3024
|
-
}
|
|
3025
|
-
function selectEditStreamDelta(update) {
|
|
3026
|
-
const oneof = update.toolCallDelta?.delta;
|
|
3027
|
-
if (oneof?.case === "editToolCallDelta") return oneof.value?.streamContentDelta;
|
|
3028
|
-
return update.toolCallDelta?.editToolCallDelta?.streamContentDelta;
|
|
3029
|
-
}
|
|
3030
|
-
function rememberEditOwnedToolCall(state, toolCall, envelopeId) {
|
|
3031
|
-
if (!state.editOwnedToolCallIds) state.editOwnedToolCallIds = /* @__PURE__ */ new Set();
|
|
3032
|
-
const ids = state.editOwnedToolCallIds;
|
|
3033
|
-
if (envelopeId) ids.add(envelopeId);
|
|
3034
|
-
if (toolCall?.toolCallId) ids.add(toolCall.toolCallId);
|
|
3035
|
-
}
|
|
3036
|
-
function isEditOwnedToolCallId(state, output, toolCallId) {
|
|
3037
|
-
if (state.editOwnedToolCallIds?.has(toolCallId)) return true;
|
|
3038
|
-
return output.content.some((block) => block.type === "toolCall" && block.id === toolCallId && block.name === "edit");
|
|
3039
|
-
}
|
|
3040
|
-
function stringToolArg(args, key) {
|
|
3041
|
-
const value = args?.[key];
|
|
3042
|
-
return typeof value === "string" ? value : void 0;
|
|
3043
|
-
}
|
|
3044
|
-
function markEditToolCallPaired(state, toolCallId) {
|
|
3045
|
-
if (!state.pairedEditToolCallIds) state.pairedEditToolCallIds = /* @__PURE__ */ new Set();
|
|
3046
|
-
state.pairedEditToolCallIds.add(toolCallId);
|
|
3047
|
-
}
|
|
3048
|
-
/**
|
|
3049
|
-
* `EditToolCall.result` is an `EditResult` message whose own oneof is also
|
|
3050
|
-
* named `result`. The discriminator is `result.result.case`, same nesting as
|
|
3051
|
-
* `describeConnectScmResult` / the todo extractors.
|
|
3052
|
-
*/
|
|
3053
|
-
function describeEditResult(toolCall) {
|
|
3054
|
-
const oneof = selectEditCall(toolCall)?.result?.result;
|
|
3055
|
-
const variant = oneof?.case;
|
|
3056
|
-
const value = oneof?.value;
|
|
3057
|
-
if (variant === "success") return {
|
|
3058
|
-
text: value?.message || value?.path || "Edited",
|
|
3059
|
-
isError: false
|
|
3060
|
-
};
|
|
3061
|
-
if (variant === "error") return {
|
|
3062
|
-
text: value?.modelVisibleError || value?.error || "Edit failed",
|
|
3063
|
-
isError: true
|
|
3064
|
-
};
|
|
3065
|
-
if (variant === "rejected") return {
|
|
3066
|
-
text: value?.reason || "Edit rejected",
|
|
3067
|
-
isError: true
|
|
3068
|
-
};
|
|
3069
|
-
if (variant === "fileNotFound") return {
|
|
3070
|
-
text: value?.path ? `File not found: ${value.path}` : "File not found",
|
|
3071
|
-
isError: true
|
|
3072
|
-
};
|
|
3073
|
-
if (variant === "readPermissionDenied") return {
|
|
3074
|
-
text: value?.path ? `Read permission denied: ${value.path}` : "Read permission denied",
|
|
3075
|
-
isError: true
|
|
3076
|
-
};
|
|
3077
|
-
if (variant === "writePermissionDenied") return {
|
|
3078
|
-
text: value?.error || (value?.path ? `Write permission denied: ${value.path}` : "Write permission denied"),
|
|
3079
|
-
isError: true
|
|
3080
|
-
};
|
|
3081
|
-
return {
|
|
3082
|
-
text: "Edit reported no result",
|
|
3083
|
-
isError: true
|
|
3084
|
-
};
|
|
3085
|
-
}
|
|
3086
|
-
function remapExecHandlerToolName(result, toolName) {
|
|
3087
|
-
if (isToolResultMessage(result)) return {
|
|
3088
|
-
...result,
|
|
3089
|
-
toolName
|
|
3090
|
-
};
|
|
3091
|
-
if (result && typeof result === "object" && "toolResult" in result) {
|
|
3092
|
-
const record = result;
|
|
3093
|
-
if (record.toolResult && record.result !== void 0) return {
|
|
3094
|
-
result: record.result,
|
|
3095
|
-
toolResult: {
|
|
3096
|
-
...record.toolResult,
|
|
3097
|
-
toolName
|
|
3098
|
-
}
|
|
3099
|
-
};
|
|
3100
|
-
if (record.toolResult) return {
|
|
3101
|
-
...record.toolResult,
|
|
3102
|
-
toolName
|
|
3103
|
-
};
|
|
3104
|
-
}
|
|
3105
|
-
return result;
|
|
3106
|
-
}
|
|
3107
|
-
/**
|
|
3108
|
-
* Open (or refresh) the single `edit` transcript block for a native StrReplace
|
|
3109
|
-
* `editToolCall`. Materialization reads/writes reuse this id and must not
|
|
3110
|
-
* synthesize their own blocks.
|
|
3111
|
-
*/
|
|
3112
|
-
function openOrUpdateEditBlock(output, stream, state, toolCall, envelopeId) {
|
|
3113
|
-
const edit = selectEditCall(toolCall);
|
|
3114
|
-
if (!edit && toolCall?.tool?.case !== "editToolCall") return void 0;
|
|
3115
|
-
rememberEditOwnedToolCall(state, toolCall, envelopeId);
|
|
3116
|
-
const id = toolCall?.toolCallId || envelopeId;
|
|
3117
|
-
if (!id) return void 0;
|
|
3118
|
-
const nextArgs = omitUndefinedArgs({
|
|
3119
|
-
path: edit?.args?.path,
|
|
3120
|
-
stream_content: edit?.args?.streamContent
|
|
3121
|
-
});
|
|
3122
|
-
const existing = output.content.find((block) => block.type === "toolCall" && block.id === id);
|
|
3123
|
-
if (existing) {
|
|
3124
|
-
existing.arguments = {
|
|
3125
|
-
...existing.arguments,
|
|
3126
|
-
...nextArgs
|
|
3127
|
-
};
|
|
3128
|
-
return existing;
|
|
3129
|
-
}
|
|
3130
|
-
endCurrentTextBlock(output, stream, state);
|
|
3131
|
-
endCurrentThinkingBlock(output, stream, state);
|
|
3132
|
-
const block = {
|
|
3133
|
-
type: "toolCall",
|
|
3134
|
-
id,
|
|
3135
|
-
name: "edit",
|
|
3136
|
-
arguments: nextArgs,
|
|
3137
|
-
[kStreamingBlockIndex]: output.content.length,
|
|
3138
|
-
[kStreamingBlockKind]: "cursor-edit",
|
|
3139
|
-
[kStreamingEnvelopeId]: envelopeId || void 0,
|
|
3140
|
-
[kCursorExecResolved]: true
|
|
3141
|
-
};
|
|
3142
|
-
output.content.push(block);
|
|
3143
|
-
retainStreamedCall(state, block, envelopeId);
|
|
3144
|
-
stream.push({
|
|
3145
|
-
type: "toolcall_start",
|
|
3146
|
-
contentIndex: output.content.length - 1,
|
|
3147
|
-
partial: output
|
|
3148
|
-
});
|
|
3149
|
-
return block;
|
|
3150
|
-
}
|
|
3151
|
-
/**
|
|
3152
|
-
* The streamed `ToolCall` variants whose block the exec channel owns.
|
|
3153
|
-
*
|
|
3154
|
-
* Each of these is announced on the interaction stream AND dispatched as its
|
|
3155
|
-
* own `ExecServerMessage` frame — the Pi family (45-51), plus the two MCP
|
|
3156
|
-
* resource frames — so the block is synthesized once, by the exec handler,
|
|
3157
|
-
* which is the side that has the result.
|
|
3158
|
-
*
|
|
3159
|
-
* `connect_scm` is deliberately NOT here: `ExecServerMessage` has no
|
|
3160
|
-
* connect-SCM case at all (field 44 is `git_diff_request`), so nothing on the
|
|
3161
|
-
* exec channel ever answers it and the streamed announcement is the only
|
|
3162
|
-
* signal. `search_conversations` is not here either: frame 53 answers it, but
|
|
3163
|
-
* it carries its own `tool_call_id` on the streamed envelope and pairs there,
|
|
3164
|
-
* so the exec branch does not synthesize a block for it.
|
|
3165
|
-
*/
|
|
3166
|
-
const EXEC_OWNED_TOOL_CALL_CASES = /* @__PURE__ */ new Set([
|
|
3167
|
-
"piReadToolCall",
|
|
3168
|
-
"piBashToolCall",
|
|
3169
|
-
"piEditToolCall",
|
|
3170
|
-
"piWriteToolCall",
|
|
3171
|
-
"piGrepToolCall",
|
|
3172
|
-
"piFindToolCall",
|
|
3173
|
-
"piLsToolCall",
|
|
3174
|
-
"listMcpResourcesToolCall",
|
|
3175
|
-
"readMcpResourceToolCall"
|
|
3176
|
-
]);
|
|
3177
|
-
function isExecOwnedToolCall(toolCall) {
|
|
3178
|
-
const variant = toolCall?.tool?.case;
|
|
3179
|
-
return variant !== void 0 && EXEC_OWNED_TOOL_CALL_CASES.has(variant);
|
|
3180
|
-
}
|
|
3181
|
-
/**
|
|
3182
|
-
* Retain a freshly opened streamed tool-call block.
|
|
3183
|
-
*
|
|
3184
|
-
* Keyed by the interaction envelope's `call_id`, which is the only key every
|
|
3185
|
-
* `ToolCall*Update` for that call shares. The block's own `id` is deliberately
|
|
3186
|
-
* not the key: MCP, Pi and connect-SCM blocks are filed under the id carried
|
|
3187
|
-
* inside the call's `args`, because that is what the exec channel pairs its
|
|
3188
|
-
* result under and what the transcript files the visible block under.
|
|
3189
|
-
*
|
|
3190
|
-
* `currentToolCall` is still set, as the fallback for frames that carry no
|
|
3191
|
-
* `call_id` (proto3-optional, and unset on what older builds send).
|
|
3192
|
-
*/
|
|
3193
|
-
/**
|
|
3194
|
-
* Close every tool-call block still open when the stream ends.
|
|
3195
|
-
*
|
|
3196
|
-
* Not just the last one started: with interleaved calls several can be open at
|
|
3197
|
-
* once, and an unclosed block leaves its live card animating and its call
|
|
3198
|
-
* unpaired.
|
|
3199
|
-
*
|
|
3200
|
-
* Only blocks fed by a streamed argument buffer get reparsed. Todo,
|
|
3201
|
-
* connect-SCM and MCP-settled frames arrive with complete `arguments` and
|
|
3202
|
-
* never set the partial buffer; `parseStreamingJson(undefined)` returns `{}`,
|
|
3203
|
-
* so reparsing unconditionally would erase the arguments of every such block
|
|
3204
|
-
* caught open by a truncated stream.
|
|
3205
|
-
*
|
|
3206
|
-
* Server-owned blocks are also paired here. `connect-scm` and `todo` are
|
|
3207
|
-
* stamped {@link kCursorExecResolved} the moment they open, so `agent-loop.ts`
|
|
3208
|
-
* synthesizes no placeholder for them and only their `toolCallCompleted` frame
|
|
3209
|
-
* pairs a result. A transport that closes before that frame would leave the
|
|
3210
|
-
* call unpaired, and `buildSessionContext` strips a dangling call from every
|
|
3211
|
-
* rebuilt transcript — the interaction disappears. An interrupted result is
|
|
3212
|
-
* emitted instead.
|
|
3213
|
-
*
|
|
3214
|
-
* MCP blocks are excluded even when resolved: the exec dispatch that marked
|
|
3215
|
-
* them owns their result, and `drainInFlightDispatches` awaits it before this
|
|
3216
|
-
* runs, so pairing here would duplicate one against the same `toolCallId`.
|
|
3217
|
-
*/
|
|
3218
|
-
function flushOpenToolCalls(output, stream, state) {
|
|
3219
|
-
const openBlocks = new Set(state.openToolCalls.values());
|
|
3220
|
-
if (state.currentToolCall) openBlocks.add(state.currentToolCall);
|
|
3221
|
-
for (const block of openBlocks) {
|
|
3222
|
-
const idx = output.content.indexOf(block);
|
|
3223
|
-
const partialJson = block[kStreamingPartialJson];
|
|
3224
|
-
if (partialJson !== void 0) {
|
|
3225
|
-
block.arguments = parseStreamingJson(partialJson);
|
|
3226
|
-
clearStreamingPartialJson(block);
|
|
3227
|
-
}
|
|
3228
|
-
const kind = block[kStreamingBlockKind];
|
|
3229
|
-
if (kind === "connect-scm" || kind === "todo" || kind === "cursor-edit") {
|
|
3230
|
-
if (!(kind === "cursor-edit" && state.pairedEditToolCallIds?.has(block.id))) state.onToolResult?.({
|
|
3231
|
-
role: "toolResult",
|
|
3232
|
-
toolCallId: block.id,
|
|
3233
|
-
toolName: block.name,
|
|
3234
|
-
content: [{
|
|
3235
|
-
type: "text",
|
|
3236
|
-
text: "The connection to Cursor closed before this call completed."
|
|
3237
|
-
}],
|
|
3238
|
-
isError: true,
|
|
3239
|
-
timestamp: Date.now()
|
|
3240
|
-
});
|
|
3241
|
-
}
|
|
3242
|
-
stream.push({
|
|
3243
|
-
type: "toolcall_end",
|
|
3244
|
-
contentIndex: idx,
|
|
3245
|
-
toolCall: block,
|
|
3246
|
-
partial: output
|
|
3247
|
-
});
|
|
3248
|
-
}
|
|
3249
|
-
state.openToolCalls.clear();
|
|
3250
|
-
state.setToolCall(null);
|
|
3251
|
-
}
|
|
3252
|
-
function retainStreamedCall(state, block, envelopeId) {
|
|
3253
|
-
if (envelopeId) state.openToolCalls.set(envelopeId, block);
|
|
3254
|
-
state.setToolCall(block);
|
|
3255
|
-
}
|
|
3256
|
-
/**
|
|
3257
|
-
* The open block a streamed update addresses, or `null` to ignore the update.
|
|
3258
|
-
*
|
|
3259
|
-
* Cursor interleaves calls: `start A, start B, complete A` is legal, so the
|
|
3260
|
-
* update must reach block A even though B opened last. An id naming no open
|
|
3261
|
-
* block is ignored rather than misapplied — settling the wrong block would pair
|
|
3262
|
-
* it with another call's result.
|
|
3263
|
-
*
|
|
3264
|
-
* A missing id falls back to the current block: the correlation key is
|
|
3265
|
-
* optional, and dropping those updates would strand a block stamped
|
|
3266
|
-
* {@link kCursorExecResolved}, which nothing else settles and whose whole
|
|
3267
|
-
* interaction is then stripped from every rebuilt transcript.
|
|
3268
|
-
*/
|
|
3269
|
-
function resolveStreamedCall(state, envelopeId) {
|
|
3270
|
-
if (!envelopeId) return state.currentToolCall;
|
|
3271
|
-
const keyed = state.openToolCalls.get(envelopeId);
|
|
3272
|
-
if (keyed) return keyed;
|
|
3273
|
-
const current = state.currentToolCall;
|
|
3274
|
-
return current && current[kStreamingEnvelopeId] === void 0 ? current : null;
|
|
3275
|
-
}
|
|
3276
|
-
/** Release a settled block from both the keyed map and the current slot. */
|
|
3277
|
-
function releaseStreamedCall(state, block) {
|
|
3278
|
-
const envelopeId = block[kStreamingEnvelopeId];
|
|
3279
|
-
if (envelopeId) state.openToolCalls.delete(envelopeId);
|
|
3280
|
-
if (state.currentToolCall === block) state.setToolCall(null);
|
|
3281
|
-
}
|
|
3282
|
-
/**
|
|
3283
|
-
* The streamed `connect_scm_tool_call` variant, if this update carries one.
|
|
3284
|
-
*
|
|
3285
|
-
* Same oneof-vs-flattened handling as {@link selectMcpCall}: a wire-decoded
|
|
3286
|
-
* `ToolCall` exposes its variant as `{ case, value }`, while hand-shaped test
|
|
3287
|
-
* fixtures use the flat property.
|
|
3288
|
-
*/
|
|
3289
|
-
function selectConnectScmCall(toolCall) {
|
|
3290
|
-
const oneof = toolCall?.tool;
|
|
3291
|
-
if (oneof?.case === "connectScmToolCall") return oneof.value;
|
|
3292
|
-
return toolCall?.connectScmToolCall;
|
|
3293
|
-
}
|
|
3294
|
-
/** The repository a connect-SCM call targets, across the `target` oneof. */
|
|
3295
|
-
function selectConnectScmRepository(call) {
|
|
3296
|
-
const target = call?.args?.target;
|
|
3297
|
-
if (target?.case === "github") return target.value?.repository;
|
|
3298
|
-
return call?.args?.github?.repository;
|
|
3299
|
-
}
|
|
3300
|
-
/**
|
|
3301
|
-
* Render a settled `ConnectScmResult` as the text of its paired tool result.
|
|
3302
|
-
*
|
|
3303
|
-
* Returns `isError` because the three outcomes are not interchangeable: only
|
|
3304
|
-
* `success` means the repository was connected, and reporting a rejection as
|
|
3305
|
-
* success would tell the model to proceed against a repo it cannot reach.
|
|
3306
|
-
*/
|
|
3307
|
-
function describeConnectScmResult(call) {
|
|
3308
|
-
const result = call?.result?.result;
|
|
3309
|
-
switch (result?.case) {
|
|
3310
|
-
case "success": return {
|
|
3311
|
-
text: "SCM connected",
|
|
3312
|
-
isError: false
|
|
3313
|
-
};
|
|
3314
|
-
case "error": return {
|
|
3315
|
-
text: result.value?.error || "SCM connection failed",
|
|
3316
|
-
isError: true
|
|
3317
|
-
};
|
|
3318
|
-
case "rejected": return {
|
|
3319
|
-
text: result.value?.reason || "SCM connection rejected",
|
|
3320
|
-
isError: true
|
|
3321
|
-
};
|
|
3322
|
-
default: return {
|
|
3323
|
-
text: "SCM connection reported no result",
|
|
3324
|
-
isError: true
|
|
3325
|
-
};
|
|
3326
|
-
}
|
|
3327
|
-
}
|
|
3328
|
-
/**
|
|
3329
|
-
* Extract the authoritative full todo list from a completed native todo call.
|
|
3330
|
-
*
|
|
3331
|
-
* Cursor owns this list server-side: `update_todos` / `read_todos` are resolved
|
|
3332
|
-
* remotely and the settled state rides on the tool call's `result`, never on
|
|
3333
|
-
* the exec channel (`ExecServerMessage` has no todo case). Only
|
|
3334
|
-
* `result.success.todos` is authoritative — the request `args` may differ from
|
|
3335
|
-
* what the server actually stored after a merge or normalization, and on
|
|
3336
|
-
* `UpdateTodosError` nothing was stored at all.
|
|
3337
|
-
*
|
|
3338
|
-
* A `read_todos` call carrying `status_filter` / `id_filter` (agent.proto
|
|
3339
|
-
* `ReadTodosArgs`) returns a SUBSET, not the list, and its `total_count`
|
|
3340
|
-
* reports the full size. Mirroring a partial response would delete every task
|
|
3341
|
-
* it omitted, so filtered and short reads are refused here. An empty read is
|
|
3342
|
-
* refused too: proto3 defaults unset `total_count` to 0, so `todos=[]` cannot
|
|
3343
|
-
* be told from a missing count.
|
|
3344
|
-
*
|
|
3345
|
-
* A snapshot whose rows are not unique by content is refused for a different
|
|
3346
|
-
* reason: Cursor keys todos by `id`, the local list is keyed by content, and
|
|
3347
|
-
* the collision is unrepresentable rather than merely partial.
|
|
3348
|
-
*
|
|
3349
|
-
* Returns `null` when no usable full snapshot is available, which the caller
|
|
3350
|
-
* MUST treat as "leave local state untouched".
|
|
3351
|
-
*/
|
|
3352
|
-
function extractTodoSnapshot(toolCall) {
|
|
3353
|
-
const { update, read } = selectTodoCalls(toolCall);
|
|
3354
|
-
if (read && ((read.args?.statusFilter?.length ?? 0) > 0 || (read.args?.idFilter?.length ?? 0) > 0)) return null;
|
|
3355
|
-
const call = update ?? read;
|
|
3356
|
-
if (!call) return null;
|
|
3357
|
-
const result = call.result?.result;
|
|
3358
|
-
if (result?.case !== "success") return null;
|
|
3359
|
-
const todos = result.value?.todos;
|
|
3360
|
-
if (!todos) return null;
|
|
3361
|
-
const totalCount = result.value?.totalCount;
|
|
3362
|
-
if (typeof totalCount === "number" && totalCount !== todos.length) return null;
|
|
3363
|
-
if (read && todos.length === 0) return null;
|
|
3364
|
-
const mapped = mapTodoSnapshot(todos);
|
|
3365
|
-
if (mapped.some((todo) => todo.content.length === 0)) return null;
|
|
3366
|
-
const seen = /* @__PURE__ */ new Set();
|
|
3367
|
-
for (const todo of mapped) {
|
|
3368
|
-
if (seen.has(todo.content)) return null;
|
|
3369
|
-
seen.add(todo.content);
|
|
3370
|
-
}
|
|
3371
|
-
const finished = /* @__PURE__ */ new Set();
|
|
3372
|
-
for (const todo of todos) {
|
|
3373
|
-
const status = mapTodoStatusValue(typeof todo.status === "number" ? todo.status : void 0);
|
|
3374
|
-
if (todo.id && (status === "completed" || status === "abandoned")) finished.add(todo.id);
|
|
3375
|
-
}
|
|
3376
|
-
for (const todo of todos) for (const dependency of todo.dependencies ?? []) if (!finished.has(dependency)) return null;
|
|
3377
|
-
return {
|
|
3378
|
-
todos: mapped,
|
|
3379
|
-
merged: result.value?.wasMerge === true
|
|
3380
|
-
};
|
|
3381
|
-
}
|
|
3382
|
-
/**
|
|
3383
|
-
* Error text when the server itself rejected the call.
|
|
3384
|
-
*
|
|
3385
|
-
* Distinct from {@link extractTodoSnapshot} returning `null`: a filtered read, a
|
|
3386
|
-
* truncated or empty one (proto3 cannot tell unset `total_count` from zero), or
|
|
3387
|
-
* a snapshot the local model cannot represent are all benign refusals (the call
|
|
3388
|
-
* succeeded, we just decline to mirror it), whereas an `UpdateTodosError` /
|
|
3389
|
-
* `ReadTodosError` is a real failure that must not replay as a successful no-op.
|
|
3390
|
-
*/
|
|
3391
|
-
function extractTodoError(toolCall) {
|
|
3392
|
-
const { update, read } = selectTodoCalls(toolCall);
|
|
3393
|
-
const result = (update ?? read)?.result?.result;
|
|
3394
|
-
if (result?.case !== "error") return null;
|
|
3395
|
-
const error = result.value?.error;
|
|
3396
|
-
return typeof error === "string" && error.length > 0 ? error : "Todo operation failed";
|
|
3397
|
-
}
|
|
3398
|
-
/** Args echoed onto the synthesized display block, for rendering only. */
|
|
3399
|
-
function buildTodoDisplayArgs(toolCall) {
|
|
3400
|
-
const args = selectTodoCalls(toolCall).update?.args;
|
|
3401
|
-
return {
|
|
3402
|
-
todos: args?.todos ? mapTodoSnapshot(args.todos) : [],
|
|
3403
|
-
merge: args?.merge === true ? true : void 0
|
|
3404
|
-
};
|
|
3405
|
-
}
|
|
3406
|
-
/**
|
|
3407
|
-
* Paired result for a server-resolved native todo call.
|
|
3408
|
-
*
|
|
3409
|
-
* The bridge never runs a local `todo` tool for these, so nothing else would
|
|
3410
|
-
* produce a `toolResult` for the block — and `buildSessionContext` strips any
|
|
3411
|
-
* `toolCall` left unpaired, taking the interaction out of every rebuilt
|
|
3412
|
-
* transcript.
|
|
3413
|
-
*
|
|
3414
|
-
* Three outcomes, kept distinct: a server error replays as a failure, a benign
|
|
3415
|
-
* refusal (a filtered, truncated, or empty read, or a snapshot the local model
|
|
3416
|
-
* cannot represent) replays as `"Todo snapshot not mirrored"`, and a settled
|
|
3417
|
-
* snapshot replays as its summary. Collapsing the first into the second would
|
|
3418
|
-
* hide the failure and let downstream lifecycle logic treat it as success. The
|
|
3419
|
-
* refusal text must not say `"No todo changes"`: an `update_todos` the server
|
|
3420
|
-
* accepted may still be declined locally, and that is not "no changes".
|
|
3421
|
-
*/
|
|
3422
|
-
function buildTodoToolResult(toolCallId, snapshot, error) {
|
|
3423
|
-
return {
|
|
3424
|
-
role: "toolResult",
|
|
3425
|
-
toolCallId,
|
|
3426
|
-
toolName: "todo",
|
|
3427
|
-
content: [{
|
|
3428
|
-
type: "text",
|
|
3429
|
-
text: error ?? (snapshot ? formatTodoSnapshotSummary(snapshot.todos) : "Todo snapshot not mirrored")
|
|
3430
|
-
}],
|
|
3431
|
-
isError: error !== null,
|
|
3432
|
-
timestamp: Date.now()
|
|
3433
|
-
};
|
|
3434
|
-
}
|
|
3435
|
-
function formatTodoSnapshotSummary(todos) {
|
|
3436
|
-
if (todos.length === 0) return "No todos";
|
|
3437
|
-
return `${todos.filter((todo) => todo.status === "completed").length}/${todos.length} tasks completed`;
|
|
3438
|
-
}
|
|
3439
|
-
function buildMcpResultFromToolResult(_mcpCall, toolResult) {
|
|
3440
|
-
if (toolResult.isError) return buildMcpErrorResult(toolResultToText(toolResult) || "MCP tool failed");
|
|
3441
|
-
const content = toolResult.content.map((item) => {
|
|
3442
|
-
if (item.type === "image") return create(McpToolResultContentItemSchema, { content: {
|
|
3443
|
-
case: "image",
|
|
3444
|
-
value: create(McpImageContentSchema, {
|
|
3445
|
-
data: Uint8Array.from(Buffer.from(item.data, "base64")),
|
|
3446
|
-
mimeType: item.mimeType
|
|
3447
|
-
})
|
|
3448
|
-
} });
|
|
3449
|
-
return create(McpToolResultContentItemSchema, { content: {
|
|
3450
|
-
case: "text",
|
|
3451
|
-
value: create(McpTextContentSchema, { text: item.text })
|
|
3452
|
-
} });
|
|
3453
|
-
});
|
|
3454
|
-
return create(McpResultSchema, { result: {
|
|
3455
|
-
case: "success",
|
|
3456
|
-
value: create(McpSuccessSchema, {
|
|
3457
|
-
content,
|
|
3458
|
-
isError: false
|
|
3459
|
-
})
|
|
3460
|
-
} });
|
|
3461
|
-
}
|
|
3462
|
-
const MCP_EXTERNAL_HANDOFF_MESSAGE = cursor_external_tool_handoff_default.trim();
|
|
3463
|
-
function buildMcpExternalHandoffResult() {
|
|
3464
|
-
return create(McpResultSchema, { result: {
|
|
3465
|
-
case: "success",
|
|
3466
|
-
value: create(McpSuccessSchema, {
|
|
3467
|
-
content: [create(McpToolResultContentItemSchema, { content: {
|
|
3468
|
-
case: "text",
|
|
3469
|
-
value: create(McpTextContentSchema, { text: MCP_EXTERNAL_HANDOFF_MESSAGE })
|
|
3470
|
-
} })],
|
|
3471
|
-
isError: false
|
|
3472
|
-
})
|
|
3473
|
-
} });
|
|
3474
|
-
}
|
|
3475
|
-
function buildMcpToolNotFoundResult(mcpCall) {
|
|
3476
|
-
return create(McpResultSchema, { result: {
|
|
3477
|
-
case: "toolNotFound",
|
|
3478
|
-
value: create(McpToolNotFoundSchema, {
|
|
3479
|
-
name: mcpCall.toolName,
|
|
3480
|
-
availableTools: []
|
|
3481
|
-
})
|
|
3482
|
-
} });
|
|
3483
|
-
}
|
|
3484
|
-
function buildMcpErrorResult(error) {
|
|
3485
|
-
return create(McpResultSchema, { result: {
|
|
3486
|
-
case: "error",
|
|
3487
|
-
value: create(McpErrorSchema, { error })
|
|
3488
|
-
} });
|
|
3489
|
-
}
|
|
3490
|
-
/**
|
|
3491
|
-
* Merge the decoded completion-frame `McpArgs` map into the args assembled
|
|
3492
|
-
* from streamed `args_text_delta` snapshots.
|
|
3493
|
-
*
|
|
3494
|
-
* The completion frame is authoritative for the scalars it carries — but it
|
|
3495
|
-
* can omit oversized parameters entirely and can downgrade a structured value
|
|
3496
|
-
* to its raw string fallback when `decodeMcpArgValue` cannot parse it as
|
|
3497
|
-
* JSON. Overwriting the streamed args wholesale therefore loses data (e.g.
|
|
3498
|
-
* the task tool's `tasks` array on multi-subagent dispatches, issue #2615).
|
|
3499
|
-
*
|
|
3500
|
-
* Rules per key:
|
|
3501
|
-
* - completion key absent → keep the streamed value.
|
|
3502
|
-
* - completion is a string while the streamed value is structured (object or
|
|
3503
|
-
* array) → keep the streamed value (the completion frame downgraded it).
|
|
3504
|
-
* - otherwise → completion wins.
|
|
3505
|
-
*/
|
|
3506
|
-
function mergeCursorMcpToolCallArgs(streamed, completion) {
|
|
3507
|
-
const merged = { ...streamed };
|
|
3508
|
-
if (!completion) return merged;
|
|
3509
|
-
for (const [key, completionValue] of Object.entries(completion)) {
|
|
3510
|
-
const streamedValue = merged[key];
|
|
3511
|
-
if (typeof completionValue === "string" && streamedValue !== null && typeof streamedValue === "object") continue;
|
|
3512
|
-
merged[key] = completionValue;
|
|
3513
|
-
}
|
|
3514
|
-
return merged;
|
|
3515
|
-
}
|
|
3516
|
-
function endCurrentTextBlock(output, stream, state) {
|
|
3517
|
-
const block = state.currentTextBlock;
|
|
3518
|
-
if (!block) return;
|
|
3519
|
-
const idx = output.content.indexOf(block);
|
|
3520
|
-
stream.push({
|
|
3521
|
-
type: "text_end",
|
|
3522
|
-
contentIndex: idx,
|
|
3523
|
-
content: block.text,
|
|
3524
|
-
partial: output
|
|
3525
|
-
});
|
|
3526
|
-
state.setTextBlock(null);
|
|
3527
|
-
}
|
|
3528
|
-
function endCurrentThinkingBlock(output, stream, state) {
|
|
3529
|
-
const block = state.currentThinkingBlock;
|
|
3530
|
-
if (!block) return;
|
|
3531
|
-
const idx = output.content.indexOf(block);
|
|
3532
|
-
stream.push({
|
|
3533
|
-
type: "thinking_end",
|
|
3534
|
-
contentIndex: idx,
|
|
3535
|
-
content: block.thinking,
|
|
3536
|
-
partial: output
|
|
3537
|
-
});
|
|
3538
|
-
state.setThinkingBlock(null);
|
|
3539
|
-
}
|
|
3540
|
-
/**
|
|
3541
|
-
* Synthesize a completed `toolCall` content block for a Cursor exec-channel
|
|
3542
|
-
* native tool (`shell`, `read`, `write`, `grep`, `ls`, `delete`, `diagnostics`)
|
|
3543
|
-
* or for an MCP exec frame whose corresponding interaction block is absent.
|
|
3544
|
-
*
|
|
3545
|
-
* Args arrive complete on the exec message, so the block opens and closes in
|
|
3546
|
-
* one step — no partial-JSON streaming path. Without this the persisted
|
|
3547
|
-
* assistant message carries only text/thinking blocks, and on replay the
|
|
3548
|
-
* following `toolResult` messages have no matching `toolCall.id` in
|
|
3549
|
-
* `renderSessionContext`, so they render beneath the final answer or disappear.
|
|
3550
|
-
*
|
|
3551
|
-
* The block is stamped with {@link kCursorExecResolved} so the shared
|
|
3552
|
-
* `agent-loop.ts` execution pass skips it — Cursor's server-driven exec
|
|
3553
|
-
* channel already ran the tool via the bridge and buffered the result, so
|
|
3554
|
-
* treating this block as runnable would re-execute the same side-effecting
|
|
3555
|
-
* tool a second time.
|
|
3556
|
-
*
|
|
3557
|
-
* Exported for tests to exercise ordering with adjacent text/thinking blocks.
|
|
3558
|
-
*/
|
|
3559
|
-
function synthesizeCursorExecToolCall(output, stream, state, toolCallId, toolName, args) {
|
|
3560
|
-
endCurrentTextBlock(output, stream, state);
|
|
3561
|
-
endCurrentThinkingBlock(output, stream, state);
|
|
3562
|
-
const block = {
|
|
3563
|
-
type: "toolCall",
|
|
3564
|
-
id: toolCallId,
|
|
3565
|
-
name: toolName,
|
|
3566
|
-
arguments: omitUndefinedArgs(args),
|
|
3567
|
-
[kStreamingBlockIndex]: output.content.length,
|
|
3568
|
-
[kStreamingBlockKind]: "cursor-exec",
|
|
3569
|
-
[kCursorExecResolved]: true
|
|
3570
|
-
};
|
|
3571
|
-
output.content.push(block);
|
|
3572
|
-
const idx = output.content.length - 1;
|
|
3573
|
-
stream.push({
|
|
3574
|
-
type: "toolcall_start",
|
|
3575
|
-
contentIndex: idx,
|
|
3576
|
-
partial: output
|
|
3577
|
-
});
|
|
3578
|
-
stream.push({
|
|
3579
|
-
type: "toolcall_end",
|
|
3580
|
-
contentIndex: idx,
|
|
3581
|
-
toolCall: block,
|
|
3582
|
-
partial: output
|
|
3583
|
-
});
|
|
3584
|
-
}
|
|
3585
|
-
/**
|
|
3586
|
-
* Pair a `toolResult` for a synthesized block the client answered itself,
|
|
3587
|
-
* without ever consulting a handler.
|
|
3588
|
-
*
|
|
3589
|
-
* {@link resolveExecHandler} does this for every frame backed by a local tool.
|
|
3590
|
-
* Frames answered from a fixed verdict — no handler, no local execution — still
|
|
3591
|
-
* need the pair for the same reason: the block was stamped
|
|
3592
|
-
* {@link kCursorExecResolved}, so `agent-loop.ts` emits no placeholder for it
|
|
3593
|
-
* and `buildSessionContext` strips an unpaired call, taking the whole
|
|
3594
|
-
* interaction out of every rebuilt transcript.
|
|
3595
|
-
*
|
|
3596
|
-
* `isError` defaults true because most such verdicts are refusals; the MCP
|
|
3597
|
-
* resource frames run locally and can genuinely succeed, and a success filed
|
|
3598
|
-
* as an error would render as a failed call in every rebuilt transcript.
|
|
3599
|
-
*/
|
|
3600
|
-
async function pairSynthesizedExecResult(state, onToolResult, toolCallId, toolName, text, isError = true) {
|
|
3601
|
-
const synthesized = {
|
|
3602
|
-
role: "toolResult",
|
|
3603
|
-
toolCallId,
|
|
3604
|
-
toolName,
|
|
3605
|
-
content: [{
|
|
3606
|
-
type: "text",
|
|
3607
|
-
text
|
|
3608
|
-
}],
|
|
3609
|
-
isError,
|
|
3610
|
-
timestamp: Date.now()
|
|
3611
|
-
};
|
|
3612
|
-
const sink = onToolResult ?? state.onToolResult;
|
|
3613
|
-
if (!sink) return;
|
|
3614
|
-
await sink(synthesized);
|
|
3615
|
-
}
|
|
3616
|
-
/** Exported for tests: drives one Cursor interaction update through the streaming state machine. */
|
|
3617
|
-
function processInteractionUpdate(update, output, stream, state, usageState) {
|
|
3618
|
-
const updateCase = update.message?.case;
|
|
3619
|
-
log("interactionUpdate", updateCase, update.message?.value);
|
|
3620
|
-
if (updateCase === "textDelta") {
|
|
3621
|
-
state.setFirstTokenTime();
|
|
3622
|
-
const delta = update.message.value.text || "";
|
|
3623
|
-
if (!state.currentTextBlock) {
|
|
3624
|
-
const block = {
|
|
3625
|
-
type: "text",
|
|
3626
|
-
text: "",
|
|
3627
|
-
[kStreamingBlockIndex]: output.content.length
|
|
3628
|
-
};
|
|
3629
|
-
output.content.push(block);
|
|
3630
|
-
state.setTextBlock(block);
|
|
3631
|
-
stream.push({
|
|
3632
|
-
type: "text_start",
|
|
3633
|
-
contentIndex: output.content.length - 1,
|
|
3634
|
-
partial: output
|
|
3635
|
-
});
|
|
3636
|
-
}
|
|
3637
|
-
state.currentTextBlock.text += delta;
|
|
3638
|
-
const idx = output.content.indexOf(state.currentTextBlock);
|
|
3639
|
-
stream.push({
|
|
3640
|
-
type: "text_delta",
|
|
3641
|
-
contentIndex: idx,
|
|
3642
|
-
delta,
|
|
3643
|
-
partial: output
|
|
3644
|
-
});
|
|
3645
|
-
} else if (updateCase === "thinkingDelta") {
|
|
3646
|
-
state.setFirstTokenTime();
|
|
3647
|
-
const delta = update.message.value.text || "";
|
|
3648
|
-
if (!state.currentThinkingBlock) {
|
|
3649
|
-
const block = {
|
|
3650
|
-
type: "thinking",
|
|
3651
|
-
thinking: "",
|
|
3652
|
-
[kStreamingBlockIndex]: output.content.length
|
|
3653
|
-
};
|
|
3654
|
-
output.content.push(block);
|
|
3655
|
-
state.setThinkingBlock(block);
|
|
3656
|
-
stream.push({
|
|
3657
|
-
type: "thinking_start",
|
|
3658
|
-
contentIndex: output.content.length - 1,
|
|
3659
|
-
partial: output
|
|
3660
|
-
});
|
|
3661
|
-
}
|
|
3662
|
-
state.currentThinkingBlock.thinking += delta;
|
|
3663
|
-
const idx = output.content.indexOf(state.currentThinkingBlock);
|
|
3664
|
-
stream.push({
|
|
3665
|
-
type: "thinking_delta",
|
|
3666
|
-
contentIndex: idx,
|
|
3667
|
-
delta,
|
|
3668
|
-
partial: output
|
|
3669
|
-
});
|
|
3670
|
-
} else if (updateCase === "thinkingCompleted") endCurrentThinkingBlock(output, stream, state);
|
|
3671
|
-
else if (updateCase === "toolCallStarted" && selectConnectScmCall(update.message.value.toolCall)) {
|
|
3672
|
-
endCurrentTextBlock(output, stream, state);
|
|
3673
|
-
endCurrentThinkingBlock(output, stream, state);
|
|
3674
|
-
const scmCall = selectConnectScmCall(update.message.value.toolCall);
|
|
3675
|
-
const repository = selectConnectScmRepository(scmCall);
|
|
3676
|
-
const block = {
|
|
3677
|
-
type: "toolCall",
|
|
3678
|
-
id: scmCall?.args?.toolCallId || update.message.value.callId || crypto.randomUUID(),
|
|
3679
|
-
name: "connect_scm",
|
|
3680
|
-
arguments: repository ? {
|
|
3681
|
-
owner: repository.owner,
|
|
3682
|
-
repo: repository.repo
|
|
3683
|
-
} : {},
|
|
3684
|
-
[kStreamingBlockIndex]: output.content.length,
|
|
3685
|
-
[kStreamingBlockKind]: "connect-scm",
|
|
3686
|
-
[kStreamingEnvelopeId]: update.message.value.callId || void 0,
|
|
3687
|
-
[kCursorExecResolved]: true
|
|
3688
|
-
};
|
|
3689
|
-
output.content.push(block);
|
|
3690
|
-
retainStreamedCall(state, block, update.message.value.callId);
|
|
3691
|
-
stream.push({
|
|
3692
|
-
type: "toolcall_start",
|
|
3693
|
-
contentIndex: output.content.length - 1,
|
|
3694
|
-
partial: output
|
|
3695
|
-
});
|
|
3696
|
-
} else if (updateCase === "toolCallStarted" && isExecOwnedToolCall(update.message.value.toolCall)) {
|
|
3697
|
-
endCurrentTextBlock(output, stream, state);
|
|
3698
|
-
endCurrentThinkingBlock(output, stream, state);
|
|
3699
|
-
log("exec", "streamedToolCallOwnedByExec", { case: update.message.value.toolCall?.tool?.case });
|
|
3700
|
-
} else if (updateCase === "toolCallStarted") {
|
|
3701
|
-
endCurrentTextBlock(output, stream, state);
|
|
3702
|
-
endCurrentThinkingBlock(output, stream, state);
|
|
3703
|
-
const toolCall = update.message.value.toolCall;
|
|
3704
|
-
if (toolCall) {
|
|
3705
|
-
const mcpCall = selectMcpCall(toolCall);
|
|
3706
|
-
if (mcpCall) {
|
|
3707
|
-
const args = mcpCall.args || {};
|
|
3708
|
-
const id = args.toolCallId || crypto.randomUUID();
|
|
3709
|
-
const resolvedByExec = state.resolvedMcpToolCallIds.delete(id);
|
|
3710
|
-
if (resolvedByExec && output.content.some((block) => block.type === "toolCall" && block.id === id)) return;
|
|
3711
|
-
const block = {
|
|
3712
|
-
type: "toolCall",
|
|
3713
|
-
id,
|
|
3714
|
-
name: args.toolName || args.name || "",
|
|
3715
|
-
arguments: decodeMcpArgsMap(args.args) ?? {},
|
|
3716
|
-
[kStreamingBlockIndex]: output.content.length,
|
|
3717
|
-
[kStreamingBlockKind]: "mcp",
|
|
3718
|
-
[kStreamingEnvelopeId]: update.message.value.callId || void 0
|
|
3719
|
-
};
|
|
3720
|
-
if (resolvedByExec) markCursorExecResolved(block);
|
|
3721
|
-
output.content.push(block);
|
|
3722
|
-
retainStreamedCall(state, block, update.message.value.callId);
|
|
3723
|
-
stream.push({
|
|
3724
|
-
type: "toolcall_start",
|
|
3725
|
-
contentIndex: output.content.length - 1,
|
|
3726
|
-
partial: output
|
|
3727
|
-
});
|
|
3728
|
-
return;
|
|
3729
|
-
}
|
|
3730
|
-
const todoCalls = selectTodoCalls(toolCall);
|
|
3731
|
-
if (todoCalls.update || todoCalls.read) {
|
|
3732
|
-
const block = {
|
|
3733
|
-
type: "toolCall",
|
|
3734
|
-
id: update.message.value.callId || crypto.randomUUID(),
|
|
3735
|
-
name: "todo",
|
|
3736
|
-
arguments: buildTodoDisplayArgs(toolCall),
|
|
3737
|
-
[kStreamingBlockIndex]: output.content.length,
|
|
3738
|
-
[kStreamingBlockKind]: "todo",
|
|
3739
|
-
[kStreamingEnvelopeId]: update.message.value.callId || void 0,
|
|
3740
|
-
[kCursorExecResolved]: true
|
|
3741
|
-
};
|
|
3742
|
-
output.content.push(block);
|
|
3743
|
-
retainStreamedCall(state, block, update.message.value.callId);
|
|
3744
|
-
stream.push({
|
|
3745
|
-
type: "toolcall_start",
|
|
3746
|
-
contentIndex: output.content.length - 1,
|
|
3747
|
-
partial: output
|
|
3748
|
-
});
|
|
3749
|
-
return;
|
|
3750
|
-
}
|
|
3751
|
-
const fetchCall = selectHostedFetchCall(toolCall);
|
|
3752
|
-
if (fetchCall || hostedFetchUnknown(toolCall)) {
|
|
3753
|
-
const url = fetchCall?.args?.url || extractHttpUrlFromUnknown(toolCall);
|
|
3754
|
-
const block = {
|
|
3755
|
-
type: "toolCall",
|
|
3756
|
-
id: fetchCall?.args?.toolCallId || update.message.value.callId || crypto.randomUUID(),
|
|
3757
|
-
name: "web_fetch",
|
|
3758
|
-
arguments: url ? { url } : {},
|
|
3759
|
-
[kStreamingBlockIndex]: output.content.length,
|
|
3760
|
-
[kStreamingBlockKind]: "web-fetch",
|
|
3761
|
-
[kStreamingEnvelopeId]: update.message.value.callId || void 0,
|
|
3762
|
-
[kCursorExecResolved]: true
|
|
3763
|
-
};
|
|
3764
|
-
output.content.push(block);
|
|
3765
|
-
retainStreamedCall(state, block, update.message.value.callId);
|
|
3766
|
-
stream.push({
|
|
3767
|
-
type: "toolcall_start",
|
|
3768
|
-
contentIndex: output.content.length - 1,
|
|
3769
|
-
partial: output
|
|
3770
|
-
});
|
|
3771
|
-
return;
|
|
3772
|
-
}
|
|
3773
|
-
openOrUpdateEditBlock(output, stream, state, toolCall, update.message.value.callId);
|
|
3774
|
-
}
|
|
3775
|
-
} else if (updateCase === "toolCallDelta" || updateCase === "partialToolCall") {
|
|
3776
|
-
const value = update.message.value;
|
|
3777
|
-
if (updateCase === "partialToolCall" && value.toolCall) openOrUpdateEditBlock(output, stream, state, value.toolCall, value.callId);
|
|
3778
|
-
const editDelta = selectEditStreamDelta(value);
|
|
3779
|
-
if (editDelta) {
|
|
3780
|
-
const target = resolveStreamedCall(state, value.callId);
|
|
3781
|
-
if (target?.[kStreamingBlockKind] === "cursor-edit") {
|
|
3782
|
-
const current = stringToolArg(target.arguments, "stream_content") ?? "";
|
|
3783
|
-
target.arguments = {
|
|
3784
|
-
...target.arguments,
|
|
3785
|
-
stream_content: current + editDelta
|
|
3786
|
-
};
|
|
3787
|
-
const idx = output.content.indexOf(target);
|
|
3788
|
-
stream.push({
|
|
3789
|
-
type: "toolcall_delta",
|
|
3790
|
-
contentIndex: idx,
|
|
3791
|
-
delta: editDelta,
|
|
3792
|
-
partial: output
|
|
3793
|
-
});
|
|
3794
|
-
return;
|
|
3795
|
-
}
|
|
3796
|
-
}
|
|
3797
|
-
const target = resolveStreamedCall(state, value.callId);
|
|
3798
|
-
if (target?.[kStreamingBlockKind] === "mcp") {
|
|
3799
|
-
const snapshot = update.message.value.argsTextDelta || "";
|
|
3800
|
-
const current = target[kStreamingPartialJson] ?? "";
|
|
3801
|
-
const chunk = snapshot.startsWith(current) ? snapshot.slice(current.length) : snapshot;
|
|
3802
|
-
if (chunk.length === 0) return;
|
|
3803
|
-
const nextBuffer = current + chunk;
|
|
3804
|
-
target[kStreamingPartialJson] = nextBuffer;
|
|
3805
|
-
const throttled = parseStreamingJsonThrottled(nextBuffer, target[kStreamingLastParseLen] ?? 0);
|
|
3806
|
-
if (throttled) {
|
|
3807
|
-
target.arguments = throttled.value;
|
|
3808
|
-
target[kStreamingLastParseLen] = throttled.parsedLen;
|
|
3809
|
-
}
|
|
3810
|
-
const idx = output.content.indexOf(target);
|
|
3811
|
-
stream.push({
|
|
3812
|
-
type: "toolcall_delta",
|
|
3813
|
-
contentIndex: idx,
|
|
3814
|
-
delta: chunk,
|
|
3815
|
-
partial: output
|
|
3816
|
-
});
|
|
3817
|
-
}
|
|
3818
|
-
} else if (updateCase === "toolCallCompleted") {
|
|
3819
|
-
const settled = resolveStreamedCall(state, update.message.value.callId);
|
|
3820
|
-
if (settled) {
|
|
3821
|
-
const toolCall = update.message.value.toolCall;
|
|
3822
|
-
if (settled[kStreamingBlockKind] === "mcp") {
|
|
3823
|
-
const partial = settled[kStreamingPartialJson];
|
|
3824
|
-
if (partial) settled.arguments = parseStreamingJson(partial);
|
|
3825
|
-
const decodedArgs = decodeMcpArgsMap(selectMcpCall(toolCall)?.args?.args);
|
|
3826
|
-
settled.arguments = mergeCursorMcpToolCallArgs(settled.arguments, decodedArgs);
|
|
3827
|
-
} else if (settled[kStreamingBlockKind] === "connect-scm") {
|
|
3828
|
-
const scmCall = selectConnectScmCall(toolCall);
|
|
3829
|
-
const repository = selectConnectScmRepository(scmCall);
|
|
3830
|
-
if (repository) settled.arguments = {
|
|
3831
|
-
owner: repository.owner,
|
|
3832
|
-
repo: repository.repo
|
|
3833
|
-
};
|
|
3834
|
-
const { text, isError } = describeConnectScmResult(scmCall);
|
|
3835
|
-
state.onToolResult?.({
|
|
3836
|
-
role: "toolResult",
|
|
3837
|
-
toolCallId: settled.id,
|
|
3838
|
-
toolName: "connect_scm",
|
|
3839
|
-
content: [{
|
|
3840
|
-
type: "text",
|
|
3841
|
-
text
|
|
3842
|
-
}],
|
|
3843
|
-
isError,
|
|
3844
|
-
timestamp: Date.now()
|
|
3845
|
-
});
|
|
3846
|
-
} else if (settled[kStreamingBlockKind] === "web-fetch") {
|
|
3847
|
-
const fetchCall = selectHostedFetchCall(toolCall);
|
|
3848
|
-
const url = fetchCall?.args?.url || extractHttpUrlFromUnknown(toolCall ?? {});
|
|
3849
|
-
if (url) settled.arguments = { url };
|
|
3850
|
-
const { text, isError } = describeHostedFetchResult(fetchCall);
|
|
3851
|
-
state.onToolResult?.({
|
|
3852
|
-
role: "toolResult",
|
|
3853
|
-
toolCallId: settled.id,
|
|
3854
|
-
toolName: "web_fetch",
|
|
3855
|
-
content: [{
|
|
3856
|
-
type: "text",
|
|
3857
|
-
text
|
|
3858
|
-
}],
|
|
3859
|
-
isError,
|
|
3860
|
-
timestamp: Date.now()
|
|
3861
|
-
});
|
|
3862
|
-
} else if (settled[kStreamingBlockKind] === "todo") {
|
|
3863
|
-
const snapshot = toolCall ? extractTodoSnapshot(toolCall) : null;
|
|
3864
|
-
const error = toolCall ? extractTodoError(toolCall) : null;
|
|
3865
|
-
if (snapshot) settled.arguments = {
|
|
3866
|
-
todos: snapshot.todos,
|
|
3867
|
-
merged: snapshot.merged
|
|
3868
|
-
};
|
|
3869
|
-
let persisted;
|
|
3870
|
-
let hostError = null;
|
|
3871
|
-
try {
|
|
3872
|
-
persisted = state.onTodoSnapshot?.(snapshot, settled.id, error) ?? void 0;
|
|
3873
|
-
} catch (callbackError) {
|
|
3874
|
-
hostError = callbackError instanceof Error ? callbackError.message : String(callbackError);
|
|
3875
|
-
log("error", "onTodoSnapshot", { error: hostError });
|
|
3876
|
-
}
|
|
3877
|
-
state.onToolResult?.(persisted ?? buildTodoToolResult(settled.id, snapshot, hostError ?? error));
|
|
3878
|
-
} else if (settled[kStreamingBlockKind] === "cursor-edit") {
|
|
3879
|
-
const edit = selectEditCall(toolCall);
|
|
3880
|
-
if (edit?.args) settled.arguments = omitUndefinedArgs({
|
|
3881
|
-
...settled.arguments,
|
|
3882
|
-
path: edit.args.path ?? stringToolArg(settled.arguments, "path"),
|
|
3883
|
-
stream_content: edit.args.streamContent ?? stringToolArg(settled.arguments, "stream_content")
|
|
3884
|
-
});
|
|
3885
|
-
if (!state.pairedEditToolCallIds?.has(settled.id)) {
|
|
3886
|
-
const { text, isError } = describeEditResult(toolCall);
|
|
3887
|
-
state.onToolResult?.({
|
|
3888
|
-
role: "toolResult",
|
|
3889
|
-
toolCallId: settled.id,
|
|
3890
|
-
toolName: "edit",
|
|
3891
|
-
content: [{
|
|
3892
|
-
type: "text",
|
|
3893
|
-
text
|
|
3894
|
-
}],
|
|
3895
|
-
isError,
|
|
3896
|
-
timestamp: Date.now()
|
|
3897
|
-
});
|
|
3898
|
-
}
|
|
3899
|
-
}
|
|
3900
|
-
const idx = output.content.indexOf(settled);
|
|
3901
|
-
clearStreamingPartialJson(settled);
|
|
3902
|
-
stream.push({
|
|
3903
|
-
type: "toolcall_end",
|
|
3904
|
-
contentIndex: idx,
|
|
3905
|
-
toolCall: settled,
|
|
3906
|
-
partial: output
|
|
3907
|
-
});
|
|
3908
|
-
releaseStreamedCall(state, settled);
|
|
3909
|
-
}
|
|
3910
|
-
} else if (updateCase === "turnEnded") {
|
|
3911
|
-
output.stopReason = "stop";
|
|
3912
|
-
if (classifyModel("cursor", output.model).family === "k3" && !output.content.some((item) => item.type === "thinking" && item.thinking.length > 0)) warn("Cursor kimi-k3 turn completed without thinking blocks; persisted history will replay this turn without reasoning", {
|
|
3913
|
-
model: output.model,
|
|
3914
|
-
messageTimestamp: output.timestamp
|
|
3915
|
-
});
|
|
3916
|
-
} else if (updateCase === "tokenDelta") {
|
|
3917
|
-
const tokenDelta = update.message.value;
|
|
3918
|
-
usageState.sawTokenDelta = true;
|
|
3919
|
-
output.usage.output += tokenDelta.tokens || 0;
|
|
3920
|
-
output.usage.totalTokens = output.usage.input + output.usage.output;
|
|
3921
|
-
}
|
|
3922
|
-
}
|
|
3923
|
-
function handleConversationCheckpointUpdate(checkpoint, output, usageState, onConversationCheckpoint) {
|
|
3924
|
-
onConversationCheckpoint?.(checkpoint);
|
|
3925
|
-
if (usageState.sawTokenDelta) return;
|
|
3926
|
-
const usedTokens = checkpoint.tokenDetails?.usedTokens ?? 0;
|
|
3927
|
-
if (usedTokens <= 0) return;
|
|
3928
|
-
if (output.usage.contextTokens !== usedTokens) output.usage.contextTokens = usedTokens;
|
|
3929
|
-
}
|
|
3930
|
-
function createBlobId(data) {
|
|
3931
|
-
return new Uint8Array(createHash("sha256").update(data).digest());
|
|
3932
|
-
}
|
|
3933
|
-
function storeCursorBlob(blobStore, data) {
|
|
3934
|
-
const blobId = createBlobId(data);
|
|
3935
|
-
blobStore.set(Buffer.from(blobId).toString("hex"), data);
|
|
3936
|
-
return blobId;
|
|
3937
|
-
}
|
|
3938
|
-
/**
|
|
3939
|
-
* Cursor AgentService reconstructs the model prompt from `requestContext.rules`,
|
|
3940
|
-
* not from the client-supplied `rootPromptMessagesJson` system blobs. Map each
|
|
3941
|
-
* OMP system-prompt entry to a global CursorRule so always-apply rules survive
|
|
3942
|
-
* that reconstruction.
|
|
3943
|
-
*/
|
|
3944
|
-
function buildCursorRequestContextRules(systemPrompt) {
|
|
3945
|
-
return normalizeSystemPrompts(systemPrompt).map((content, index) => create(CursorRuleSchema, {
|
|
3946
|
-
fullPath: `/omp/system-prompt/${index}.mdc`,
|
|
3947
|
-
content,
|
|
3948
|
-
source: 2,
|
|
3949
|
-
type: create(CursorRuleTypeSchema, { type: {
|
|
3950
|
-
case: "global",
|
|
3951
|
-
value: create(CursorRuleTypeGlobalSchema, {})
|
|
3952
|
-
} })
|
|
3953
|
-
}));
|
|
3954
|
-
}
|
|
3955
|
-
/**
|
|
3956
|
-
* Local tools Cursor already drives natively over the exec channel, so
|
|
3957
|
-
* advertising them again as MCP tools would give the model two ways to call the
|
|
3958
|
-
* same thing.
|
|
3959
|
-
*
|
|
3960
|
-
* `lsp` is deliberately NOT here. The native `diagnosticsArgs` frame covers
|
|
3961
|
-
* exactly one of the tool's actions (`action: "diagnostics"`); the rest —
|
|
3962
|
-
* `definition`, `references`, `rename`, `code_actions`, `hover`,
|
|
3963
|
-
* `implementation`, `type_definition`, `symbols`, ... — have no native frame at
|
|
3964
|
-
* all, so filtering the whole tool out hid every one of them from the model.
|
|
3965
|
-
*/
|
|
3966
|
-
const CURSOR_NATIVE_TOOL_NAMES = /* @__PURE__ */ new Set([
|
|
3967
|
-
"bash",
|
|
3968
|
-
"read",
|
|
3969
|
-
"write",
|
|
3970
|
-
"delete",
|
|
3971
|
-
"ls",
|
|
3972
|
-
"grep",
|
|
3973
|
-
"todo"
|
|
3974
|
-
]);
|
|
3975
|
-
function isJsonValue(value) {
|
|
3976
|
-
if (value === null || typeof value === "string" || typeof value === "boolean") return true;
|
|
3977
|
-
if (typeof value === "number") return Number.isFinite(value);
|
|
3978
|
-
if (Array.isArray(value)) return value.every(isJsonValue);
|
|
3979
|
-
if (!isRecord$1(value)) return false;
|
|
3980
|
-
for (const key in value) if (!isJsonValue(value[key])) return false;
|
|
3981
|
-
return true;
|
|
3982
|
-
}
|
|
3983
|
-
function buildMcpToolDefinitions(tools, requiresCursorToolSchemaProjection = false) {
|
|
3984
|
-
if (!tools || tools.length === 0) return [];
|
|
3985
|
-
const advertisedTools = tools.filter((tool) => !CURSOR_NATIVE_TOOL_NAMES.has(tool.name));
|
|
3986
|
-
if (advertisedTools.length === 0) return [];
|
|
3987
|
-
const writeTool = tools.find((tool) => tool.name === "write");
|
|
3988
|
-
return (writeTool ? [...advertisedTools, writeTool] : advertisedTools).map((tool) => {
|
|
3989
|
-
const wireSchema = toolWireSchema(tool);
|
|
3990
|
-
const jsonSchema = requiresCursorToolSchemaProjection ? sanitizeSchemaForCursor(wireSchema) : wireSchema;
|
|
3991
|
-
const schemaValue = jsonSchema !== null && !Array.isArray(jsonSchema) && isJsonValue(jsonSchema) ? jsonSchema : {
|
|
3992
|
-
type: "object",
|
|
3993
|
-
properties: {},
|
|
3994
|
-
required: []
|
|
3995
|
-
};
|
|
3996
|
-
const inputSchema = encodeJsonValue(schemaValue);
|
|
3997
|
-
return create(McpToolDefinitionSchema, {
|
|
3998
|
-
name: tool.name,
|
|
3999
|
-
description: tool.description || "",
|
|
4000
|
-
providerIdentifier: "pi-agent",
|
|
4001
|
-
toolName: tool.name,
|
|
4002
|
-
inputSchema
|
|
4003
|
-
});
|
|
4004
|
-
});
|
|
4005
|
-
}
|
|
4006
|
-
/**
|
|
4007
|
-
* Extract text content from a user or developer message.
|
|
4008
|
-
*/
|
|
4009
|
-
function extractUserMessageText(msg) {
|
|
4010
|
-
if (msg.role !== "user" && msg.role !== "developer") return "";
|
|
4011
|
-
const content = msg.content;
|
|
4012
|
-
if (typeof content === "string") return content.trim();
|
|
4013
|
-
return content.filter((c) => c.type === "text").map((c) => c.text).join("\n").trim();
|
|
4014
|
-
}
|
|
4015
|
-
function hasUserMessageImages(msg) {
|
|
4016
|
-
return (msg.role === "user" || msg.role === "developer") && Array.isArray(msg.content) && msg.content.some((item) => item.type === "image");
|
|
4017
|
-
}
|
|
4018
|
-
function buildCursorRootPromptContent(content) {
|
|
4019
|
-
if (typeof content === "string") {
|
|
4020
|
-
const text = content.trim();
|
|
4021
|
-
return text ? [{
|
|
4022
|
-
type: "text",
|
|
4023
|
-
text
|
|
4024
|
-
}] : [];
|
|
4025
|
-
}
|
|
4026
|
-
const parts = [];
|
|
4027
|
-
for (const item of content) if (item.type === "text") {
|
|
4028
|
-
const text = item.text.trim();
|
|
4029
|
-
if (text) parts.push({
|
|
4030
|
-
type: "text",
|
|
4031
|
-
text
|
|
4032
|
-
});
|
|
4033
|
-
} else parts.push({
|
|
4034
|
-
type: "image",
|
|
4035
|
-
image: `data:${item.mimeType};base64,${item.data}`,
|
|
4036
|
-
mediaType: item.mimeType
|
|
4037
|
-
});
|
|
4038
|
-
return parts;
|
|
4039
|
-
}
|
|
4040
|
-
function cursorUserContentKey(content) {
|
|
4041
|
-
if (typeof content === "string") return content.trim();
|
|
4042
|
-
const hash = createHash("sha256");
|
|
4043
|
-
for (const item of content) {
|
|
4044
|
-
hash.update(item.type);
|
|
4045
|
-
if (item.type === "text") hash.update(item.text);
|
|
4046
|
-
else {
|
|
4047
|
-
hash.update(item.mimeType);
|
|
4048
|
-
hash.update(item.data);
|
|
4049
|
-
}
|
|
4050
|
-
}
|
|
4051
|
-
return hash.digest("hex");
|
|
4052
|
-
}
|
|
4053
|
-
function canReplayCursorThinking(msg, targetModelId) {
|
|
4054
|
-
return targetModelId !== void 0 && classifyModel("cursor", targetModelId).family === "k3" && msg.api === "cursor-agent" && msg.provider === "cursor" && msg.model === targetModelId;
|
|
4055
|
-
}
|
|
4056
|
-
function buildCursorAssistantContent(msg, targetModelId) {
|
|
4057
|
-
const content = [];
|
|
4058
|
-
const replayThinking = canReplayCursorThinking(msg, targetModelId);
|
|
4059
|
-
for (const item of msg.content) if (item.type === "text") {
|
|
4060
|
-
if (item.text) content.push({
|
|
4061
|
-
type: "text",
|
|
4062
|
-
text: item.text
|
|
4063
|
-
});
|
|
4064
|
-
} else if (item.type === "thinking") {
|
|
4065
|
-
if (replayThinking && item.thinking) content.push({
|
|
4066
|
-
type: "reasoning",
|
|
4067
|
-
text: item.thinking,
|
|
4068
|
-
providerOptions: { cursor: { modelName: msg.model } },
|
|
4069
|
-
...item.thinkingSignature ? { signature: item.thinkingSignature } : {}
|
|
4070
|
-
});
|
|
4071
|
-
} else if (item.type === "toolCall") content.push({
|
|
4072
|
-
type: "tool-call",
|
|
4073
|
-
toolCallId: normalizeToolCallId(item.id),
|
|
4074
|
-
toolName: item.name,
|
|
4075
|
-
args: normalizeCursorMcpArguments(item.arguments)
|
|
4076
|
-
});
|
|
4077
|
-
return content;
|
|
4078
|
-
}
|
|
4079
|
-
function assertCursorKimiK3HistoryReplayable(messages, activeUserMessageIndex, targetModelId) {
|
|
4080
|
-
if (!targetModelId || classifyModel("cursor", targetModelId).family !== "k3") return;
|
|
4081
|
-
const historyEnd = activeUserMessageIndex >= 0 ? activeUserMessageIndex : messages.length;
|
|
4082
|
-
const missingThinkingTurns = [];
|
|
4083
|
-
const newlyWarnedKeys = [];
|
|
4084
|
-
let assistantTurn = 0;
|
|
4085
|
-
for (let i = 0; i < historyEnd; i++) {
|
|
4086
|
-
const msg = messages[i];
|
|
4087
|
-
if (msg.role !== "assistant") continue;
|
|
4088
|
-
assistantTurn++;
|
|
4089
|
-
if (!(msg.api === "cursor-agent" && msg.provider === "cursor" && msg.model === targetModelId)) throw new ValidationError(`Cursor ${targetModelId} cannot continue history from a different model (${msg.provider}/${msg.model}); start a new session.`);
|
|
4090
|
-
if (msg.content.some((item) => item.type === "thinking" && item.thinking.length > 0)) continue;
|
|
4091
|
-
const warningKey = `${msg.api}\0${msg.provider}\0${msg.model}\0${msg.timestamp}`;
|
|
4092
|
-
if (warnedCursorKimiK3ReplayMessages.has(warningKey)) continue;
|
|
4093
|
-
missingThinkingTurns.push(assistantTurn);
|
|
4094
|
-
newlyWarnedKeys.push(warningKey);
|
|
4095
|
-
}
|
|
4096
|
-
if (missingThinkingTurns.length === 0) return;
|
|
4097
|
-
for (const key of newlyWarnedKeys) warnedCursorKimiK3ReplayMessages.add(key);
|
|
4098
|
-
warn(`Cursor kimi-k3 history contains same-model assistant turn(s) ${missingThinkingTurns.join(", ")} without thinking blocks; replaying those spans without reasoning may make generation less stable`, {
|
|
4099
|
-
model: targetModelId,
|
|
4100
|
-
assistantTurns: missingThinkingTurns
|
|
4101
|
-
});
|
|
4102
|
-
}
|
|
4103
|
-
/**
|
|
4104
|
-
* Index of the last user/developer message in `messages`, or -1 if none.
|
|
4105
|
-
* Used to exclude the current user turn from history builders — it goes in
|
|
4106
|
-
* `ConversationActionSchema.userMessageAction`, not in history structures.
|
|
4107
|
-
*/
|
|
4108
|
-
function findLastUserMessageIndex(messages) {
|
|
4109
|
-
for (let i = messages.length - 1; i >= 0; i--) {
|
|
4110
|
-
const role = messages[i].role;
|
|
4111
|
-
if (role === "user" || role === "developer") return i;
|
|
4112
|
-
}
|
|
4113
|
-
return -1;
|
|
4114
|
-
}
|
|
4115
|
-
/**
|
|
4116
|
-
* Build `ConversationStateStructure.rootPromptMessagesJson` blob IDs for the
|
|
4117
|
-
* system prompt plus prior conversation history, as JSON blobs matching
|
|
4118
|
-
* Cursor's internal Vercel-AI-SDK-shaped message format.
|
|
4119
|
-
*
|
|
4120
|
-
* Cursor's server uses `rootPromptMessagesJson` (not `turns[]`) to build the
|
|
4121
|
-
* actual model prompt. `turns[]` is UI/display metadata. Without populating
|
|
4122
|
-
* this field, multi-turn conversations lose prior context — the model sees
|
|
4123
|
-
* only an empty placeholder where historical user turns should be.
|
|
4124
|
-
* The active user message is excluded because it is sent in the action.
|
|
4125
|
-
*/
|
|
4126
|
-
/**
|
|
4127
|
-
* Build one Cursor system-message JSON blob per ordered system prompt. Emitting separate blobs
|
|
4128
|
-
* (rather than a single `\n\n`-joined string) lets Cursor's blob cache hit independently per
|
|
4129
|
-
* entry: changing only the last prompt does not invalidate earlier blob ids, so the prefix
|
|
4130
|
-
* up to the changed prompt remains cached on the server side.
|
|
4131
|
-
*
|
|
4132
|
-
* When no system prompts are provided, returns a single default greeting so we never emit
|
|
4133
|
-
* an empty `rootPromptMessagesJson` head.
|
|
4134
|
-
*/
|
|
4135
|
-
function buildCursorSystemPromptJsons(systemPrompt) {
|
|
4136
|
-
const systemPrompts = normalizeSystemPrompts(systemPrompt);
|
|
4137
|
-
if (systemPrompts.length === 0) return [JSON.stringify({
|
|
4138
|
-
role: "system",
|
|
4139
|
-
content: "You are a helpful assistant."
|
|
4140
|
-
})];
|
|
4141
|
-
return systemPrompts.map((content) => JSON.stringify({
|
|
4142
|
-
role: "system",
|
|
4143
|
-
content
|
|
4144
|
-
}));
|
|
4145
|
-
}
|
|
4146
|
-
function collectCursorToolHistory(messages, historyEnd) {
|
|
4147
|
-
const toolResults = /* @__PURE__ */ new Map();
|
|
4148
|
-
const pairedToolCallIds = /* @__PURE__ */ new Set();
|
|
4149
|
-
for (let index = 0; index < historyEnd; index++) {
|
|
4150
|
-
const message = messages[index];
|
|
4151
|
-
if (message.role === "toolResult") toolResults.set(message.toolCallId, message);
|
|
4152
|
-
else if (message.role === "assistant") {
|
|
4153
|
-
for (const item of message.content) if (item.type === "toolCall") pairedToolCallIds.add(item.id);
|
|
4154
|
-
}
|
|
4155
|
-
}
|
|
4156
|
-
return {
|
|
4157
|
-
toolResults,
|
|
4158
|
-
pairedToolCallIds
|
|
4159
|
-
};
|
|
4160
|
-
}
|
|
4161
|
-
function cursorOrphanToolResultText(result) {
|
|
4162
|
-
return `${result.isError ? "[Tool Error]" : "[Tool Result]"}\n${toolResultToText(result) || "(empty result)"}`;
|
|
4163
|
-
}
|
|
4164
|
-
function buildRootPromptMessagesJson(messages, systemPromptIds, blobStore, activeUserMessageIndex = findLastUserMessageIndex(messages), targetModelId) {
|
|
4165
|
-
assertCursorKimiK3HistoryReplayable(messages, activeUserMessageIndex, targetModelId);
|
|
4166
|
-
const { pairedToolCallIds } = collectCursorToolHistory(messages, activeUserMessageIndex >= 0 ? activeUserMessageIndex : messages.length);
|
|
4167
|
-
const entries = [...systemPromptIds];
|
|
4168
|
-
const pushJson = (obj) => {
|
|
4169
|
-
const bytes = new TextEncoder().encode(JSON.stringify(obj));
|
|
4170
|
-
entries.push(storeCursorBlob(blobStore, bytes));
|
|
4171
|
-
};
|
|
4172
|
-
for (let i = 0; i < messages.length; i++) {
|
|
4173
|
-
if (i === activeUserMessageIndex) break;
|
|
4174
|
-
const msg = messages[i];
|
|
4175
|
-
if (msg.role === "user" || msg.role === "developer") {
|
|
4176
|
-
const content = buildCursorRootPromptContent(msg.content);
|
|
4177
|
-
if (content.length === 0) continue;
|
|
4178
|
-
pushJson({
|
|
4179
|
-
role: "user",
|
|
4180
|
-
content
|
|
4181
|
-
});
|
|
4182
|
-
} else if (msg.role === "assistant") {
|
|
4183
|
-
const content = buildCursorAssistantContent(msg, targetModelId);
|
|
4184
|
-
if (content.length === 0) continue;
|
|
4185
|
-
pushJson({
|
|
4186
|
-
role: "assistant",
|
|
4187
|
-
content
|
|
4188
|
-
});
|
|
4189
|
-
} else if (msg.role === "toolResult") {
|
|
4190
|
-
if (!pairedToolCallIds.has(msg.toolCallId)) {
|
|
4191
|
-
pushJson({
|
|
4192
|
-
role: "assistant",
|
|
4193
|
-
content: [{
|
|
4194
|
-
type: "text",
|
|
4195
|
-
text: cursorOrphanToolResultText(msg)
|
|
4196
|
-
}]
|
|
4197
|
-
});
|
|
4198
|
-
continue;
|
|
4199
|
-
}
|
|
4200
|
-
const toolCallId = normalizeToolCallId(msg.toolCallId);
|
|
4201
|
-
pushJson({
|
|
4202
|
-
role: "tool",
|
|
4203
|
-
id: toolCallId,
|
|
4204
|
-
content: [{
|
|
4205
|
-
type: "tool-result",
|
|
4206
|
-
toolName: msg.toolName,
|
|
4207
|
-
toolCallId,
|
|
4208
|
-
result: toolResultToText(msg),
|
|
4209
|
-
...msg.isError ? { isError: true } : {}
|
|
4210
|
-
}]
|
|
4211
|
-
});
|
|
4212
|
-
}
|
|
4213
|
-
}
|
|
4214
|
-
return entries;
|
|
4215
|
-
}
|
|
4216
|
-
function normalizeCursorMcpArgument(value, seen) {
|
|
4217
|
-
if (value === null || typeof value === "string" || typeof value === "boolean") return value;
|
|
4218
|
-
if (typeof value === "number") return Number.isFinite(value) ? value : null;
|
|
4219
|
-
if (typeof value !== "object") return void 0;
|
|
4220
|
-
if (seen.has(value)) return void 0;
|
|
4221
|
-
seen.add(value);
|
|
4222
|
-
try {
|
|
4223
|
-
if (Array.isArray(value)) return value.map((item) => normalizeCursorMcpArgument(item, seen) ?? null);
|
|
4224
|
-
if (!isRecord$1(value)) return void 0;
|
|
4225
|
-
const normalized = Object.create(null);
|
|
4226
|
-
for (const key in value) {
|
|
4227
|
-
if (!Object.hasOwn(value, key)) continue;
|
|
4228
|
-
const normalizedItem = normalizeCursorMcpArgument(value[key], seen);
|
|
4229
|
-
if (normalizedItem !== void 0) normalized[key] = normalizedItem;
|
|
4230
|
-
}
|
|
4231
|
-
return normalized;
|
|
4232
|
-
} finally {
|
|
4233
|
-
seen.delete(value);
|
|
4234
|
-
}
|
|
4235
|
-
}
|
|
4236
|
-
function normalizeCursorMcpArguments(args) {
|
|
4237
|
-
const normalized = Object.create(null);
|
|
4238
|
-
const seen = /* @__PURE__ */ new Set();
|
|
4239
|
-
for (const name in args) {
|
|
4240
|
-
if (!Object.hasOwn(args, name)) continue;
|
|
4241
|
-
const normalizedValue = normalizeCursorMcpArgument(args[name], seen);
|
|
4242
|
-
if (normalizedValue !== void 0) normalized[name] = normalizedValue;
|
|
4243
|
-
}
|
|
4244
|
-
return normalized;
|
|
4245
|
-
}
|
|
4246
|
-
function encodeCursorMcpArguments(toolCall) {
|
|
4247
|
-
const encoded = Object.create(null);
|
|
4248
|
-
const normalized = normalizeCursorMcpArguments(toolCall.arguments);
|
|
4249
|
-
for (const name in normalized) encoded[name] = encodeJsonValue(normalized[name]);
|
|
4250
|
-
return encoded;
|
|
4251
|
-
}
|
|
4252
|
-
function createCursorMcpResult(result) {
|
|
4253
|
-
if (result.isError) return create(McpToolResultSchema, { result: {
|
|
4254
|
-
case: "error",
|
|
4255
|
-
value: create(McpToolErrorSchema, { error: toolResultToText(result) })
|
|
4256
|
-
} });
|
|
4257
|
-
return create(McpToolResultSchema, { result: {
|
|
4258
|
-
case: "success",
|
|
4259
|
-
value: create(McpSuccessSchema, { content: result.content.map((item) => item.type === "text" ? create(McpToolResultContentItemSchema, { content: {
|
|
4260
|
-
case: "text",
|
|
4261
|
-
value: create(McpTextContentSchema, { text: item.text })
|
|
4262
|
-
} }) : create(McpToolResultContentItemSchema, { content: {
|
|
4263
|
-
case: "image",
|
|
4264
|
-
value: create(McpImageContentSchema, {
|
|
4265
|
-
data: Uint8Array.from(Buffer.from(item.data, "base64")),
|
|
4266
|
-
mimeType: item.mimeType
|
|
4267
|
-
})
|
|
4268
|
-
} })) })
|
|
4269
|
-
} });
|
|
4270
|
-
}
|
|
4271
|
-
function createCursorToolCallStep(toolCall, result) {
|
|
4272
|
-
const toolCallId = normalizeToolCallId(toolCall.id);
|
|
4273
|
-
const mcpCall = create(McpToolCallSchema, {
|
|
4274
|
-
args: create(McpArgsSchema, {
|
|
4275
|
-
name: toolCall.name,
|
|
4276
|
-
args: encodeCursorMcpArguments(toolCall),
|
|
4277
|
-
toolCallId,
|
|
4278
|
-
providerIdentifier: "pi-agent",
|
|
4279
|
-
toolName: toolCall.name
|
|
4280
|
-
}),
|
|
4281
|
-
...result ? { result: createCursorMcpResult(result) } : {}
|
|
4282
|
-
});
|
|
4283
|
-
return create(ConversationStepSchema, { message: {
|
|
4284
|
-
case: "toolCall",
|
|
4285
|
-
value: create(ToolCallSchema, {
|
|
4286
|
-
tool: {
|
|
4287
|
-
case: "mcpToolCall",
|
|
4288
|
-
value: mcpCall
|
|
4289
|
-
},
|
|
4290
|
-
toolCallId
|
|
4291
|
-
})
|
|
4292
|
-
} });
|
|
4293
|
-
}
|
|
4294
|
-
/**
|
|
4295
|
-
* Convert context.messages to Cursor's ConversationTurnStructure blob IDs.
|
|
4296
|
-
* Groups messages into turns: each turn is a user message followed by the assistant's response.
|
|
4297
|
-
* Excludes the active user message (which goes in the action).
|
|
4298
|
-
*
|
|
4299
|
-
* Each `AgentConversationTurnStructure.user_message`, `steps[]`, and the outer
|
|
4300
|
-
* `ConversationStateStructure.turns[]` entry is a blob ID into `blobStore`.
|
|
4301
|
-
*/
|
|
4302
|
-
function buildConversationTurns(messages, blobStore, activeUserMessageIndex = findLastUserMessageIndex(messages), targetModelId) {
|
|
4303
|
-
const turns = [];
|
|
4304
|
-
const { toolResults, pairedToolCallIds } = collectCursorToolHistory(messages, activeUserMessageIndex >= 0 ? activeUserMessageIndex : messages.length);
|
|
4305
|
-
let i = 0;
|
|
4306
|
-
while (i < messages.length) {
|
|
4307
|
-
const msg = messages[i];
|
|
4308
|
-
if (msg.role !== "user" && msg.role !== "developer") {
|
|
4309
|
-
i++;
|
|
4310
|
-
continue;
|
|
4311
|
-
}
|
|
4312
|
-
if (i === activeUserMessageIndex) break;
|
|
4313
|
-
const userText = extractUserMessageText(msg);
|
|
4314
|
-
if (userText.length === 0 && !hasUserMessageImages(msg)) {
|
|
4315
|
-
i++;
|
|
4316
|
-
continue;
|
|
4317
|
-
}
|
|
4318
|
-
const userMessage = createCursorUserMessage(msg.content, userText, deterministicUuid(`u:${turns.length}:${cursorUserContentKey(msg.content)}`));
|
|
4319
|
-
const userMessageBlobId = storeCursorBlob(blobStore, toBinary(UserMessageSchema, userMessage));
|
|
4320
|
-
const stepBlobIds = [];
|
|
4321
|
-
i++;
|
|
4322
|
-
while (i < messages.length && messages[i].role !== "user" && messages[i].role !== "developer") {
|
|
4323
|
-
const stepMsg = messages[i];
|
|
4324
|
-
if (stepMsg.role === "assistant") for (const item of stepMsg.content) {
|
|
4325
|
-
let step;
|
|
4326
|
-
if (item.type === "text") {
|
|
4327
|
-
if (!item.text) continue;
|
|
4328
|
-
step = create(ConversationStepSchema, { message: {
|
|
4329
|
-
case: "assistantMessage",
|
|
4330
|
-
value: create(AssistantMessageSchema, { text: item.text })
|
|
4331
|
-
} });
|
|
4332
|
-
} else if (item.type === "thinking") {
|
|
4333
|
-
if (!item.thinking || !canReplayCursorThinking(stepMsg, targetModelId)) continue;
|
|
4334
|
-
step = create(ConversationStepSchema, { message: {
|
|
4335
|
-
case: "thinkingMessage",
|
|
4336
|
-
value: create(ThinkingMessageSchema, { text: item.thinking })
|
|
4337
|
-
} });
|
|
4338
|
-
} else if (item.type === "toolCall") step = createCursorToolCallStep(item, toolResults.get(item.id));
|
|
4339
|
-
else continue;
|
|
4340
|
-
stepBlobIds.push(storeCursorBlob(blobStore, toBinary(ConversationStepSchema, step)));
|
|
4341
|
-
}
|
|
4342
|
-
else if (stepMsg.role === "toolResult" && !pairedToolCallIds.has(stepMsg.toolCallId)) {
|
|
4343
|
-
const step = create(ConversationStepSchema, { message: {
|
|
4344
|
-
case: "assistantMessage",
|
|
4345
|
-
value: create(AssistantMessageSchema, { text: cursorOrphanToolResultText(stepMsg) })
|
|
4346
|
-
} });
|
|
4347
|
-
stepBlobIds.push(storeCursorBlob(blobStore, toBinary(ConversationStepSchema, step)));
|
|
4348
|
-
}
|
|
4349
|
-
i++;
|
|
4350
|
-
}
|
|
4351
|
-
const agentTurn = create(AgentConversationTurnStructureSchema, {
|
|
4352
|
-
userMessage: userMessageBlobId,
|
|
4353
|
-
steps: stepBlobIds
|
|
4354
|
-
});
|
|
4355
|
-
const turn = create(ConversationTurnStructureSchema, { turn: {
|
|
4356
|
-
case: "agentConversationTurn",
|
|
4357
|
-
value: agentTurn
|
|
4358
|
-
} });
|
|
4359
|
-
turns.push(storeCursorBlob(blobStore, toBinary(ConversationTurnStructureSchema, turn)));
|
|
4360
|
-
}
|
|
4361
|
-
return turns;
|
|
4362
|
-
}
|
|
4363
|
-
function createCursorUserMessage(content, text, messageId = crypto.randomUUID()) {
|
|
4364
|
-
const images = typeof content === "string" ? [] : extractImages(content);
|
|
4365
|
-
return create(UserMessageSchema, {
|
|
4366
|
-
text,
|
|
4367
|
-
messageId,
|
|
4368
|
-
...images.length > 0 ? { selectedContext: create(SelectedContextSchema, { selectedImages: images }) } : {}
|
|
4369
|
-
});
|
|
4370
|
-
}
|
|
4371
|
-
function extractImages(content) {
|
|
4372
|
-
return content.filter((item) => item.type === "image").map((image) => create(SelectedImageSchema, {
|
|
4373
|
-
uuid: crypto.randomUUID(),
|
|
4374
|
-
mimeType: image.mimeType,
|
|
4375
|
-
dataOrBlobId: {
|
|
4376
|
-
case: "data",
|
|
4377
|
-
value: Uint8Array.from(Buffer.from(image.data, "base64"))
|
|
4378
|
-
}
|
|
4379
|
-
}));
|
|
4380
|
-
}
|
|
4381
|
-
/**
|
|
4382
|
-
* Resolve the Cursor Run wire model id and its parameter list.
|
|
4383
|
-
*
|
|
4384
|
-
* Cursor's `GetUsableModels` lists reasoning models as per-effort sibling
|
|
4385
|
-
* slugs (`gpt-5.4-mini-low`, `gpt-5.6-sol-high`), and OMP copies those ids 1:1.
|
|
4386
|
-
* The Run endpoint rejects a sibling slug as the wire `model_id` with
|
|
4387
|
-
* `resource_exhausted` (errorId 528384); the official `cursor-agent` splits the
|
|
4388
|
-
* slug into its base model id plus a `reasoning` effort parameter. Mirror that
|
|
4389
|
-
* for OpenAI-family ids: split the tier with the compiled catalog policy
|
|
4390
|
-
* (`collapseVariantId`, KDL suffix/lane rules) and emit
|
|
4391
|
-
* `{ id: "reasoning", value: <effort> }`. The off tier (`-none`) is a sibling
|
|
4392
|
-
* slug too, so it normalizes to the bare (lane-preserving) base with no
|
|
4393
|
-
* reasoning parameter instead of going out raw.
|
|
4394
|
-
*
|
|
4395
|
-
* Non-OpenAI ids pass through unchanged — Cursor-native ids (`composer-*`,
|
|
4396
|
-
* `cursor-grok-*`, `default`) carry no effort suffix, and Claude/other siblings
|
|
4397
|
-
* need additional parameters (`thinking`, `context`) whose per-model values are
|
|
4398
|
-
* not exposed by the decoded `GetUsableModels` schema, so guessing them would
|
|
4399
|
-
* re-trigger 528384.
|
|
4400
|
-
*/
|
|
4401
|
-
function resolveCursorWireModel(model, requestModelId, wireMode = "normalized") {
|
|
4402
|
-
const wireModelId = requestModelId ?? model.requestModelId ?? model.id;
|
|
4403
|
-
if (wireMode === "discovered") return {
|
|
4404
|
-
modelId: wireModelId,
|
|
4405
|
-
parameters: []
|
|
4406
|
-
};
|
|
4407
|
-
const collapsed = collapseVariantId("cursor", wireModelId);
|
|
4408
|
-
const effort = collapsed.effort;
|
|
4409
|
-
const base = effort !== void 0 ? collapsed.logicalId : void 0;
|
|
4410
|
-
if (effort !== void 0 && base && classifyModel("cursor", base).class === "openai") {
|
|
4411
|
-
if (effort === "off") return {
|
|
4412
|
-
modelId: base,
|
|
4413
|
-
parameters: []
|
|
4414
|
-
};
|
|
4415
|
-
if (THINKING_EFFORTS.includes(effort)) return {
|
|
4416
|
-
modelId: base,
|
|
4417
|
-
parameters: [create(RequestedModel_ModelParameterbytesSchema, {
|
|
4418
|
-
id: "reasoning",
|
|
4419
|
-
value: collapsed.effort
|
|
4420
|
-
})]
|
|
4421
|
-
};
|
|
4422
|
-
}
|
|
4423
|
-
if (wireModelId === "composer-2.5") return {
|
|
4424
|
-
modelId: wireModelId,
|
|
4425
|
-
parameters: [create(RequestedModel_ModelParameterbytesSchema, {
|
|
4426
|
-
id: "fast",
|
|
4427
|
-
value: "false"
|
|
4428
|
-
})]
|
|
4429
|
-
};
|
|
4430
|
-
return {
|
|
4431
|
-
modelId: wireModelId,
|
|
4432
|
-
parameters: []
|
|
4433
|
-
};
|
|
4434
|
-
}
|
|
4435
|
-
async function buildGrpcRequestForWireMode(model, context, options, state, wireMode) {
|
|
4436
|
-
const blobStore = state.blobStore;
|
|
4437
|
-
const systemPromptIds = buildCursorSystemPromptJsons(context.systemPrompt).map((json) => storeCursorBlob(blobStore, new TextEncoder().encode(json)));
|
|
4438
|
-
const lastUserMessageIndex = findLastUserMessageIndex(context.messages);
|
|
4439
|
-
let activeUserMessageIndex = context.messages.length - 1;
|
|
4440
|
-
const activeMessage = context.messages[activeUserMessageIndex];
|
|
4441
|
-
let activeUserMessage = activeMessage?.role === "user" || activeMessage?.role === "developer" ? activeMessage : void 0;
|
|
4442
|
-
if (state.rotatedFresh && !activeUserMessage && lastUserMessageIndex >= 0) {
|
|
4443
|
-
activeUserMessageIndex = lastUserMessageIndex;
|
|
4444
|
-
const lastUser = context.messages[lastUserMessageIndex];
|
|
4445
|
-
if (lastUser.role === "user" || lastUser.role === "developer") activeUserMessage = lastUser;
|
|
4446
|
-
}
|
|
4447
|
-
let userContent;
|
|
4448
|
-
let userText = "";
|
|
4449
|
-
let hasUserImages = false;
|
|
4450
|
-
if (activeUserMessage?.role === "user" || activeUserMessage?.role === "developer") {
|
|
4451
|
-
userContent = activeUserMessage.content;
|
|
4452
|
-
if (typeof userContent === "string") userText = userContent.trim();
|
|
4453
|
-
else {
|
|
4454
|
-
userText = extractText(userContent);
|
|
4455
|
-
hasUserImages = hasImages(userContent);
|
|
4456
|
-
}
|
|
4457
|
-
}
|
|
4458
|
-
const action = create(ConversationActionSchema, { action: userContent && (userText.trim().length > 0 || hasUserImages) ? {
|
|
4459
|
-
case: "userMessageAction",
|
|
4460
|
-
value: create(UserMessageActionSchema, { userMessage: createCursorUserMessage(userContent, userText) })
|
|
4461
|
-
} : {
|
|
4462
|
-
case: "resumeAction",
|
|
4463
|
-
value: create(ResumeActionSchema, {})
|
|
4464
|
-
} });
|
|
4465
|
-
const turns = buildConversationTurns(context.messages, blobStore, activeUserMessage ? activeUserMessageIndex : -1, model.id);
|
|
4466
|
-
const rootPromptMessagesJson = buildRootPromptMessagesJson(context.messages, systemPromptIds, blobStore, activeUserMessage ? activeUserMessageIndex : -1, model.id);
|
|
4467
|
-
const cachedPromptHead = state.conversationState?.rootPromptMessagesJson?.slice(0, systemPromptIds.length) ?? [];
|
|
4468
|
-
const hasMatchingPrompt = cachedPromptHead.length === systemPromptIds.length && systemPromptIds.every((id, idx) => Buffer.from(cachedPromptHead[idx]).equals(id));
|
|
4469
|
-
const baseState = state.conversationState && hasMatchingPrompt ? state.conversationState : create(ConversationStateStructureSchema, {
|
|
4470
|
-
rootPromptMessagesJson: systemPromptIds,
|
|
4471
|
-
turns: [],
|
|
4472
|
-
todos: [],
|
|
4473
|
-
pendingToolCalls: [],
|
|
4474
|
-
previousWorkspaceUris: [],
|
|
4475
|
-
fileStates: {},
|
|
4476
|
-
fileStatesV2: {},
|
|
4477
|
-
summaryArchives: [],
|
|
4478
|
-
turnTimings: [],
|
|
4479
|
-
subagentStates: {},
|
|
4480
|
-
selfSummaryCount: 0,
|
|
4481
|
-
readPaths: []
|
|
4482
|
-
});
|
|
4483
|
-
const conversationState = create(ConversationStateStructureSchema, {
|
|
4484
|
-
...baseState,
|
|
4485
|
-
rootPromptMessagesJson,
|
|
4486
|
-
turns
|
|
4487
|
-
});
|
|
4488
|
-
const { modelId: wireModelId, parameters: wireParameters } = resolveCursorWireModel(model, options?.wireModelId, wireMode);
|
|
4489
|
-
const cursorMaxMode = model.cursorMaxMode === true;
|
|
4490
|
-
const modelDetails = create(ModelDetailsSchema, {
|
|
4491
|
-
modelId: wireModelId,
|
|
4492
|
-
displayModelId: model.id,
|
|
4493
|
-
displayName: model.name,
|
|
4494
|
-
...cursorMaxMode ? { maxMode: true } : void 0
|
|
4495
|
-
});
|
|
4496
|
-
const requestedModel = create(RequestedModelSchema, {
|
|
4497
|
-
modelId: wireModelId,
|
|
4498
|
-
maxMode: cursorMaxMode,
|
|
4499
|
-
parameters: wireParameters
|
|
4500
|
-
});
|
|
4501
|
-
let runRequest = create(AgentRunRequestSchema, {
|
|
4502
|
-
conversationState,
|
|
4503
|
-
action,
|
|
4504
|
-
modelDetails,
|
|
4505
|
-
requestedModel,
|
|
4506
|
-
conversationId: state.conversationId
|
|
4507
|
-
});
|
|
4508
|
-
if (options?.customSystemPrompt) runRequest.customSystemPrompt = options.customSystemPrompt;
|
|
4509
|
-
const replacementRequest = await options?.onPayload?.(runRequest, model);
|
|
4510
|
-
if (replacementRequest !== void 0) runRequest = replacementRequest;
|
|
4511
|
-
const discoveredWireModelId = options?.wireModelId ?? model.requestModelId ?? model.id;
|
|
4512
|
-
const serializedParameters = runRequest.requestedModel?.parameters;
|
|
4513
|
-
const fallbackWireModelId = wireMode === "normalized" && wireParameters.length > 0 && wireModelId !== discoveredWireModelId && runRequest.requestedModel?.modelId === wireModelId && runRequest.modelDetails?.modelId === wireModelId && serializedParameters?.length === wireParameters.length && serializedParameters.every((parameter, index) => {
|
|
4514
|
-
const expected = wireParameters[index];
|
|
4515
|
-
return expected !== void 0 && parameter.id === expected.id && parameter.value === expected.value;
|
|
4516
|
-
}) ? discoveredWireModelId : void 0;
|
|
4517
|
-
const clientMessage = create(AgentClientMessageSchema, { message: {
|
|
4518
|
-
case: "runRequest",
|
|
4519
|
-
value: runRequest
|
|
4520
|
-
} });
|
|
4521
|
-
const requestBytes = toBinary(AgentClientMessageSchema, clientMessage);
|
|
4522
|
-
const toolNames = context.tools?.map((tool) => tool.name) ?? [];
|
|
4523
|
-
const detail = $env.DEBUG_CURSOR === "2" ? ` ${JSON.stringify(clientMessage.message.value, debugReplacer, 2)?.slice(0, 2e3)}` : "";
|
|
4524
|
-
log("info", "builtRunRequest", {
|
|
4525
|
-
bytes: requestBytes.length,
|
|
4526
|
-
tools: toolNames.length,
|
|
4527
|
-
toolNames: toolNames.slice(0, 20),
|
|
4528
|
-
detail: detail || void 0
|
|
4529
|
-
});
|
|
4530
|
-
return {
|
|
4531
|
-
requestBytes,
|
|
4532
|
-
blobStore,
|
|
4533
|
-
conversationState,
|
|
4534
|
-
fallbackWireModelId
|
|
4535
|
-
};
|
|
4536
|
-
}
|
|
4537
|
-
function hasImages(content) {
|
|
4538
|
-
return content.some((item) => item.type === "image");
|
|
4539
|
-
}
|
|
4540
|
-
function extractText(content) {
|
|
4541
|
-
return content.filter((c) => c.type === "text").map((c) => c.text).join("\n");
|
|
4542
|
-
}
|
|
4543
|
-
//#endregion
|
|
4544
|
-
export { streamCursor };
|