@oh-my-pi/pi-ai 17.4.0 → 17.4.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +30 -0
- package/LICENSE +23 -0
- package/THIRD-PARTY-NOTICES.txt +22909 -0
- package/dist/types/auth-retry.d.ts +8 -3
- package/dist/types/auth-storage.d.ts +9 -0
- package/dist/types/dialect/qwen-xml.d.ts +6 -0
- package/dist/types/error/auth-classify.d.ts +8 -8
- package/dist/types/error/oauth.d.ts +2 -1
- package/dist/types/providers/cursor/exec-modern.d.ts +1 -1
- package/dist/types/providers/cursor-pi-args.d.ts +21 -0
- package/dist/types/providers/cursor.d.ts +23 -3
- package/dist/types/providers/google-types.d.ts +6 -0
- package/dist/types/providers/mock.d.ts +3 -1
- package/dist/types/providers/openai-shared.d.ts +6 -1
- package/dist/types/registry/oauth/google-antigravity.d.ts +2 -6
- package/dist/types/registry/oauth/index.d.ts +2 -3
- package/dist/types/registry/oauth/types.d.ts +2 -1
- package/dist/types/registry/oauth/xai-oauth.d.ts +2 -2
- package/dist/types/registry/registry.d.ts +1 -1
- package/dist/types/registry/types.d.ts +2 -1
- package/dist/types/registry/xai-oauth.d.ts +1 -1
- package/dist/types/types.d.ts +21 -0
- package/dist/types/utils/stream-markup-healing.d.ts +1 -1
- package/package.json +8 -6
- package/src/auth-retry.ts +41 -3
- package/src/auth-storage.ts +229 -75
- package/src/dialect/qwen-xml.ts +82 -0
- package/src/error/auth-classify.ts +10 -8
- package/src/error/oauth.ts +2 -1
- package/src/providers/amazon-bedrock.ts +12 -19
- package/src/providers/anthropic.ts +29 -35
- package/src/providers/cursor/exec-modern.ts +2 -0
- package/src/providers/cursor-pi-args.ts +38 -2
- package/src/providers/cursor.ts +357 -32
- package/src/providers/google-shared.ts +11 -12
- package/src/providers/google-types.ts +7 -0
- package/src/providers/mock.ts +4 -1
- package/src/providers/openai-codex-responses.ts +34 -8
- package/src/providers/openai-completions.ts +2 -2
- package/src/providers/openai-shared.ts +72 -14
- package/src/registry/oauth/google-antigravity.ts +247 -162
- package/src/registry/oauth/index.ts +3 -3
- package/src/registry/oauth/types.ts +2 -1
- package/src/registry/oauth/xai-oauth.ts +14 -7
- package/src/registry/types.ts +2 -1
- package/src/registry/xai-oauth.ts +2 -2
- package/src/stream.ts +8 -4
- package/src/types.ts +22 -0
- package/src/utils/schema/CONSTRAINTS.md +1 -0
- package/src/utils/schema/fields.ts +20 -4
- package/src/utils/stream-markup-healing.ts +9 -2
package/src/providers/cursor.ts
CHANGED
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import * as fs from "node:fs/promises";
|
|
3
3
|
import http2 from "node:http2";
|
|
4
|
-
import type {
|
|
4
|
+
import type {
|
|
5
|
+
ConversationStep,
|
|
6
|
+
CursorRule,
|
|
7
|
+
McpToolDefinition,
|
|
8
|
+
RequestedModel_ModelParameterbytes,
|
|
9
|
+
} from "@oh-my-pi/pi-catalog/discovery/cursor-proto";
|
|
5
10
|
import {
|
|
6
11
|
AgentClientMessageSchema,
|
|
7
12
|
AgentConversationTurnStructureSchema,
|
|
@@ -24,6 +29,10 @@ import {
|
|
|
24
29
|
ConversationStateStructureSchema,
|
|
25
30
|
ConversationStepSchema,
|
|
26
31
|
ConversationTurnStructureSchema,
|
|
32
|
+
CursorRuleSchema,
|
|
33
|
+
CursorRuleSource,
|
|
34
|
+
CursorRuleTypeGlobalSchema,
|
|
35
|
+
CursorRuleTypeSchema,
|
|
27
36
|
DeleteErrorSchema,
|
|
28
37
|
DeleteRejectedSchema,
|
|
29
38
|
DeleteResultSchema,
|
|
@@ -102,6 +111,7 @@ import {
|
|
|
102
111
|
RequestContextResultSchema,
|
|
103
112
|
RequestContextSchema,
|
|
104
113
|
RequestContextSuccessSchema,
|
|
114
|
+
RequestedModel_ModelParameterbytesSchema,
|
|
105
115
|
RequestedModelSchema,
|
|
106
116
|
ResumeActionSchema,
|
|
107
117
|
SelectedContextSchema,
|
|
@@ -147,7 +157,8 @@ import {
|
|
|
147
157
|
toBinary,
|
|
148
158
|
toJson,
|
|
149
159
|
} from "@oh-my-pi/pi-catalog/discovery/protobuf";
|
|
150
|
-
import {
|
|
160
|
+
import { THINKING_EFFORTS } from "@oh-my-pi/pi-catalog/effort";
|
|
161
|
+
import { isKimiK3ModelId, parseOpenAIModel } from "@oh-my-pi/pi-catalog/identity";
|
|
151
162
|
import { calculateCost } from "@oh-my-pi/pi-catalog/models";
|
|
152
163
|
import {
|
|
153
164
|
$env,
|
|
@@ -196,7 +207,7 @@ import {
|
|
|
196
207
|
} from "../utils/block-symbols";
|
|
197
208
|
import { deterministicUuid } from "../utils/deterministic-id";
|
|
198
209
|
import { AssistantMessageEventStream } from "../utils/event-stream";
|
|
199
|
-
import { connectProxiedSocket,
|
|
210
|
+
import { connectProxiedSocket, getProxyForUrl } from "../utils/proxy";
|
|
200
211
|
import { createRequestDebugSession, isRequestDebugEnabled, type RequestDebugResponseLog } from "../utils/request-debug";
|
|
201
212
|
import { toolWireSchema } from "../utils/schema/wire";
|
|
202
213
|
import {
|
|
@@ -218,6 +229,7 @@ import {
|
|
|
218
229
|
buildPiWriteError,
|
|
219
230
|
buildPiWriteRejected,
|
|
220
231
|
buildPiWriteResult,
|
|
232
|
+
cursorEditOwnedReadPath,
|
|
221
233
|
omitUndefinedArgs,
|
|
222
234
|
piEscapeRegexLiteral,
|
|
223
235
|
piGrepSkip,
|
|
@@ -325,6 +337,8 @@ export interface CursorOptions extends StreamOptions {
|
|
|
325
337
|
conversationId?: string;
|
|
326
338
|
execHandlers?: CursorExecHandlers;
|
|
327
339
|
onToolResult?: CursorToolResultHandler;
|
|
340
|
+
/** Wire model id selected after thinking-effort routing (`resolveWireModelId`). */
|
|
341
|
+
wireModelId?: string;
|
|
328
342
|
}
|
|
329
343
|
|
|
330
344
|
const CONNECT_END_STREAM_FLAG = 0b00000010;
|
|
@@ -631,6 +645,7 @@ export const streamCursor: StreamFunction<"cursor-agent"> = (
|
|
|
631
645
|
});
|
|
632
646
|
conversationStateCache.set(conversationId, conversationState);
|
|
633
647
|
const requestContextTools = buildMcpToolDefinitions(context.tools);
|
|
648
|
+
const requestContextRules = buildCursorRequestContextRules(context.systemPrompt);
|
|
634
649
|
|
|
635
650
|
const baseUrl = model.baseUrl || CURSOR_API_URL;
|
|
636
651
|
const requestPath = "/agent.v1.AgentService/Run";
|
|
@@ -671,7 +686,7 @@ export const streamCursor: StreamFunction<"cursor-agent"> = (
|
|
|
671
686
|
})
|
|
672
687
|
: undefined;
|
|
673
688
|
|
|
674
|
-
const proxyUrl =
|
|
689
|
+
const proxyUrl = getProxyForUrl(model.provider, new URL(baseUrl));
|
|
675
690
|
if (proxyUrl) {
|
|
676
691
|
const tlsSocket = await connectProxiedSocket(proxyUrl, baseUrl, {
|
|
677
692
|
signal: options?.signal,
|
|
@@ -788,6 +803,7 @@ export const streamCursor: StreamFunction<"cursor-agent"> = (
|
|
|
788
803
|
options?.onToolResult,
|
|
789
804
|
usageState!,
|
|
790
805
|
requestContextTools,
|
|
806
|
+
requestContextRules,
|
|
791
807
|
onConversationCheckpoint,
|
|
792
808
|
).catch(error => {
|
|
793
809
|
log("error", "handleServerMessage", { error: String(error) });
|
|
@@ -949,7 +965,7 @@ export type ToolCallState = ToolCall & {
|
|
|
949
965
|
[kStreamingBlockIndex]: number;
|
|
950
966
|
[kStreamingPartialJson]?: string;
|
|
951
967
|
[kStreamingLastParseLen]?: number;
|
|
952
|
-
[kStreamingBlockKind]: "mcp" | "todo" | "cursor-exec" | "connect-scm" | "web-fetch";
|
|
968
|
+
[kStreamingBlockKind]: "mcp" | "todo" | "cursor-exec" | "cursor-edit" | "connect-scm" | "web-fetch";
|
|
953
969
|
[kStreamingEnvelopeId]?: string;
|
|
954
970
|
[kCursorExecResolved]?: true;
|
|
955
971
|
};
|
|
@@ -971,6 +987,17 @@ export interface BlockState {
|
|
|
971
987
|
openToolCalls: Map<string, ToolCallState>;
|
|
972
988
|
/** MCP call IDs synthesized from exec frames before their redundant streamed block arrives. */
|
|
973
989
|
resolvedMcpToolCallIds: Set<string>;
|
|
990
|
+
/**
|
|
991
|
+
* Native `editToolCall` (StrReplace) ids whose materialization `readArgs` /
|
|
992
|
+
* `writeArgs` must stay raw and must not synthesize extra transcript blocks.
|
|
993
|
+
*
|
|
994
|
+
* Optional so existing test harnesses stay valid. Both the interaction
|
|
995
|
+
* envelope `call_id` and the inner `toolCallId` are recorded — exec frames
|
|
996
|
+
* pair on the inner id.
|
|
997
|
+
*/
|
|
998
|
+
editOwnedToolCallIds?: Set<string>;
|
|
999
|
+
/** Edit blocks whose write already persisted a `toolResult`. */
|
|
1000
|
+
pairedEditToolCallIds?: Set<string>;
|
|
974
1001
|
firstTokenTime: number | undefined;
|
|
975
1002
|
setTextBlock: (b: (TextContent & { [kStreamingBlockIndex]: number }) | null) => void;
|
|
976
1003
|
setThinkingBlock: (b: (ThinkingContent & { [kStreamingBlockIndex]: number }) | null) => void;
|
|
@@ -1006,6 +1033,7 @@ export async function handleServerMessage(
|
|
|
1006
1033
|
onToolResult: CursorToolResultHandler | undefined,
|
|
1007
1034
|
usageState: UsageState,
|
|
1008
1035
|
requestContextTools: McpToolDefinition[],
|
|
1036
|
+
requestContextRules: CursorRule[] = [],
|
|
1009
1037
|
onConversationCheckpoint?: (checkpoint: ConversationStateStructure) => void,
|
|
1010
1038
|
): Promise<void> {
|
|
1011
1039
|
const msgCase = msg.message.case;
|
|
@@ -1028,6 +1056,7 @@ export async function handleServerMessage(
|
|
|
1028
1056
|
execHandlers,
|
|
1029
1057
|
onToolResult,
|
|
1030
1058
|
requestContextTools,
|
|
1059
|
+
requestContextRules,
|
|
1031
1060
|
output,
|
|
1032
1061
|
stream,
|
|
1033
1062
|
state,
|
|
@@ -1445,6 +1474,7 @@ async function handleExecServerMessage(
|
|
|
1445
1474
|
execHandlers: CursorExecHandlers | undefined,
|
|
1446
1475
|
onToolResult: CursorToolResultHandler | undefined,
|
|
1447
1476
|
requestContextTools: McpToolDefinition[],
|
|
1477
|
+
requestContextRules: CursorRule[],
|
|
1448
1478
|
output: AssistantMessage,
|
|
1449
1479
|
stream: AssistantMessageEventStream,
|
|
1450
1480
|
state: BlockState,
|
|
@@ -1453,7 +1483,7 @@ async function handleExecServerMessage(
|
|
|
1453
1483
|
log("exec", "dispatch", { execCase, execId: execMsg.execId, hasHandlers: !!execHandlers });
|
|
1454
1484
|
if (execCase === "requestContextArgs") {
|
|
1455
1485
|
const requestContext = create(RequestContextSchema, {
|
|
1456
|
-
rules:
|
|
1486
|
+
rules: requestContextRules,
|
|
1457
1487
|
repositoryInfo: [],
|
|
1458
1488
|
tools: requestContextTools,
|
|
1459
1489
|
gitRepos: [],
|
|
@@ -1491,16 +1521,30 @@ async function handleExecServerMessage(
|
|
|
1491
1521
|
case "readArgs": {
|
|
1492
1522
|
const args = execMsg.message.value;
|
|
1493
1523
|
if (!args.toolCallId) args.toolCallId = crypto.randomUUID();
|
|
1494
|
-
|
|
1495
|
-
//
|
|
1496
|
-
// file
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1524
|
+
const editOwned = isEditOwnedToolCallId(state, output, args.toolCallId);
|
|
1525
|
+
// Native StrReplace materializes by reading then writing the same
|
|
1526
|
+
// toolCallId. The server treats the read result as file bytes, so a
|
|
1527
|
+
// hashline-formatted native read would be written back as markup.
|
|
1528
|
+
// Force `:raw` and skip the extra transcript block — the edit card
|
|
1529
|
+
// already owns this id.
|
|
1530
|
+
const composed = editOwned ? cursorEditOwnedReadPath(args.path, args.offset, args.limit) : args.path;
|
|
1531
|
+
const handlerArgs = editOwned
|
|
1532
|
+
? composed === null
|
|
1533
|
+
? args
|
|
1534
|
+
: { ...args, path: composed, offset: undefined, limit: undefined }
|
|
1535
|
+
: args;
|
|
1536
|
+
if (!editOwned) {
|
|
1537
|
+
// The same composed selector the bridge executes: showing a bare path
|
|
1538
|
+
// for a ranged read makes the returned slice look like the whole
|
|
1539
|
+
// file in every rebuilt transcript.
|
|
1540
|
+
synthesizeCursorExecToolCall(output, stream, state, args.toolCallId, "read", {
|
|
1541
|
+
path: piReadDisplayPath(args.path, args.offset, args.limit),
|
|
1542
|
+
});
|
|
1543
|
+
}
|
|
1500
1544
|
const { execResult } = await resolveExecHandler(
|
|
1501
|
-
|
|
1545
|
+
handlerArgs,
|
|
1502
1546
|
execHandlers?.read?.bind(execHandlers),
|
|
1503
|
-
onToolResult,
|
|
1547
|
+
editOwned ? undefined : onToolResult,
|
|
1504
1548
|
toolResult =>
|
|
1505
1549
|
buildReadResultFromToolResult(
|
|
1506
1550
|
args.path,
|
|
@@ -1509,7 +1553,7 @@ async function handleExecServerMessage(
|
|
|
1509
1553
|
),
|
|
1510
1554
|
reason => buildReadRejectedResult(args.path, reason),
|
|
1511
1555
|
error => buildReadErrorResult(args.path, error),
|
|
1512
|
-
{ toolCallId: args.toolCallId, toolName: "read" },
|
|
1556
|
+
editOwned ? null : { toolCallId: args.toolCallId, toolName: "read" },
|
|
1513
1557
|
);
|
|
1514
1558
|
sendExecClientMessage(h2Request, execMsg, "readResult", execResult);
|
|
1515
1559
|
return;
|
|
@@ -1572,15 +1616,25 @@ async function handleExecServerMessage(
|
|
|
1572
1616
|
case "writeArgs": {
|
|
1573
1617
|
const args = execMsg.message.value;
|
|
1574
1618
|
if (!args.toolCallId) args.toolCallId = crypto.randomUUID();
|
|
1619
|
+
const editOwned = isEditOwnedToolCallId(state, output, args.toolCallId);
|
|
1575
1620
|
// Match the bridge: prefer `fileText`, fall back to decoded `fileBytes`.
|
|
1576
1621
|
const content = args.fileText ?? new TextDecoder().decode(args.fileBytes ?? new Uint8Array());
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1622
|
+
if (!editOwned) {
|
|
1623
|
+
synthesizeCursorExecToolCall(output, stream, state, args.toolCallId, "write", {
|
|
1624
|
+
path: args.path,
|
|
1625
|
+
content,
|
|
1626
|
+
});
|
|
1627
|
+
}
|
|
1628
|
+
const write = execHandlers?.write?.bind(execHandlers);
|
|
1629
|
+
const writeHandler = write
|
|
1630
|
+
? async (writeArgs: typeof args) => {
|
|
1631
|
+
const result = await write(writeArgs);
|
|
1632
|
+
return editOwned ? remapExecHandlerToolName(result, "edit") : result;
|
|
1633
|
+
}
|
|
1634
|
+
: undefined;
|
|
1581
1635
|
const { execResult } = await resolveExecHandler(
|
|
1582
1636
|
args,
|
|
1583
|
-
|
|
1637
|
+
writeHandler,
|
|
1584
1638
|
onToolResult,
|
|
1585
1639
|
toolResult =>
|
|
1586
1640
|
buildWriteResultFromToolResult(
|
|
@@ -1594,8 +1648,9 @@ async function handleExecServerMessage(
|
|
|
1594
1648
|
),
|
|
1595
1649
|
reason => buildWriteRejectedResult(args.path, reason),
|
|
1596
1650
|
error => buildWriteErrorResult(args.path, error),
|
|
1597
|
-
{ toolCallId: args.toolCallId, toolName: "write" },
|
|
1651
|
+
{ toolCallId: args.toolCallId, toolName: editOwned ? "edit" : "write" },
|
|
1598
1652
|
);
|
|
1653
|
+
if (editOwned) markEditToolCallPaired(state, args.toolCallId);
|
|
1599
1654
|
sendExecClientMessage(h2Request, execMsg, "writeResult", execResult);
|
|
1600
1655
|
return;
|
|
1601
1656
|
}
|
|
@@ -3256,6 +3311,174 @@ function selectMcpCall(toolCall: CursorMcpToolCallCarrier | undefined): CursorMc
|
|
|
3256
3311
|
return toolCall?.mcpToolCall;
|
|
3257
3312
|
}
|
|
3258
3313
|
|
|
3314
|
+
interface CursorEditToolCall {
|
|
3315
|
+
args?: {
|
|
3316
|
+
path?: string;
|
|
3317
|
+
streamContent?: string;
|
|
3318
|
+
};
|
|
3319
|
+
result?: {
|
|
3320
|
+
result?: {
|
|
3321
|
+
case?: string;
|
|
3322
|
+
value?: {
|
|
3323
|
+
error?: string;
|
|
3324
|
+
reason?: string;
|
|
3325
|
+
message?: string;
|
|
3326
|
+
path?: string;
|
|
3327
|
+
modelVisibleError?: string;
|
|
3328
|
+
};
|
|
3329
|
+
};
|
|
3330
|
+
};
|
|
3331
|
+
}
|
|
3332
|
+
|
|
3333
|
+
interface CursorEditToolCallCarrier {
|
|
3334
|
+
tool?: { case?: string; value?: unknown };
|
|
3335
|
+
toolCallId?: string;
|
|
3336
|
+
editToolCall?: CursorEditToolCall;
|
|
3337
|
+
}
|
|
3338
|
+
|
|
3339
|
+
/**
|
|
3340
|
+
* Same oneof-first selector as MCP: a wire-decoded `ToolCall` exposes
|
|
3341
|
+
* `editToolCall` only as `{ case: "editToolCall", value }`.
|
|
3342
|
+
*/
|
|
3343
|
+
function selectEditCall(toolCall: CursorEditToolCallCarrier | undefined): CursorEditToolCall | undefined {
|
|
3344
|
+
const oneof = toolCall?.tool;
|
|
3345
|
+
if (oneof?.case === "editToolCall") return oneof.value as CursorEditToolCall;
|
|
3346
|
+
return toolCall?.editToolCall;
|
|
3347
|
+
}
|
|
3348
|
+
|
|
3349
|
+
function selectEditStreamDelta(update: {
|
|
3350
|
+
toolCallDelta?: {
|
|
3351
|
+
delta?: { case?: string; value?: { streamContentDelta?: string } };
|
|
3352
|
+
editToolCallDelta?: { streamContentDelta?: string };
|
|
3353
|
+
};
|
|
3354
|
+
}): string | undefined {
|
|
3355
|
+
const oneof = update.toolCallDelta?.delta;
|
|
3356
|
+
if (oneof?.case === "editToolCallDelta") return oneof.value?.streamContentDelta;
|
|
3357
|
+
return update.toolCallDelta?.editToolCallDelta?.streamContentDelta;
|
|
3358
|
+
}
|
|
3359
|
+
|
|
3360
|
+
function rememberEditOwnedToolCall(
|
|
3361
|
+
state: BlockState,
|
|
3362
|
+
toolCall: CursorEditToolCallCarrier | undefined,
|
|
3363
|
+
envelopeId?: string,
|
|
3364
|
+
): void {
|
|
3365
|
+
if (!state.editOwnedToolCallIds) state.editOwnedToolCallIds = new Set();
|
|
3366
|
+
const ids = state.editOwnedToolCallIds;
|
|
3367
|
+
if (envelopeId) ids.add(envelopeId);
|
|
3368
|
+
if (toolCall?.toolCallId) ids.add(toolCall.toolCallId);
|
|
3369
|
+
}
|
|
3370
|
+
|
|
3371
|
+
function isEditOwnedToolCallId(state: BlockState, output: AssistantMessage, toolCallId: string): boolean {
|
|
3372
|
+
if (state.editOwnedToolCallIds?.has(toolCallId)) return true;
|
|
3373
|
+
return output.content.some(block => block.type === "toolCall" && block.id === toolCallId && block.name === "edit");
|
|
3374
|
+
}
|
|
3375
|
+
|
|
3376
|
+
function stringToolArg(args: Record<string, unknown> | undefined, key: string): string | undefined {
|
|
3377
|
+
const value = args?.[key];
|
|
3378
|
+
return typeof value === "string" ? value : undefined;
|
|
3379
|
+
}
|
|
3380
|
+
|
|
3381
|
+
function markEditToolCallPaired(state: BlockState, toolCallId: string): void {
|
|
3382
|
+
if (!state.pairedEditToolCallIds) state.pairedEditToolCallIds = new Set();
|
|
3383
|
+
state.pairedEditToolCallIds.add(toolCallId);
|
|
3384
|
+
}
|
|
3385
|
+
|
|
3386
|
+
/**
|
|
3387
|
+
* `EditToolCall.result` is an `EditResult` message whose own oneof is also
|
|
3388
|
+
* named `result`. The discriminator is `result.result.case`, same nesting as
|
|
3389
|
+
* `describeConnectScmResult` / the todo extractors.
|
|
3390
|
+
*/
|
|
3391
|
+
function describeEditResult(toolCall: CursorEditToolCallCarrier | undefined): { text: string; isError: boolean } {
|
|
3392
|
+
const oneof = selectEditCall(toolCall)?.result?.result;
|
|
3393
|
+
const variant = oneof?.case;
|
|
3394
|
+
const value = oneof?.value;
|
|
3395
|
+
if (variant === "success") {
|
|
3396
|
+
return { text: value?.message || value?.path || "Edited", isError: false };
|
|
3397
|
+
}
|
|
3398
|
+
if (variant === "error") {
|
|
3399
|
+
return { text: value?.modelVisibleError || value?.error || "Edit failed", isError: true };
|
|
3400
|
+
}
|
|
3401
|
+
if (variant === "rejected") {
|
|
3402
|
+
return { text: value?.reason || "Edit rejected", isError: true };
|
|
3403
|
+
}
|
|
3404
|
+
if (variant === "fileNotFound") {
|
|
3405
|
+
return { text: value?.path ? `File not found: ${value.path}` : "File not found", isError: true };
|
|
3406
|
+
}
|
|
3407
|
+
if (variant === "readPermissionDenied") {
|
|
3408
|
+
return { text: value?.path ? `Read permission denied: ${value.path}` : "Read permission denied", isError: true };
|
|
3409
|
+
}
|
|
3410
|
+
if (variant === "writePermissionDenied") {
|
|
3411
|
+
return {
|
|
3412
|
+
text: value?.error || (value?.path ? `Write permission denied: ${value.path}` : "Write permission denied"),
|
|
3413
|
+
isError: true,
|
|
3414
|
+
};
|
|
3415
|
+
}
|
|
3416
|
+
return { text: "Edit reported no result", isError: true };
|
|
3417
|
+
}
|
|
3418
|
+
|
|
3419
|
+
function remapExecHandlerToolName<TResult>(
|
|
3420
|
+
result: CursorExecHandlerResult<TResult>,
|
|
3421
|
+
toolName: string,
|
|
3422
|
+
): CursorExecHandlerResult<TResult> {
|
|
3423
|
+
if (isToolResultMessage(result)) return { ...result, toolName };
|
|
3424
|
+
if (result && typeof result === "object" && "toolResult" in result) {
|
|
3425
|
+
const record = result as { result?: TResult; toolResult?: ToolResultMessage };
|
|
3426
|
+
if (record.toolResult && record.result !== undefined) {
|
|
3427
|
+
return { result: record.result, toolResult: { ...record.toolResult, toolName } };
|
|
3428
|
+
}
|
|
3429
|
+
if (record.toolResult) return { ...record.toolResult, toolName };
|
|
3430
|
+
}
|
|
3431
|
+
return result;
|
|
3432
|
+
}
|
|
3433
|
+
|
|
3434
|
+
/**
|
|
3435
|
+
* Open (or refresh) the single `edit` transcript block for a native StrReplace
|
|
3436
|
+
* `editToolCall`. Materialization reads/writes reuse this id and must not
|
|
3437
|
+
* synthesize their own blocks.
|
|
3438
|
+
*/
|
|
3439
|
+
function openOrUpdateEditBlock(
|
|
3440
|
+
output: AssistantMessage,
|
|
3441
|
+
stream: AssistantMessageEventStream,
|
|
3442
|
+
state: BlockState,
|
|
3443
|
+
toolCall: CursorEditToolCallCarrier | undefined,
|
|
3444
|
+
envelopeId?: string,
|
|
3445
|
+
): ToolCallState | undefined {
|
|
3446
|
+
const edit = selectEditCall(toolCall);
|
|
3447
|
+
if (!edit && toolCall?.tool?.case !== "editToolCall") return undefined;
|
|
3448
|
+
rememberEditOwnedToolCall(state, toolCall, envelopeId);
|
|
3449
|
+
const id = toolCall?.toolCallId || envelopeId;
|
|
3450
|
+
if (!id) return undefined;
|
|
3451
|
+
|
|
3452
|
+
const nextArgs = omitUndefinedArgs({
|
|
3453
|
+
path: edit?.args?.path,
|
|
3454
|
+
stream_content: edit?.args?.streamContent,
|
|
3455
|
+
});
|
|
3456
|
+
const existing = output.content.find(
|
|
3457
|
+
(block): block is ToolCallState => block.type === "toolCall" && block.id === id,
|
|
3458
|
+
);
|
|
3459
|
+
if (existing) {
|
|
3460
|
+
existing.arguments = { ...existing.arguments, ...nextArgs };
|
|
3461
|
+
return existing;
|
|
3462
|
+
}
|
|
3463
|
+
|
|
3464
|
+
endCurrentTextBlock(output, stream, state);
|
|
3465
|
+
endCurrentThinkingBlock(output, stream, state);
|
|
3466
|
+
const block: ToolCallState = {
|
|
3467
|
+
type: "toolCall",
|
|
3468
|
+
id,
|
|
3469
|
+
name: "edit",
|
|
3470
|
+
arguments: nextArgs,
|
|
3471
|
+
[kStreamingBlockIndex]: output.content.length,
|
|
3472
|
+
[kStreamingBlockKind]: "cursor-edit",
|
|
3473
|
+
[kStreamingEnvelopeId]: envelopeId || undefined,
|
|
3474
|
+
[kCursorExecResolved]: true,
|
|
3475
|
+
};
|
|
3476
|
+
output.content.push(block);
|
|
3477
|
+
retainStreamedCall(state, block, envelopeId);
|
|
3478
|
+
stream.push({ type: "toolcall_start", contentIndex: output.content.length - 1, partial: output });
|
|
3479
|
+
return block;
|
|
3480
|
+
}
|
|
3481
|
+
|
|
3259
3482
|
/**
|
|
3260
3483
|
* The streamed `ToolCall` variants whose block the exec channel owns.
|
|
3261
3484
|
*
|
|
@@ -3340,15 +3563,17 @@ export function flushOpenToolCalls(
|
|
|
3340
3563
|
clearStreamingPartialJson(block);
|
|
3341
3564
|
}
|
|
3342
3565
|
const kind = block[kStreamingBlockKind];
|
|
3343
|
-
if (kind === "connect-scm" || kind === "todo") {
|
|
3344
|
-
state.
|
|
3345
|
-
|
|
3346
|
-
|
|
3347
|
-
|
|
3348
|
-
|
|
3349
|
-
|
|
3350
|
-
|
|
3351
|
-
|
|
3566
|
+
if (kind === "connect-scm" || kind === "todo" || kind === "cursor-edit") {
|
|
3567
|
+
if (!(kind === "cursor-edit" && state.pairedEditToolCallIds?.has(block.id))) {
|
|
3568
|
+
state.onToolResult?.({
|
|
3569
|
+
role: "toolResult",
|
|
3570
|
+
toolCallId: block.id,
|
|
3571
|
+
toolName: block.name,
|
|
3572
|
+
content: [{ type: "text", text: "The connection to Cursor closed before this call completed." }],
|
|
3573
|
+
isError: true,
|
|
3574
|
+
timestamp: Date.now(),
|
|
3575
|
+
});
|
|
3576
|
+
}
|
|
3352
3577
|
}
|
|
3353
3578
|
stream.push({ type: "toolcall_end", contentIndex: idx, toolCall: block, partial: output });
|
|
3354
3579
|
}
|
|
@@ -3991,13 +4216,31 @@ export function processInteractionUpdate(
|
|
|
3991
4216
|
output.content.push(block);
|
|
3992
4217
|
retainStreamedCall(state, block, update.message.value.callId);
|
|
3993
4218
|
stream.push({ type: "toolcall_start", contentIndex: output.content.length - 1, partial: output });
|
|
4219
|
+
return;
|
|
3994
4220
|
}
|
|
4221
|
+
|
|
4222
|
+
openOrUpdateEditBlock(output, stream, state, toolCall, update.message.value.callId);
|
|
3995
4223
|
}
|
|
3996
4224
|
} else if (updateCase === "toolCallDelta" || updateCase === "partialToolCall") {
|
|
4225
|
+
const value = update.message.value;
|
|
4226
|
+
if (updateCase === "partialToolCall" && value.toolCall) {
|
|
4227
|
+
openOrUpdateEditBlock(output, stream, state, value.toolCall, value.callId);
|
|
4228
|
+
}
|
|
4229
|
+
const editDelta = selectEditStreamDelta(value);
|
|
4230
|
+
if (editDelta) {
|
|
4231
|
+
const target = resolveStreamedCall(state, value.callId);
|
|
4232
|
+
if (target?.[kStreamingBlockKind] === "cursor-edit") {
|
|
4233
|
+
const current = stringToolArg(target.arguments, "stream_content") ?? "";
|
|
4234
|
+
target.arguments = { ...target.arguments, stream_content: current + editDelta };
|
|
4235
|
+
const idx = output.content.indexOf(target);
|
|
4236
|
+
stream.push({ type: "toolcall_delta", contentIndex: idx, delta: editDelta, partial: output });
|
|
4237
|
+
return;
|
|
4238
|
+
}
|
|
4239
|
+
}
|
|
3997
4240
|
// Same correlation rule as the completion path below: an argument delta
|
|
3998
4241
|
// belonging to a different call must not be appended to this block's
|
|
3999
4242
|
// buffer, which would corrupt the JSON both of them parse.
|
|
4000
|
-
const target = resolveStreamedCall(state,
|
|
4243
|
+
const target = resolveStreamedCall(state, value.callId);
|
|
4001
4244
|
if (target?.[kStreamingBlockKind] === "mcp") {
|
|
4002
4245
|
// Cursor's `args_text_delta` is "aggregated args text so far" per agent.proto: each
|
|
4003
4246
|
// delta is a cumulative snapshot of the JSON-text args. Strip the prefix we already
|
|
@@ -4122,6 +4365,26 @@ export function processInteractionUpdate(
|
|
|
4122
4365
|
log("error", "onTodoSnapshot", { error: hostError });
|
|
4123
4366
|
}
|
|
4124
4367
|
state.onToolResult?.(persisted ?? buildTodoToolResult(settled.id, snapshot, hostError ?? error));
|
|
4368
|
+
} else if (settled[kStreamingBlockKind] === "cursor-edit") {
|
|
4369
|
+
const edit = selectEditCall(toolCall);
|
|
4370
|
+
if (edit?.args) {
|
|
4371
|
+
settled.arguments = omitUndefinedArgs({
|
|
4372
|
+
...settled.arguments,
|
|
4373
|
+
path: edit.args.path ?? stringToolArg(settled.arguments, "path"),
|
|
4374
|
+
stream_content: edit.args.streamContent ?? stringToolArg(settled.arguments, "stream_content"),
|
|
4375
|
+
});
|
|
4376
|
+
}
|
|
4377
|
+
if (!state.pairedEditToolCallIds?.has(settled.id)) {
|
|
4378
|
+
const { text, isError } = describeEditResult(toolCall);
|
|
4379
|
+
state.onToolResult?.({
|
|
4380
|
+
role: "toolResult",
|
|
4381
|
+
toolCallId: settled.id,
|
|
4382
|
+
toolName: "edit",
|
|
4383
|
+
content: [{ type: "text", text }],
|
|
4384
|
+
isError,
|
|
4385
|
+
timestamp: Date.now(),
|
|
4386
|
+
});
|
|
4387
|
+
}
|
|
4125
4388
|
}
|
|
4126
4389
|
const idx = output.content.indexOf(settled);
|
|
4127
4390
|
clearStreamingPartialJson(settled);
|
|
@@ -4184,6 +4447,28 @@ function readCursorBlob(blobStore: Map<string, Uint8Array>, blobId: Uint8Array):
|
|
|
4184
4447
|
return data;
|
|
4185
4448
|
}
|
|
4186
4449
|
|
|
4450
|
+
/**
|
|
4451
|
+
* Cursor AgentService reconstructs the model prompt from `requestContext.rules`,
|
|
4452
|
+
* not from the client-supplied `rootPromptMessagesJson` system blobs. Map each
|
|
4453
|
+
* OMP system-prompt entry to a global CursorRule so always-apply rules survive
|
|
4454
|
+
* that reconstruction.
|
|
4455
|
+
*/
|
|
4456
|
+
export function buildCursorRequestContextRules(systemPrompt: readonly string[] | undefined): CursorRule[] {
|
|
4457
|
+
return normalizeSystemPrompts(systemPrompt).map((content, index) =>
|
|
4458
|
+
create(CursorRuleSchema, {
|
|
4459
|
+
fullPath: `/omp/system-prompt/${index}.mdc`,
|
|
4460
|
+
content,
|
|
4461
|
+
source: CursorRuleSource.USER,
|
|
4462
|
+
type: create(CursorRuleTypeSchema, {
|
|
4463
|
+
type: {
|
|
4464
|
+
case: "global",
|
|
4465
|
+
value: create(CursorRuleTypeGlobalSchema, {}),
|
|
4466
|
+
},
|
|
4467
|
+
}),
|
|
4468
|
+
}),
|
|
4469
|
+
);
|
|
4470
|
+
}
|
|
4471
|
+
|
|
4187
4472
|
/**
|
|
4188
4473
|
* Local tools Cursor already drives natively over the exec channel, so
|
|
4189
4474
|
* advertising them again as MCP tools would give the model two ways to call the
|
|
@@ -4735,6 +5020,45 @@ function extractImages(content: (TextContent | ImageContent)[]) {
|
|
|
4735
5020
|
);
|
|
4736
5021
|
}
|
|
4737
5022
|
|
|
5023
|
+
/**
|
|
5024
|
+
* Resolve the Cursor Run wire model id and its parameter list.
|
|
5025
|
+
*
|
|
5026
|
+
* Cursor's `GetUsableModels` lists reasoning models as per-effort sibling
|
|
5027
|
+
* slugs (`gpt-5.4-mini-low`, `gpt-5.6-sol-high`), and OMP copies those ids 1:1.
|
|
5028
|
+
* The Run endpoint rejects a sibling slug as the wire `model_id` with
|
|
5029
|
+
* `resource_exhausted` (errorId 528384); the official `cursor-agent` splits the
|
|
5030
|
+
* slug into its base model id plus a `reasoning` effort parameter. Mirror that
|
|
5031
|
+
* for OpenAI-family ids: strip a trailing effort tier and emit
|
|
5032
|
+
* `{ id: "reasoning", value: <effort> }`.
|
|
5033
|
+
*
|
|
5034
|
+
* Non-OpenAI ids pass through unchanged — Cursor-native ids (`composer-*`,
|
|
5035
|
+
* `cursor-grok-*`, `default`) carry no effort suffix, and Claude/other siblings
|
|
5036
|
+
* need additional parameters (`thinking`, `context`) whose per-model values are
|
|
5037
|
+
* not exposed by the decoded `GetUsableModels` schema, so guessing them would
|
|
5038
|
+
* re-trigger 528384.
|
|
5039
|
+
*/
|
|
5040
|
+
function resolveCursorWireModel(
|
|
5041
|
+
model: Model<"cursor-agent">,
|
|
5042
|
+
requestModelId?: string,
|
|
5043
|
+
): {
|
|
5044
|
+
modelId: string;
|
|
5045
|
+
parameters: RequestedModel_ModelParameterbytes[];
|
|
5046
|
+
} {
|
|
5047
|
+
const wireModelId = requestModelId ?? model.requestModelId ?? model.id;
|
|
5048
|
+
// Cursor's fast lane follows the effort token (`-high-fast`), while the
|
|
5049
|
+
// standard lane ends at it (`-high`). Preserve the lane in the base id.
|
|
5050
|
+
const match = /^(.*)-(minimal|low|medium|high|xhigh|max)(-fast)?$/.exec(wireModelId);
|
|
5051
|
+
const base = match?.[1];
|
|
5052
|
+
const effort = match?.[2];
|
|
5053
|
+
if (base && effort && (THINKING_EFFORTS as readonly string[]).includes(effort) && parseOpenAIModel(base) !== null) {
|
|
5054
|
+
return {
|
|
5055
|
+
modelId: `${base}${match[3] ?? ""}`,
|
|
5056
|
+
parameters: [create(RequestedModel_ModelParameterbytesSchema, { id: "reasoning", value: effort })],
|
|
5057
|
+
};
|
|
5058
|
+
}
|
|
5059
|
+
return { modelId: wireModelId, parameters: [] };
|
|
5060
|
+
}
|
|
5061
|
+
|
|
4738
5062
|
export async function buildGrpcRequest(
|
|
4739
5063
|
model: Model<"cursor-agent">,
|
|
4740
5064
|
context: Context,
|
|
@@ -4841,7 +5165,7 @@ export async function buildGrpcRequest(
|
|
|
4841
5165
|
turns,
|
|
4842
5166
|
});
|
|
4843
5167
|
|
|
4844
|
-
const wireModelId = model
|
|
5168
|
+
const { modelId: wireModelId, parameters: wireParameters } = resolveCursorWireModel(model, options?.wireModelId);
|
|
4845
5169
|
const cursorMaxMode = model.cursorMaxMode === true;
|
|
4846
5170
|
const modelDetails = create(ModelDetailsSchema, {
|
|
4847
5171
|
modelId: wireModelId,
|
|
@@ -4852,6 +5176,7 @@ export async function buildGrpcRequest(
|
|
|
4852
5176
|
const requestedModel = create(RequestedModelSchema, {
|
|
4853
5177
|
modelId: wireModelId,
|
|
4854
5178
|
maxMode: cursorMaxMode,
|
|
5179
|
+
parameters: wireParameters,
|
|
4855
5180
|
});
|
|
4856
5181
|
|
|
4857
5182
|
let runRequest = create(AgentRunRequestSchema, {
|
|
@@ -53,6 +53,15 @@ export { normalizeSchemaForGoogle };
|
|
|
53
53
|
|
|
54
54
|
type GoogleApiType = "google-generative-ai" | "google-gemini-cli" | "google-vertex";
|
|
55
55
|
|
|
56
|
+
function convertGoogleImagePart(image: ImageContent): Part {
|
|
57
|
+
if (image.providerFile?.provider === "google" && image.providerFile.uri) {
|
|
58
|
+
return { fileData: { fileUri: image.providerFile.uri, mimeType: image.mimeType } };
|
|
59
|
+
}
|
|
60
|
+
return image.url
|
|
61
|
+
? { fileData: { fileUri: image.url, mimeType: image.mimeType } }
|
|
62
|
+
: { inlineData: { mimeType: image.mimeType, data: image.data } };
|
|
63
|
+
}
|
|
64
|
+
|
|
56
65
|
/**
|
|
57
66
|
* Thinking level for Gemini 3 models. Mirrors Google's `ThinkingLevel` enum values.
|
|
58
67
|
* Defined here (not in any specific provider) so all Google providers can reference it
|
|
@@ -211,12 +220,7 @@ export function convertMessages<T extends GoogleApiType>(model: Model<T>, contex
|
|
|
211
220
|
if (text.trim().length === 0) continue;
|
|
212
221
|
parts.push({ text });
|
|
213
222
|
} else if (supportsImages) {
|
|
214
|
-
parts.push(
|
|
215
|
-
inlineData: {
|
|
216
|
-
mimeType: item.mimeType,
|
|
217
|
-
data: item.data,
|
|
218
|
-
},
|
|
219
|
-
});
|
|
223
|
+
parts.push(convertGoogleImagePart(item));
|
|
220
224
|
} else {
|
|
221
225
|
omittedImages = true;
|
|
222
226
|
}
|
|
@@ -315,12 +319,7 @@ export function convertMessages<T extends GoogleApiType>(model: Model<T>, contex
|
|
|
315
319
|
? "(see attached image)"
|
|
316
320
|
: "";
|
|
317
321
|
|
|
318
|
-
const imageParts
|
|
319
|
-
inlineData: {
|
|
320
|
-
mimeType: imageBlock.mimeType,
|
|
321
|
-
data: imageBlock.data,
|
|
322
|
-
},
|
|
323
|
-
}));
|
|
322
|
+
const imageParts = imageContent.map(convertGoogleImagePart);
|
|
324
323
|
|
|
325
324
|
const includeId = supportsFunctionPartId(model);
|
|
326
325
|
const emittedName = emittedToolCallNames.get(msg.toolCallId);
|
|
@@ -65,10 +65,17 @@ export interface Part {
|
|
|
65
65
|
thought?: boolean;
|
|
66
66
|
thoughtSignature?: string;
|
|
67
67
|
inlineData?: InlineDataPart;
|
|
68
|
+
fileData?: FileDataPart;
|
|
68
69
|
functionCall?: FunctionCallPart;
|
|
69
70
|
functionResponse?: FunctionResponsePart;
|
|
70
71
|
}
|
|
71
72
|
|
|
73
|
+
/** Remote media reference; the backend fetches `fileUri` server-side. */
|
|
74
|
+
export interface FileDataPart {
|
|
75
|
+
fileUri: string;
|
|
76
|
+
mimeType?: string;
|
|
77
|
+
}
|
|
78
|
+
|
|
72
79
|
/** Conversation turn. Roles: `"user"`, `"model"`, optionally absent for system instructions. */
|
|
73
80
|
export interface Content {
|
|
74
81
|
role?: string;
|
package/src/providers/mock.ts
CHANGED
|
@@ -137,6 +137,8 @@ export interface MockModelOptions {
|
|
|
137
137
|
id?: string;
|
|
138
138
|
/** Provider string used in the returned AssistantMessage. Defaults to `"mock"`. */
|
|
139
139
|
provider?: string;
|
|
140
|
+
/** Base URL reported by the model. Defaults to `"mock://"`. */
|
|
141
|
+
baseUrl?: string;
|
|
140
142
|
/** A sequence of responses, one per call. Accepts arrays, generators, or any iterable. */
|
|
141
143
|
responses?: MockResponseSource;
|
|
142
144
|
/** Fallback handler used when `responses` is exhausted. */
|
|
@@ -168,7 +170,7 @@ export class MockModel implements Model<MockApi> {
|
|
|
168
170
|
readonly name: string;
|
|
169
171
|
readonly api: MockApi = MOCK_API;
|
|
170
172
|
readonly provider: string;
|
|
171
|
-
readonly baseUrl
|
|
173
|
+
readonly baseUrl: string;
|
|
172
174
|
readonly reasoning: boolean;
|
|
173
175
|
readonly input: ("text" | "image")[] = ["text"];
|
|
174
176
|
readonly cost: Model["cost"];
|
|
@@ -189,6 +191,7 @@ export class MockModel implements Model<MockApi> {
|
|
|
189
191
|
this.id = options.id ?? "mock-model";
|
|
190
192
|
this.name = options.id ?? "mock-model";
|
|
191
193
|
this.provider = options.provider ?? "mock";
|
|
194
|
+
this.baseUrl = options.baseUrl ?? "mock://";
|
|
192
195
|
this.reasoning = options.reasoning ?? false;
|
|
193
196
|
this.cost = options.cost ?? ZERO_COST;
|
|
194
197
|
this.contextWindow = options.contextWindow ?? 200_000;
|