@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.
Files changed (51) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/LICENSE +23 -0
  3. package/THIRD-PARTY-NOTICES.txt +22909 -0
  4. package/dist/types/auth-retry.d.ts +8 -3
  5. package/dist/types/auth-storage.d.ts +9 -0
  6. package/dist/types/dialect/qwen-xml.d.ts +6 -0
  7. package/dist/types/error/auth-classify.d.ts +8 -8
  8. package/dist/types/error/oauth.d.ts +2 -1
  9. package/dist/types/providers/cursor/exec-modern.d.ts +1 -1
  10. package/dist/types/providers/cursor-pi-args.d.ts +21 -0
  11. package/dist/types/providers/cursor.d.ts +23 -3
  12. package/dist/types/providers/google-types.d.ts +6 -0
  13. package/dist/types/providers/mock.d.ts +3 -1
  14. package/dist/types/providers/openai-shared.d.ts +6 -1
  15. package/dist/types/registry/oauth/google-antigravity.d.ts +2 -6
  16. package/dist/types/registry/oauth/index.d.ts +2 -3
  17. package/dist/types/registry/oauth/types.d.ts +2 -1
  18. package/dist/types/registry/oauth/xai-oauth.d.ts +2 -2
  19. package/dist/types/registry/registry.d.ts +1 -1
  20. package/dist/types/registry/types.d.ts +2 -1
  21. package/dist/types/registry/xai-oauth.d.ts +1 -1
  22. package/dist/types/types.d.ts +21 -0
  23. package/dist/types/utils/stream-markup-healing.d.ts +1 -1
  24. package/package.json +8 -6
  25. package/src/auth-retry.ts +41 -3
  26. package/src/auth-storage.ts +229 -75
  27. package/src/dialect/qwen-xml.ts +82 -0
  28. package/src/error/auth-classify.ts +10 -8
  29. package/src/error/oauth.ts +2 -1
  30. package/src/providers/amazon-bedrock.ts +12 -19
  31. package/src/providers/anthropic.ts +29 -35
  32. package/src/providers/cursor/exec-modern.ts +2 -0
  33. package/src/providers/cursor-pi-args.ts +38 -2
  34. package/src/providers/cursor.ts +357 -32
  35. package/src/providers/google-shared.ts +11 -12
  36. package/src/providers/google-types.ts +7 -0
  37. package/src/providers/mock.ts +4 -1
  38. package/src/providers/openai-codex-responses.ts +34 -8
  39. package/src/providers/openai-completions.ts +2 -2
  40. package/src/providers/openai-shared.ts +72 -14
  41. package/src/registry/oauth/google-antigravity.ts +247 -162
  42. package/src/registry/oauth/index.ts +3 -3
  43. package/src/registry/oauth/types.ts +2 -1
  44. package/src/registry/oauth/xai-oauth.ts +14 -7
  45. package/src/registry/types.ts +2 -1
  46. package/src/registry/xai-oauth.ts +2 -2
  47. package/src/stream.ts +8 -4
  48. package/src/types.ts +22 -0
  49. package/src/utils/schema/CONSTRAINTS.md +1 -0
  50. package/src/utils/schema/fields.ts +20 -4
  51. package/src/utils/stream-markup-healing.ts +9 -2
@@ -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 { ConversationStep, McpToolDefinition } from "@oh-my-pi/pi-catalog/discovery/cursor-proto";
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 { isKimiK3ModelId } from "@oh-my-pi/pi-catalog/identity";
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, getProxyForProvider, shouldBypassProxy } from "../utils/proxy";
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 = shouldBypassProxy(new URL(baseUrl)) ? undefined : getProxyForProvider(model.provider);
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
- // The same composed selector the bridge executes: showing a bare path
1495
- // for a ranged read makes the returned slice look like the whole
1496
- // file in every rebuilt transcript.
1497
- synthesizeCursorExecToolCall(output, stream, state, args.toolCallId, "read", {
1498
- path: piReadDisplayPath(args.path, args.offset, args.limit),
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
- args,
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
- synthesizeCursorExecToolCall(output, stream, state, args.toolCallId, "write", {
1578
- path: args.path,
1579
- content,
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
- execHandlers?.write?.bind(execHandlers),
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.onToolResult?.({
3345
- role: "toolResult",
3346
- toolCallId: block.id,
3347
- toolName: block.name,
3348
- content: [{ type: "text", text: "The connection to Cursor closed before this call completed." }],
3349
- isError: true,
3350
- timestamp: Date.now(),
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, update.message.value.callId);
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.requestModelId ?? model.id;
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: Part[] = imageContent.map(imageBlock => ({
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;
@@ -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 = "mock://";
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;