Skip to content
Merged
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
24 changes: 21 additions & 3 deletions open-sse/executors/cursor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,8 +64,11 @@ import { promisify } from "node:util";
import { toolChoiceDirectiveLine, buildCursorOutputConstraints } from "./cursor/prompt.ts";
import {
bridgeCursorBuiltinTool,
bridgeCursorNativeTodoWrite,
extractLatestTodoHistory,
selectCursorBridgeTools,
type CursorClientPlatform,
type CursorTodoHistoryItem,
} from "./cursor/builtinToolBridge.ts";
import {
isComposerModel,
Expand Down Expand Up @@ -463,6 +466,7 @@ export function processFrame(
mcpTools?: McpToolDefinition[];
blobStore?: Map<string, Buffer>;
clientPlatform?: CursorClientPlatform;
todoHistory?: CursorTodoHistoryItem[];
} = {}
): void {
// 1. JSON error envelope (Connect-RPC style — usually status > 200).
Expand Down Expand Up @@ -573,7 +577,18 @@ export function processFrame(
return;
}
for (const d of deltas) {
if (d.kind === "text" && d.text) {
if (d.kind === "native_todo_write") {
const dedupKey = `native_todo_write:${d.toolCallId}`;
if (!ackedExecIds.has(dedupKey)) {
ackedExecIds.add(dedupKey);
const bridge = bridgeCursorNativeTodoWrite(d, opts.mcpTools ?? [], opts.todoHistory);
if (bridge) {
emitStructuredToolCall(ctx, bridge.toolName, bridge.arguments);
ctx.requiresColdResume = true;
ctx.endReason = "tool_calls";
}
}
} else if (d.kind === "text" && d.text) {
if (!ctx.emittedRoleChunk) {
emitChunk(ctx, { role: "assistant", content: "" });
ctx.emittedRoleChunk = true;
Expand Down Expand Up @@ -987,6 +1002,7 @@ export class CursorExecutor extends BaseExecutor {
mcpTools: McpToolDefinition[] | undefined,
blobStore: Map<string, Buffer> | undefined,
clientPlatform: CursorClientPlatform | undefined,
todoHistory: CursorTodoHistoryItem[] | undefined,
signal?: AbortSignal
): Promise<void> {
const ackedExecIds = new Set<string>();
Expand Down Expand Up @@ -1090,6 +1106,7 @@ export class CursorExecutor extends BaseExecutor {
mcpTools,
blobStore,
clientPlatform,
todoHistory,
});
} catch (err) {
debugLog(
Expand Down Expand Up @@ -1148,6 +1165,7 @@ export class CursorExecutor extends BaseExecutor {
: undefined;
const mcpTools = selectCursorBridgeTools(declaredMcpTools, body.tool_choice);
const clientPlatform = inferCursorClientPlatform(messages);
const todoHistory = extractLatestTodoHistory(messages);

// Sanitize error messages: strip stack traces and absolute paths to
// prevent information exposure. Shared helper in utils/error.ts.
Expand Down Expand Up @@ -1314,7 +1332,7 @@ export class CursorExecutor extends BaseExecutor {
start: async (controller) => {
const ctx = newStreamCtx(model, (s) => controller.enqueue(enc.encode(s)));
try {
await this.driveH2(h2, ctx, mcpTools, blobStore, clientPlatform, signal);
await this.driveH2(h2, ctx, mcpTools, blobStore, clientPlatform, todoHistory, signal);
this.finalizeSseStream(ctx, body);
finishLifecycle(ctx, false);
controller.close();
Expand Down Expand Up @@ -1344,7 +1362,7 @@ export class CursorExecutor extends BaseExecutor {
// Non-streaming: drive to completion, return chat.completion JSON.
const ctx = newStreamCtx(model, () => {});
try {
await this.driveH2(h2, ctx, mcpTools, blobStore, clientPlatform, signal);
await this.driveH2(h2, ctx, mcpTools, blobStore, clientPlatform, todoHistory, signal);
} catch (err) {
finishLifecycle(ctx, true);
const message = err instanceof Error ? err.message : String(err);
Expand Down
220 changes: 194 additions & 26 deletions open-sse/executors/cursor/builtinToolBridge.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import {
decodeProtobufValue,
type ChatMessage,
type DecodedDelta,
type ExecServerEvent,
type McpToolDefinition,
} from "../../utils/cursorAgentProtobuf.ts";
Expand All @@ -11,6 +13,13 @@ export type CursorBuiltinToolBridge = {

export type CursorClientPlatform = "windows" | "posix";

export type CursorTodoHistoryItem = {
content: string;
priority?: string;
};

type CursorNativeTodoWrite = Extract<DecodedDelta, { kind: "native_todo_write" }>;

type OpenAIToolChoice =
string | { type?: unknown; function?: { name?: unknown } } | null | undefined;

Expand All @@ -22,6 +31,7 @@ type JsonSchema = {
};

const DIRECT_SHELL_TOOL_NAMES = ["bash", "shell", "run_terminal_cmd"];
const TODO_WRITE_TOOL_NAMES = ["todowrite", "todo_write"];
const BRIDGE_DESCRIPTION = "Run Cursor-requested shell command";
const ROOT_SCHEMA_KEYS = new Set([
"$schema",
Expand All @@ -46,21 +56,32 @@ const PROPERTY_ANNOTATION_KEYS = [
] as const;
const SCALAR_PROPERTY_KEYS = new Set(["type", ...PROPERTY_ANNOTATION_KEYS]);
const ARRAY_PROPERTY_KEYS = new Set(["type", "items", ...PROPERTY_ANNOTATION_KEYS]);
const TODO_SCALAR_PROPERTY_KEYS = new Set(["type", "enum", ...PROPERTY_ANNOTATION_KEYS]);

/** Restrict bridge candidates to the caller's OpenAI tool_choice contract. */
export function selectCursorBridgeTools(
tools: McpToolDefinition[] | undefined,
toolChoice: OpenAIToolChoice
): McpToolDefinition[] | undefined {
if (toolChoice === "none") return undefined;
const specificName =
toolChoice &&
typeof toolChoice === "object" &&
toolChoice.type === "function" &&
typeof toolChoice.function?.name === "string"
? toolChoice.function.name
: undefined;
return specificName ? tools?.filter((tool) => tool.name === specificName) : tools;
if (
toolChoice === undefined ||
toolChoice === null ||
toolChoice === "auto" ||
toolChoice === "required"
) {
return tools;
}
if (
!isRecord(toolChoice) ||
toolChoice.type !== "function" ||
!isRecord(toolChoice.function) ||
typeof toolChoice.function.name !== "string" ||
!toolChoice.function.name
) {
return undefined;
}
return tools?.filter((tool) => tool.name === toolChoice.function.name);
}

function isRecord(value: unknown): value is Record<string, unknown> {
Expand All @@ -73,29 +94,28 @@ function hasOnlyKeys(value: Record<string, unknown>, allowed: Set<string>): bool

function schemaFor(tool: McpToolDefinition): JsonSchema | null {
try {
const decoded = decodeProtobufValue(tool.inputSchemaBytes);
if (!isRecord(decoded) || decoded.type !== "object") return null;
if (!hasOnlyKeys(decoded, ROOT_SCHEMA_KEYS)) return null;
if (decoded.properties !== undefined && !isRecord(decoded.properties)) return null;
if (
decoded.required !== undefined &&
(!Array.isArray(decoded.required) ||
!decoded.required.every((key) => typeof key === "string"))
) {
return null;
}
if (
decoded.additionalProperties !== undefined &&
typeof decoded.additionalProperties !== "boolean"
) {
return null;
}
return decoded as JsonSchema;
return objectSchema(decodeProtobufValue(tool.inputSchemaBytes));
} catch {
return null;
}
}

function objectSchema(value: unknown): JsonSchema | null {
if (!isRecord(value) || value.type !== "object") return null;
if (!hasOnlyKeys(value, ROOT_SCHEMA_KEYS)) return null;
if (value.properties !== undefined && !isRecord(value.properties)) return null;
if (
value.required !== undefined &&
(!Array.isArray(value.required) || !value.required.every((key) => typeof key === "string"))
) {
return null;
}
if (value.additionalProperties !== undefined && typeof value.additionalProperties !== "boolean") {
return null;
}
return value as JsonSchema;
}

function schemaProperties(schema: JsonSchema): Record<string, unknown> {
return isRecord(schema.properties) ? schema.properties : {};
}
Expand Down Expand Up @@ -125,6 +145,17 @@ function propertySupports(value: unknown, expected: "string" | "boolean" | "stri
return hasOnlyKeys(value, SCALAR_PROPERTY_KEYS) && value.type === expected;
}

function propertyAcceptsString(value: unknown, actual: string): boolean {
if (!isRecord(value) || !hasOnlyKeys(value, TODO_SCALAR_PROPERTY_KEYS)) return false;
if (value.type !== "string") return false;
if (value.enum === undefined) return true;
return (
Array.isArray(value.enum) &&
value.enum.every((entry) => typeof entry === "string") &&
value.enum.includes(actual)
);
}

function namedTools(tools: McpToolDefinition[], names: string[]): McpToolDefinition[] {
const out: McpToolDefinition[] = [];
for (const name of names) {
Expand Down Expand Up @@ -228,6 +259,143 @@ function readBridge(
return null;
}

/**
* Recover priorities from the latest structured external TodoWrite call in
* OpenAI history. Cursor's native TodoItem wire schema has no priority field,
* so the bridge may preserve a prior declared value but must never invent one.
*/
export function extractLatestTodoHistory(
messages: ChatMessage[]
): CursorTodoHistoryItem[] | undefined {
for (let messageIndex = messages.length - 1; messageIndex >= 0; messageIndex -= 1) {
const message = messages[messageIndex];
if (message.role !== "assistant") continue;
const calls = message.tool_calls;
if (!Array.isArray(calls)) continue;
for (let callIndex = calls.length - 1; callIndex >= 0; callIndex -= 1) {
const call = calls[callIndex];
if (
!isRecord(call) ||
!isRecord(call.function) ||
typeof call.function.name !== "string" ||
!call.function.name ||
typeof call.function.arguments !== "string"
) {
return undefined;
}
if (!TODO_WRITE_TOOL_NAMES.includes(call.function.name.toLowerCase())) continue;
let parsed: unknown;
try {
parsed = JSON.parse(call.function.arguments);
} catch {
return undefined;
}
if (!isRecord(parsed) || !Array.isArray(parsed.todos)) return undefined;
const out: CursorTodoHistoryItem[] = [];
const contents = new Set<string>();
for (const value of parsed.todos) {
if (!isRecord(value) || typeof value.content !== "string" || !value.content) {
return undefined;
}
if (contents.has(value.content)) return undefined;
contents.add(value.content);
if (
value.priority !== undefined &&
(typeof value.priority !== "string" || !value.priority)
) {
return undefined;
}
out.push({
content: value.content,
...(typeof value.priority === "string" ? { priority: value.priority } : {}),
});
}
return out;
}
}
return undefined;
}

/**
* Surface Cursor's native TodoWrite as a declared OpenAI TodoWrite call.
* OpenCode's todowrite replaces the complete list, so native merge=true is
* accepted only when the payload's content set exactly matches structured
* history and is therefore provably a complete replacement.
*/
export function bridgeCursorNativeTodoWrite(
event: CursorNativeTodoWrite,
tools: McpToolDefinition[],
history: CursorTodoHistoryItem[] | undefined
): CursorBuiltinToolBridge | null {
const nativeContents = new Set<string>();
for (const item of event.todos) {
if (nativeContents.has(item.content)) return null;
nativeContents.add(item.content);
}

const historyByContent = new Map<string, CursorTodoHistoryItem>();
for (const item of history ?? []) {
if (historyByContent.has(item.content)) return null;
historyByContent.set(item.content, item);
}
if (
event.merge &&
(!history ||
historyByContent.size !== nativeContents.size ||
[...nativeContents].some((content) => !historyByContent.has(content)))
) {
return null;
}

for (const tool of namedTools(tools, TODO_WRITE_TOOL_NAMES)) {
const schema = schemaFor(tool);
if (!schema) continue;
const properties = schemaProperties(schema);
const todosProperty = properties.todos;
if (
!isRecord(todosProperty) ||
!hasOnlyKeys(todosProperty, ARRAY_PROPERTY_KEYS) ||
todosProperty.type !== "array" ||
!isRecord(todosProperty.items)
) {
continue;
}
const itemSchema = objectSchema(todosProperty.items);
if (!itemSchema) continue;
const itemProperties = schemaProperties(itemSchema);
if (!isRecord(itemProperties.content) || !isRecord(itemProperties.status)) continue;

const bridgedTodos: Array<Record<string, unknown>> = [];
let compatible = true;
for (const item of event.todos) {
if (
!propertyAcceptsString(itemProperties.content, item.content) ||
!propertyAcceptsString(itemProperties.status, item.status)
) {
compatible = false;
break;
}
const bridged: Record<string, unknown> = {
content: item.content,
status: item.status,
};
const priority = historyByContent.get(item.content)?.priority;
if (priority && propertyAcceptsString(itemProperties.priority, priority)) {
bridged.priority = priority;
}
if (!hasAllRequired(itemSchema, bridged)) {
compatible = false;
break;
}
bridgedTodos.push(bridged);
}
if (!compatible) continue;
const args: Record<string, unknown> = { todos: bridgedTodos };
if (hasAllRequired(schema, args)) return { toolName: tool.name, arguments: args };
}
return null;
}

/**
* Convert a Cursor-native built-in request into a declared external tool call.
* Only event variants whose complete arguments are decoded are supported.
Expand Down
Loading
Loading