Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 66 additions & 3 deletions extensions/ai-providers/cursor/proto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export interface AgentClientMessage extends ProtoMessage {
| { case: "execClientMessage"; value: ExecClientMessage }
| { case: "execClientControlMessage"; value: ExecClientControlMessage }
| { case: "kvClientMessage"; value: KvClientMessage }
| { case: "interactionResponse"; value: InteractionResponse }
| { case: "clientHeartbeat"; value: ClientHeartbeat };
}

Expand Down Expand Up @@ -50,6 +51,12 @@ export const AgentClientMessageSchema: MessageCodec<AgentClientMessage> =
kind: "message",
T: () => KvClientMessageSchema,
},
{
no: 6,
name: "interactionResponse",
kind: "message",
T: () => InteractionResponseSchema,
},
{
no: 7,
name: "clientHeartbeat",
Expand Down Expand Up @@ -903,10 +910,66 @@ export const AgentServerMessageSchema: MessageCodec<AgentServerMessage> =
]);

/**
* Queries require an interactive client answer. This provider has no UI or
* tool execution channel, so it recognizes the envelope and fails the turn
* explicitly instead of silently dropping a server request.
* Queries require a client answer. Only WebFetch rejection is supported;
* other queries still fail explicitly rather than silently hanging the Run.
* Response fields come from oh-my-pi@856d9375,
* packages/ai/src/providers/cursor/proto/agent.proto (MIT).
*/
export interface WebFetchRequestRejected extends ProtoMessage {
reason: string;
}

export const WebFetchRequestRejectedSchema: MessageCodec<WebFetchRequestRejected> =
pb<WebFetchRequestRejected>("agent.v1.WebFetchRequestResponse_Rejected", [
{ no: 1, name: "reason", kind: "string" },
]);

export interface WebFetchRequestResponse extends ProtoMessage {
result:
| { case: undefined; value?: undefined }
| { case: "rejected"; value: WebFetchRequestRejected };
}

export const WebFetchRequestResponseSchema: MessageCodec<WebFetchRequestResponse> =
pb<WebFetchRequestResponse>("agent.v1.WebFetchRequestResponse", [
{
kind: "oneof",
name: "result",
variants: [
{
no: 2,
name: "rejected",
kind: "message",
T: () => WebFetchRequestRejectedSchema,
},
],
},
]);

export interface InteractionResponse extends ProtoMessage {
id: number;
result:
| { case: undefined; value?: undefined }
| { case: "webFetchRequestResponse"; value: WebFetchRequestResponse };
}

export const InteractionResponseSchema: MessageCodec<InteractionResponse> =
pb<InteractionResponse>("agent.v1.InteractionResponse", [
{ no: 1, name: "id", kind: "uint32" },
{
kind: "oneof",
name: "result",
variants: [
{
no: 9,
name: "webFetchRequestResponse",
kind: "message",
T: () => WebFetchRequestResponseSchema,
},
],
},
]);

export interface InteractionQuery extends ProtoMessage {
id: number;
query:
Expand Down
44 changes: 44 additions & 0 deletions extensions/ai-providers/cursor/provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ import {
ExecClientStreamCloseSchema,
ExecClientThrowSchema,
GetBlobResultSchema,
InteractionResponseSchema,
type InteractionUpdate,
KvClientMessageSchema,
type KvServerMessage,
Expand Down Expand Up @@ -70,6 +71,8 @@ import {
SetBlobResultSchema,
UserMessageActionSchema,
UserMessageSchema,
WebFetchRequestRejectedSchema,
WebFetchRequestResponseSchema,
} from "./proto.ts";
import { create, encodeJsonValue, fromBinary, toBinary } from "./protobuf.ts";
import { connectCursorHttp2 } from "./proxy.ts";
Expand All @@ -86,6 +89,7 @@ const MAX_CONNECT_FRAME_BYTES = 16 * 1024 * 1024;
const HEARTBEAT_INTERVAL_MS = 5_000;
const PROXY_TUNNEL_TIMEOUT_MS = 30_000;
const MAX_NATIVE_EXEC_REJECTIONS = 3;
const MAX_WEB_FETCH_REJECTIONS = 3;

export const CURSOR_CHAT_ONLY_SYSTEM_PROMPT =
"This Cursor provider is running in chat-only mode. No filesystem, shell, code modification, MCP, web, or user-interaction tools are available. Never emit tool calls or interaction queries. Images attached to the user message are already available for direct analysis. If required information is unavailable, explain the limitation in text instead of attempting a tool.";
Expand Down Expand Up @@ -715,6 +719,7 @@ export function streamCursor(
const pendingCalls = new Map<string, ToolCall>();
let handingOffTools = false;
let nativeExecRejections = 0;
let webFetchRejections = 0;

const closeBlocks = () => {
if (currentText) {
Expand Down Expand Up @@ -991,6 +996,45 @@ export function streamCursor(
return;
}
if (message.message.case === "interactionQuery") {
const query = message.message.value;
if (query.query.case === "webFetchRequestQuery") {
// Deny hosted execution without aborting the Run. Pi still owns
// any subsequent tool call and its permissions, including fetches.
const reply = create(AgentClientMessageSchema, {
message: {
case: "interactionResponse",
value: create(InteractionResponseSchema, {
id: query.id,
result: {
case: "webFetchRequestResponse",
value: create(WebFetchRequestResponseSchema, {
result: {
case: "rejected",
value: create(WebFetchRequestRejectedSchema, {
reason: context.tools?.length
? "Cursor-hosted web fetching is unavailable. Use an advertised Pi tool if it supports the request; otherwise explain that you cannot fetch the page. Do not retry Cursor-hosted fetching."
: "Web fetching is unavailable in this chat-only provider. Explain that you cannot fetch the page. Do not retry Cursor-hosted fetching.",
}),
},
}),
},
}),
},
});
const recover = ++webFetchRejections <= MAX_WEB_FETCH_REJECTIONS;
if (!recover) {
terminalError = new Error(
`Cursor WebFetch recovery limit (${MAX_WEB_FETCH_REJECTIONS}) exceeded; last query id ${query.id}`,
);
}
h2Request?.write(
frameConnectMessage(toBinary(AgentClientMessageSchema, reply)),
() => {
if (!recover) settle(terminalError);
},
);
return;
}
throw new Error(
`Cursor interaction query ${message.message.value.query.case ?? "unknown"} is unavailable ${context.tools?.length ? "outside Pi's interaction lifecycle" : "in chat-only mode"}`,
);
Expand Down
Loading
Loading