Skip to content

Commit 7eb1987

Browse files
committed
feat: fix task tool guard threshold and inject subtype values (v2.3.20)
2 parents e50989d + bcfebe1 commit 7eb1987

File tree

10 files changed

+404
-103
lines changed

10 files changed

+404
-103
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@rama_nigg/open-cursor",
3-
"version": "2.3.19",
3+
"version": "2.3.20",
44
"description": "No prompt limits. No broken streams. Full thinking + tool support. Your Cursor subscription, properly integrated.",
55
"type": "module",
66
"main": "dist/plugin-entry.js",

src/mcp/config.ts

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,55 @@ export function readMcpConfigs(deps: ReadMcpConfigsDeps = {}): McpServerConfig[]
9393
return configs;
9494
}
9595

96+
interface ReadSubagentNamesDeps {
97+
configJson?: string;
98+
existsSync?: (path: string) => boolean;
99+
readFileSync?: (path: string, enc: BufferEncoding) => string;
100+
env?: NodeJS.ProcessEnv;
101+
}
102+
103+
export function readSubagentNames(deps: ReadSubagentNamesDeps = {}): string[] {
104+
let raw: string;
105+
106+
if (deps.configJson != null) {
107+
raw = deps.configJson;
108+
} else {
109+
const exists = deps.existsSync ?? nodeExistsSync;
110+
const readFile = deps.readFileSync ?? nodeReadFileSync;
111+
const configPath = resolveOpenCodeConfigPath(deps.env ?? process.env);
112+
if (!exists(configPath)) return ["general-purpose"];
113+
try {
114+
raw = readFile(configPath, "utf8");
115+
} catch {
116+
return ["general-purpose"];
117+
}
118+
}
119+
120+
let parsed: Record<string, unknown>;
121+
try {
122+
parsed = JSON.parse(raw);
123+
} catch {
124+
return ["general-purpose"];
125+
}
126+
127+
const agentSection = parsed.agent;
128+
if (!agentSection || typeof agentSection !== "object" || Array.isArray(agentSection)) {
129+
return ["general-purpose"];
130+
}
131+
132+
const agents = agentSection as Record<string, unknown>;
133+
const names = Object.keys(agents);
134+
if (names.length === 0) return ["general-purpose"];
135+
136+
const subagentNames = names.filter((name) => {
137+
const entry = agents[name];
138+
return entry && typeof entry === "object" && !Array.isArray(entry)
139+
&& (entry as Record<string, unknown>).mode === "subagent";
140+
});
141+
142+
return subagentNames.length > 0 ? subagentNames : names;
143+
}
144+
96145
function isStringRecord(v: unknown): v is Record<string, string> {
97146
return typeof v === "object" && v !== null && !Array.isArray(v);
98147
}

src/plugin.ts

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ import { ToolRouter } from "./tools/router.js";
2525
import { SkillLoader } from "./tools/skills/loader.js";
2626
import { SkillResolver } from "./tools/skills/resolver.js";
2727
import { autoRefreshModels } from "./models/sync.js";
28-
import { readMcpConfigs } from "./mcp/config.js";
28+
import { readMcpConfigs, readSubagentNames } from "./mcp/config.js";
2929
import { McpClientManager } from "./mcp/client-manager.js";
3030
import { buildMcpToolHookEntries, buildMcpToolDefinitions } from "./mcp/tool-bridge.js";
3131
import { createOpencodeClient } from "@opencode-ai/sdk";
@@ -92,6 +92,7 @@ export function buildAvailableToolsSystemMessage(
9292
lastToolMap: Array<{ id: string; name: string }>,
9393
mcpToolDefs: any[],
9494
mcpToolSummaries?: McpToolSummary[],
95+
subagentNames: string[] = [],
9596
): string | null {
9697
const parts: string[] = [];
9798

@@ -132,6 +133,12 @@ export function buildAvailableToolsSystemMessage(
132133
parts.push(lines.join("\n"));
133134
}
134135

136+
if (subagentNames.length > 0) {
137+
parts.push(
138+
`When calling the task tool, set subagent_type to one of: ${subagentNames.join(", ")}. Do not omit this parameter.`
139+
);
140+
}
141+
135142
return parts.length > 0 ? parts.join("\n\n") : null;
136143
}
137144

