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
2 changes: 1 addition & 1 deletion src/adapters/cursor/tool-definitions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -615,7 +615,7 @@ export function buildCursorToolGuidanceSystemNote(
// Code mode: shell/edit/MCP live inside freeform `exec` as nested helpers. Without this the
// model probes for a top-level shell tool that is not there.
codeMode
? `\`${CODEX_UNIFIED_EXEC_TOOL}\` is Codex code mode: its body is JavaScript evaluated in a V8 isolate, not a shell command and not Node. Shell, file edits, and MCP are nested helpers called INSIDE that body as \`await tools.<name>(...)\`, for example \`await tools.exec_command({cmd: \"ls\"})\`. Read the tool description for the exact nested helpers this turn provides. Those nested helpers are not themselves top-level tools, so do not call \`exec_command\`, \`shell_command\`, or \`apply_patch\` at the top level here${codeModeOtherTopLevelNames.length > 0 ? `; every other tool this turn lists, including ${quotedNames(codeModeOtherTopLevelNames)}, remains callable at the top level as usual` : ""}.`
? `\`${CODEX_UNIFIED_EXEC_TOOL}\` is Codex code mode: its body is JavaScript evaluated in a V8 isolate, not a shell command and not Node. Shell, file edits, and MCP are nested helpers called INSIDE that body as \`await tools.<name>(...)\`, for example \`await tools.exec_command({cmd: \"ls\"})\`. Read the tool description and the isolate global \`ALL_TOOLS\` (not \`tools.ALL_TOOLS\`) for helpers this turn provides; absence from the top-level catalog or from \`exec\`'s description is not absence. Those nested helpers are not themselves top-level tools, so do not call \`exec_command\` or \`shell_command\` at the top level here${codeModeOtherTopLevelNames.length > 0 ? `; every other tool this turn lists, including ${quotedNames(codeModeOtherTopLevelNames)}, remains callable at the top level as usual` : ""}.`
: undefined,
codeMode
? "In code mode the isolate returns nothing on its own: call `text(...)` (or `notify(...)`) on any value you need to see, or the call completes with empty output. There is no `require`, no `module`, and no filesystem or network globals; reach the host only through the nested helpers."
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Expand Down
24 changes: 19 additions & 5 deletions src/adapters/tool-catalog-nudge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,10 @@ import {
// `python3` heredoc edits. The sibling list in `./cursor/tool-definitions.ts` never
// included it either.
const NEIGHBOR_AGENT_TOOL_NAMES = ["Read", "Grep", "Glob", "Bash", "LS"] as const;
const CODEX_CODE_MODE_EXEC_TOOL = "exec";

function quoteNames(names: readonly string[]): string {
return names.map(name => `\`${name}\``).join(", ");
return names.map(name => "`" + name + "`").join(", ");
}

function uniqueNames(names: readonly string[]): string[] {
Expand All @@ -40,6 +41,16 @@ export function shouldInjectNonOpenAIToolCatalogNudge(provider: Pick<OcxProvider
}
}

function advertisedCodeModeExecName(
advertised: ReadonlySet<string>,
toWireName: (name: string) => string,
): string | undefined {
const wireName = toWireName(CODEX_CODE_MODE_EXEC_TOOL);
if (advertised.has(wireName)) return wireName;
if (advertised.has(CODEX_CODE_MODE_EXEC_TOOL)) return CODEX_CODE_MODE_EXEC_TOOL;
return undefined;
}

export function buildNonOpenAIToolCatalogNudgeFromNames(
wireNames: readonly string[] | undefined,
toWireName: (name: string) => string = name => name,
Expand All @@ -50,21 +61,24 @@ export function buildNonOpenAIToolCatalogNudgeFromNames(
const advertised = new Set(names);
// Compare in the catalog's own coordinate system. `advertised` holds WIRE names, so a
// provider that rewrites them (Claude OAuth `custom_`, Anthropic compat `cx_`) would never
// match a bare neighbor name and would forbid tools the turn actually advertises the
// match a bare neighbor name and would forbid tools the turn actually advertises -- the
// catalog would list `custom_apply_patch` while the same sentence banned `apply_patch`.
const unavailableNeighborNames = NEIGHBOR_AGENT_TOOL_NAMES.filter(
name => !advertised.has(name) && !advertised.has(toWireName(name)),
);
const codeModeExecName = advertisedCodeModeExecName(advertised, toWireName);

return [
"Tool contract: use the current tool catalog as ground truth.",
`Valid tool names for this turn are exactly ${quoteNames(names)}.`,
"Valid tool names for this turn are exactly " + quoteNames(names) + ".",
"These listed names are the complete top-level tool-call surface for this turn.",
"Call only listed names with their listed argument keys; do not invent, translate, or rename tools.",
"Names mentioned only in instructions, tool descriptions, argument descriptions, or nested helper APIs are not additional top-level tools.",
"If a listed tool exposes nested helpers such as a tools.* API, call the listed parent tool and use those helpers only inside that tool's input.",
codeModeExecName
? "If `" + codeModeExecName + "` is listed, it is Codex code mode: its body is JavaScript evaluated in a V8 isolate. Nested helpers are called INSIDE that body as `await tools.<name>(...)`, for example `await tools.exec_command({cmd: \"ls\"})` or `await tools.codex_app__list_threads({})`. Absence from the top-level catalog or from `" + codeModeExecName + "`'s description is not absence: deferred helpers stay callable on `tools.<name>`. Discover them from the isolate global `ALL_TOOLS`, not `tools.ALL_TOOLS`. Do not skip an available nested helper because it is omitted from the listed top-level names."
: "If a listed tool exposes nested helpers such as a tools.* API, call the listed parent tool and use those helpers only inside that tool's input.",
unavailableNeighborNames.length > 0
? `Do not use neighboring-agent tool names ${quoteNames(unavailableNeighborNames)} unless this turn's catalog lists those exact names.`
? "Do not use neighboring-agent tool names " + quoteNames(unavailableNeighborNames) + " unless this turn's catalog lists those exact names."
: undefined,
"If you need shell, file search, file read, edit, or discovery behavior, choose the listed tool that provides that capability.",
"Count a tool call only after its tool result returns; batch independent read-only calls when the runtime supports it.",
Expand Down
18 changes: 18 additions & 0 deletions tests/cursor-tool-definitions.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -447,6 +447,9 @@ describe("Cursor code mode tool guidance", () => {
expect(note).toContain("await tools.exec_command({cmd: " + "\"" + "ls" + "\"" + "})");
expect(note).toContain("text(...)");
expect(note).toContain("There is no `require`");
expect(note).toContain("isolate global `ALL_TOOLS`");
expect(note).toContain("not `tools.ALL_TOOLS`");
expect(note).toContain("absence from the top-level catalog");

// The flat-catalog shell-bridge guidance must NOT appear: naming a top-level
// `exec_command` in code mode sends the model after a tool that does not exist.
Expand All @@ -455,6 +458,21 @@ describe("Cursor code mode tool guidance", () => {
expect(note).not.toContain("For file read/search/listing, use");
});

test("does not forbid a separately listed apply_patch in code mode", () => {
const note = buildCursorToolGuidanceSystemNote([
codeModeExec(),
{ name: "apply_patch", description: "Apply a patch", parameters: {}, freeform: true },
]);
expect(note).toBeDefined();
if (!note) throw new Error("Expected Cursor tool guidance note");

expect(note).toContain("is Codex code mode");
expect(note).toContain("remains callable at the top level as usual");
expect(note).toContain("`apply_patch`");
expect(note).not.toContain("do not call `exec_command`, `shell_command`, or `apply_patch` at the top level here");
expect(note).toContain("do not call `exec_command` or `shell_command` at the top level here");
});

test("keeps other visible top-level tools callable in code mode", () => {
// Code mode is about how `exec` works, not a claim that the rest of the catalog is nested.
// A turn can advertise freeform `exec` alongside ordinary top-level tools, and describing
Expand Down
27 changes: 26 additions & 1 deletion tests/tool-catalog-nudge.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,10 +55,35 @@ describe("non-OpenAI tool catalog nudge", () => {
expect(note).toContain("Valid tool names for this turn are exactly `exec`, `wait`, `request_user_input`");
expect(note).toContain("complete top-level tool-call surface");
expect(note).toContain("nested helper APIs are not additional top-level tools");
expect(note).toContain("call the listed parent tool");
expect(note).toContain("If `exec` is listed, it is Codex code mode");
expect(note).toContain("await tools.<name>(...)");
expect(note).toContain("await tools.codex_app__list_threads({})");
expect(note).toContain("isolate global `ALL_TOOLS`, not `tools.ALL_TOOLS`");
expect(note).toContain("Do not skip an available nested helper");
expect(note).not.toContain("call the listed parent tool and use those helpers only inside that tool's input");
expect(note).not.toContain("apply_patch");
});

test("keeps the generic nested-helper parent-tool rule when exec is not listed", () => {
const note = buildNonOpenAIToolCatalogNudgeFromNames(["exec_command", "mcp__fs__read_file"]);

expect(note).toContain("call the listed parent tool and use those helpers only inside that tool's input");
expect(note).not.toContain("If `exec` is listed, it is Codex code mode");
expect(note).not.toContain("tools.ALL_TOOLS");
});

test("detects a wire-renamed exec as code mode", () => {
const note = buildNonOpenAIToolCatalogNudgeFromNames(
["cx_exec", "cx_wait"],
name => `cx_${name}`,
);

expect(note).toContain("If `cx_exec` is listed, it is Codex code mode");
expect(note).toContain("from `cx_exec`'s description is not absence");
expect(note).toContain("isolate global `ALL_TOOLS`, not `tools.ALL_TOOLS`");
expect(note).not.toContain("If `exec` is listed, it is Codex code mode");
});

// `advertised` holds WIRE names. A provider that rewrites them (Claude OAuth `custom_`,
// Anthropic compat `cx_`) must not have every neighbor name declared unavailable while the
// catalog plainly lists the prefixed form.
Expand Down
Loading