@@ -628,7 +635,8 @@ async function ensureCursorProxyServer(workspaceDirectory: string, toolRouter?:
628635
const toolLoopGuard = createToolLoopGuard(messages, TOOL_LOOP_MAX_REPEAT);
629636
const boundaryContext = createBoundaryRuntimeContext("bun-handler");
630637

631-
const prompt = buildPromptFromMessages(messages, tools);
638+
const subagentNames = readSubagentNames();
639+
const prompt = buildPromptFromMessages(messages, tools, subagentNames);
632640
const model = boundaryContext.run("normalizeRuntimeModel", (boundary) =>
633641
boundary.normalizeRuntimeModel(body?.model),
634642
);
@@ -1092,7 +1100,8 @@ async function ensureCursorProxyServer(workspaceDirectory: string, toolRouter?:
10921100
const toolLoopGuard = createToolLoopGuard(messages, TOOL_LOOP_MAX_REPEAT);
10931101
const boundaryContext = createBoundaryRuntimeContext("node-handler");
10941102

1095-
const prompt = buildPromptFromMessages(messages, tools);
1103+
const subagentNames = readSubagentNames();
1104+
const prompt = buildPromptFromMessages(messages, tools, subagentNames);
10961105
const model = boundaryContext.run("normalizeRuntimeModel", (boundary) =>
10971106
boundary.normalizeRuntimeModel(bodyData?.model),
10981107
);
@@ -2058,7 +2067,11 @@ export const CursorPlugin: Plugin = async ({ $, directory, worktree, client, ser
20582067

20592068
async "experimental.chat.system.transform"(input: any, output: { system: string[] }) {
20602069
if (!toolsEnabled) return;
2061-
const systemMessage = buildAvailableToolsSystemMessage(lastToolNames, lastToolMap, mcpToolDefs, mcpToolSummaries);
2070+
const subagentNames = readSubagentNames();
2071+
const systemMessage = buildAvailableToolsSystemMessage(
2072+
lastToolNames, lastToolMap, mcpToolDefs, mcpToolSummaries,
2073+
subagentNames,
2074+
);
20622075
if (!systemMessage) return;
20632076
output.system = output.system || [];
20642077
output.system.push(systemMessage);

src/provider/tool-loop-guard.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ const EXPLORATION_TOOLS = new Set([
4444
"bash",
4545
"shell",
4646
"webfetch",
47+
"task",
4748
]);
4849

4950
export interface ToolLoopGuardDecision {
@@ -511,16 +512,20 @@ function evaluateWithFingerprints(
511512
};
512513
}
513514

515+
const isExplorationTool = EXPLORATION_TOOLS.has(toolName.toLowerCase());
516+
const effectiveMaxRepeat = isExplorationTool
517+
? maxRepeat * EXPLORATION_LIMIT_MULTIPLIER
518+
: maxRepeat;
519+
514520
const strictRepeatCount = (strictCounts.get(strictFingerprint) ?? 0) + 1;
515521
strictCounts.set(strictFingerprint, strictRepeatCount);
516-
const strictTriggered = strictRepeatCount > maxRepeat;
522+
const strictTriggered = strictRepeatCount > effectiveMaxRepeat;
517523

518-
const isExplorationTool = EXPLORATION_TOOLS.has(toolName.toLowerCase());
519524
if (isExplorationTool) {
520525
return {
521526
fingerprint: strictFingerprint,
522527
repeatCount: strictRepeatCount,
523-
maxRepeat,
528+
maxRepeat: effectiveMaxRepeat,
524529
errorClass,
525530
triggered: strictTriggered,
526531
tracked: true,

src/proxy/prompt-builder.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ function debugLogToFile(message: string, data: any): void {
3636
* Handles role:"tool" result messages and assistant tool_calls that
3737
* plain text flattening would silently drop.
3838
*/
39-
export function buildPromptFromMessages(messages: Array<any>, tools: Array<any>): string {
39+
export function buildPromptFromMessages(messages: Array<any>, tools: Array<any>, subagentNames: string[] = []): string {
4040
// DEBUG: Log incoming message structure to file for root cause analysis
4141
const messageSummary = messages.map((m: any, i: number) => {
4242
const role = m?.role ?? "?";
@@ -98,6 +98,15 @@ export function buildPromptFromMessages(messages: Array<any>, tools: Array<any>)
9898
`SYSTEM: You have access to the following tools. When you need to use one, respond with a tool_call in the standard OpenAI format.\n` +
9999
`Tool guidance: prefer write/edit for file changes; use bash mainly to run commands/tests.\n\nAvailable tools:\n${toolDescs}`,
100100
);
101+
const hasTaskTool = tools.some((t: any) => {
102+
const name = (t?.function?.name ?? t?.name ?? "").toLowerCase();
103+
return name === "task";
104+
});
105+
if (hasTaskTool && subagentNames.length > 0) {
106+
lines.push(
107+
`When calling the task tool, set subagent_type to one of: ${subagentNames.join(", ")}. Do not omit this parameter.`
108+
);
109+
}
101110
}
102111

103112
for (const message of messages) {

tests/unit/mcp-config.test.ts

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import { describe, it, expect } from "bun:test";
2+
import { readSubagentNames } from "../../src/mcp/config.js";
3+
4+
describe("readSubagentNames", () => {
5+
it("returns only mode:subagent agents when some exist", () => {
6+
const config = JSON.stringify({
7+
agent: {
8+
build: { mode: "primary", model: "openai/gpt-5" },
9+
codemachine: { mode: "subagent", model: "kimi/kimi-k2" },
10+
review: { mode: "subagent", model: "google/gemini" },
11+
},
12+
});
13+
expect(readSubagentNames({ configJson: config })).toEqual(["codemachine", "review"]);
14+
});
15+
16+
it("returns all agents when none have mode:subagent", () => {
17+
const config = JSON.stringify({
18+
agent: {
19+
build: { mode: "primary", model: "openai/gpt-5" },
20+
plan: { mode: "primary", model: "zai/glm" },
21+
},
22+
});
23+
expect(readSubagentNames({ configJson: config })).toEqual(["build", "plan"]);
24+
});
25+
26+
it("returns general-purpose when agent section is empty object", () => {
27+
const config = JSON.stringify({ agent: {} });
28+
expect(readSubagentNames({ configJson: config })).toEqual(["general-purpose"]);
29+
});
30+
31+
it("returns general-purpose when agent section is absent", () => {
32+
const config = JSON.stringify({ mcp: {} });
33+
expect(readSubagentNames({ configJson: config })).toEqual(["general-purpose"]);
34+
});
35+
36+
it("returns general-purpose when config file is unreadable", () => {
37+
expect(readSubagentNames({ configJson: undefined, existsSync: () => false })).toEqual(["general-purpose"]);
38+
});
39+
40+
it("returns general-purpose when config is malformed JSON", () => {
41+
expect(readSubagentNames({ configJson: "{ bad json" })).toEqual(["general-purpose"]);
42+
});
43+
});
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import { describe, it, expect } from "bun:test";
2+
import { buildAvailableToolsSystemMessage } from "../../src/plugin.js";
3+
4+
describe("buildAvailableToolsSystemMessage — subagentNames injection", () => {
5+
it("includes subagent names in task guidance", () => {
6+
const msg = buildAvailableToolsSystemMessage(
7+
["task", "read"],
8+
[{ id: "task", name: "task" }],
9+
[],
10+
[],
11+
["codemachine", "review"],
12+
);
13+
expect(msg).toContain("codemachine");
14+
expect(msg).toContain("review");
15+
expect(msg).toContain("subagent_type");
16+
});
17+
18+
it("omits task guidance when subagentNames is empty", () => {
19+
const msg = buildAvailableToolsSystemMessage(
20+
["task"],
21+
[],
22+
[],
23+
[],
24+
[],
25+
);
26+
expect(msg).not.toContain("subagent_type");
27+
});
28+
29+
it("returns null when no tools and no subagentNames", () => {
30+
const msg = buildAvailableToolsSystemMessage([], [], [], [], []);
31+
expect(msg).toBeNull();
32+
});
33+
});

0 commit comments

Comments
 (0)