@rama_nigg/open-cursor 2.3.18 → 2.3.20
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.
- package/dist/cli/mcptool.js +36 -0
- package/dist/index.js +136 -13
- package/dist/plugin-entry.js +136 -13
- package/package.json +1 -1
- package/src/mcp/config.ts +49 -0
- package/src/plugin.ts +17 -4
- package/src/provider/runtime-interception.ts +89 -1
- package/src/provider/tool-loop-guard.ts +8 -3
- package/src/proxy/prompt-builder.ts +10 -1
package/dist/cli/mcptool.js
CHANGED
|
@@ -292,6 +292,42 @@ function readMcpConfigs(deps = {}) {
|
|
|
292
292
|
}
|
|
293
293
|
return configs;
|
|
294
294
|
}
|
|
295
|
+
function readSubagentNames(deps = {}) {
|
|
296
|
+
let raw;
|
|
297
|
+
if (deps.configJson != null) {
|
|
298
|
+
raw = deps.configJson;
|
|
299
|
+
} else {
|
|
300
|
+
const exists = deps.existsSync ?? nodeExistsSync;
|
|
301
|
+
const readFile = deps.readFileSync ?? nodeReadFileSync;
|
|
302
|
+
const configPath = resolveOpenCodeConfigPath(deps.env ?? process.env);
|
|
303
|
+
if (!exists(configPath))
|
|
304
|
+
return ["general-purpose"];
|
|
305
|
+
try {
|
|
306
|
+
raw = readFile(configPath, "utf8");
|
|
307
|
+
} catch {
|
|
308
|
+
return ["general-purpose"];
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
let parsed;
|
|
312
|
+
try {
|
|
313
|
+
parsed = JSON.parse(raw);
|
|
314
|
+
} catch {
|
|
315
|
+
return ["general-purpose"];
|
|
316
|
+
}
|
|
317
|
+
const agentSection = parsed.agent;
|
|
318
|
+
if (!agentSection || typeof agentSection !== "object" || Array.isArray(agentSection)) {
|
|
319
|
+
return ["general-purpose"];
|
|
320
|
+
}
|
|
321
|
+
const agents = agentSection;
|
|
322
|
+
const names = Object.keys(agents);
|
|
323
|
+
if (names.length === 0)
|
|
324
|
+
return ["general-purpose"];
|
|
325
|
+
const subagentNames = names.filter((name) => {
|
|
326
|
+
const entry = agents[name];
|
|
327
|
+
return entry && typeof entry === "object" && !Array.isArray(entry) && entry.mode === "subagent";
|
|
328
|
+
});
|
|
329
|
+
return subagentNames.length > 0 ? subagentNames : names;
|
|
330
|
+
}
|
|
295
331
|
function isStringRecord(v) {
|
|
296
332
|
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
297
333
|
}
|
package/dist/index.js
CHANGED
|
@@ -889,7 +889,7 @@ function debugLogToFile(message, data) {
|
|
|
889
889
|
log4.debug(message, data);
|
|
890
890
|
}
|
|
891
891
|
}
|
|
892
|
-
function buildPromptFromMessages(messages, tools) {
|
|
892
|
+
function buildPromptFromMessages(messages, tools, subagentNames = []) {
|
|
893
893
|
const messageSummary = messages.map((m, i) => {
|
|
894
894
|
const role = m?.role ?? "?";
|
|
895
895
|
const hasToolCalls = Array.isArray(m?.tool_calls) ? m.tool_calls.length : 0;
|
|
@@ -947,6 +947,13 @@ function buildPromptFromMessages(messages, tools) {
|
|
|
947
947
|
|
|
948
948
|
Available tools:
|
|
949
949
|
${toolDescs}`);
|
|
950
|
+
const hasTaskTool = tools.some((t) => {
|
|
951
|
+
const name = (t?.function?.name ?? t?.name ?? "").toLowerCase();
|
|
952
|
+
return name === "task";
|
|
953
|
+
});
|
|
954
|
+
if (hasTaskTool && subagentNames.length > 0) {
|
|
955
|
+
lines.push(`When calling the task tool, set subagent_type to one of: ${subagentNames.join(", ")}. Do not omit this parameter.`);
|
|
956
|
+
}
|
|
950
957
|
}
|
|
951
958
|
for (const message of messages) {
|
|
952
959
|
const role = typeof message.role === "string" ? message.role : "user";
|
|
@@ -1908,6 +1915,42 @@ function readMcpConfigs(deps = {}) {
|
|
|
1908
1915
|
}
|
|
1909
1916
|
return configs;
|
|
1910
1917
|
}
|
|
1918
|
+
function readSubagentNames(deps = {}) {
|
|
1919
|
+
let raw;
|
|
1920
|
+
if (deps.configJson != null) {
|
|
1921
|
+
raw = deps.configJson;
|
|
1922
|
+
} else {
|
|
1923
|
+
const exists = deps.existsSync ?? nodeExistsSync2;
|
|
1924
|
+
const readFile = deps.readFileSync ?? nodeReadFileSync2;
|
|
1925
|
+
const configPath = resolveOpenCodeConfigPath(deps.env ?? process.env);
|
|
1926
|
+
if (!exists(configPath))
|
|
1927
|
+
return ["general-purpose"];
|
|
1928
|
+
try {
|
|
1929
|
+
raw = readFile(configPath, "utf8");
|
|
1930
|
+
} catch {
|
|
1931
|
+
return ["general-purpose"];
|
|
1932
|
+
}
|
|
1933
|
+
}
|
|
1934
|
+
let parsed;
|
|
1935
|
+
try {
|
|
1936
|
+
parsed = JSON.parse(raw);
|
|
1937
|
+
} catch {
|
|
1938
|
+
return ["general-purpose"];
|
|
1939
|
+
}
|
|
1940
|
+
const agentSection = parsed.agent;
|
|
1941
|
+
if (!agentSection || typeof agentSection !== "object" || Array.isArray(agentSection)) {
|
|
1942
|
+
return ["general-purpose"];
|
|
1943
|
+
}
|
|
1944
|
+
const agents = agentSection;
|
|
1945
|
+
const names = Object.keys(agents);
|
|
1946
|
+
if (names.length === 0)
|
|
1947
|
+
return ["general-purpose"];
|
|
1948
|
+
const subagentNames = names.filter((name) => {
|
|
1949
|
+
const entry = agents[name];
|
|
1950
|
+
return entry && typeof entry === "object" && !Array.isArray(entry) && entry.mode === "subagent";
|
|
1951
|
+
});
|
|
1952
|
+
return subagentNames.length > 0 ? subagentNames : names;
|
|
1953
|
+
}
|
|
1911
1954
|
function isStringRecord(v) {
|
|
1912
1955
|
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
1913
1956
|
}
|
|
@@ -14535,6 +14578,15 @@ async function handleToolLoopEventLegacy(options) {
|
|
|
14535
14578
|
if (compat.validation.hasSchema && !compat.validation.ok) {
|
|
14536
14579
|
const validationTermination = evaluateSchemaValidationLoopGuard(toolLoopGuard, normalizedToolCall, compat.validation);
|
|
14537
14580
|
if (validationTermination) {
|
|
14581
|
+
if (validationTermination.soft) {
|
|
14582
|
+
const hintChunk = createLoopGuardHintChunk(responseMeta, normalizedToolCall, validationTermination);
|
|
14583
|
+
log17.debug("Soft-blocking schema validation loop guard in legacy (emitting hint)", {
|
|
14584
|
+
tool: normalizedToolCall.function.name,
|
|
14585
|
+
fingerprint: validationTermination.fingerprint
|
|
14586
|
+
});
|
|
14587
|
+
await onToolResult(hintChunk);
|
|
14588
|
+
return { intercepted: false, skipConverter: true };
|
|
14589
|
+
}
|
|
14538
14590
|
return { intercepted: false, skipConverter: true, terminate: validationTermination };
|
|
14539
14591
|
}
|
|
14540
14592
|
const reroutedWrite = tryRerouteEditToWrite(normalizedToolCall, compat.normalizedArgs, allowedToolNames, toolSchemaMap);
|
|
@@ -14558,6 +14610,15 @@ async function handleToolLoopEventLegacy(options) {
|
|
|
14558
14610
|
}
|
|
14559
14611
|
const termination = evaluateToolLoopGuard(toolLoopGuard, normalizedToolCall);
|
|
14560
14612
|
if (termination) {
|
|
14613
|
+
if (termination.soft) {
|
|
14614
|
+
const hintChunk = createLoopGuardHintChunk(responseMeta, normalizedToolCall, termination);
|
|
14615
|
+
log17.debug("Soft-blocking tool loop guard in legacy (emitting hint)", {
|
|
14616
|
+
tool: normalizedToolCall.function.name,
|
|
14617
|
+
fingerprint: termination.fingerprint
|
|
14618
|
+
});
|
|
14619
|
+
await onToolResult(hintChunk);
|
|
14620
|
+
return { intercepted: false, skipConverter: true };
|
|
14621
|
+
}
|
|
14561
14622
|
return { intercepted: false, skipConverter: true, terminate: termination };
|
|
14562
14623
|
}
|
|
14563
14624
|
await onInterceptedToolCall(normalizedToolCall);
|
|
@@ -14654,10 +14715,30 @@ async function handleToolLoopEventV1(options) {
|
|
|
14654
14715
|
});
|
|
14655
14716
|
const validationTermination = evaluateSchemaValidationLoopGuard(toolLoopGuard, normalizedToolCall, compat.validation);
|
|
14656
14717
|
if (validationTermination) {
|
|
14718
|
+
if (validationTermination.soft) {
|
|
14719
|
+
const hintChunk = createLoopGuardHintChunk(responseMeta, normalizedToolCall, validationTermination);
|
|
14720
|
+
log17.debug("Soft-blocking schema validation loop guard (emitting hint)", {
|
|
14721
|
+
tool: normalizedToolCall.function.name,
|
|
14722
|
+
fingerprint: validationTermination.fingerprint,
|
|
14723
|
+
repeatCount: validationTermination.repeatCount
|
|
14724
|
+
});
|
|
14725
|
+
await onToolResult(hintChunk);
|
|
14726
|
+
return { intercepted: false, skipConverter: true };
|
|
14727
|
+
}
|
|
14657
14728
|
return { intercepted: false, skipConverter: true, terminate: validationTermination };
|
|
14658
14729
|
}
|
|
14659
14730
|
const termination2 = evaluateToolLoopGuard(toolLoopGuard, normalizedToolCall);
|
|
14660
14731
|
if (termination2) {
|
|
14732
|
+
if (termination2.soft) {
|
|
14733
|
+
const hintChunk = createLoopGuardHintChunk(responseMeta, normalizedToolCall, termination2);
|
|
14734
|
+
log17.debug("Soft-blocking tool loop guard in validation path (emitting hint)", {
|
|
14735
|
+
tool: normalizedToolCall.function.name,
|
|
14736
|
+
fingerprint: termination2.fingerprint,
|
|
14737
|
+
repeatCount: termination2.repeatCount
|
|
14738
|
+
});
|
|
14739
|
+
await onToolResult(hintChunk);
|
|
14740
|
+
return { intercepted: false, skipConverter: true };
|
|
14741
|
+
}
|
|
14661
14742
|
return { intercepted: false, skipConverter: true, terminate: termination2 };
|
|
14662
14743
|
}
|
|
14663
14744
|
const reroutedWrite = tryRerouteEditToWrite(normalizedToolCall, compat.normalizedArgs, allowedToolNames, toolSchemaMap);
|
|
@@ -14712,6 +14793,16 @@ async function handleToolLoopEventV1(options) {
|
|
|
14712
14793
|
}
|
|
14713
14794
|
const termination = evaluateToolLoopGuard(toolLoopGuard, normalizedToolCall);
|
|
14714
14795
|
if (termination) {
|
|
14796
|
+
if (termination.soft) {
|
|
14797
|
+
const hintChunk = createLoopGuardHintChunk(responseMeta, normalizedToolCall, termination);
|
|
14798
|
+
log17.debug("Soft-blocking tool loop guard (emitting hint)", {
|
|
14799
|
+
tool: normalizedToolCall.function.name,
|
|
14800
|
+
fingerprint: termination.fingerprint,
|
|
14801
|
+
repeatCount: termination.repeatCount
|
|
14802
|
+
});
|
|
14803
|
+
await onToolResult(hintChunk);
|
|
14804
|
+
return { intercepted: false, skipConverter: true };
|
|
14805
|
+
}
|
|
14715
14806
|
return { intercepted: false, skipConverter: true, terminate: termination };
|
|
14716
14807
|
}
|
|
14717
14808
|
await onInterceptedToolCall(normalizedToolCall);
|
|
@@ -14782,6 +14873,7 @@ function evaluateToolLoopGuard(toolLoopGuard, toolCall) {
|
|
|
14782
14873
|
silent: true
|
|
14783
14874
|
};
|
|
14784
14875
|
}
|
|
14876
|
+
const isFirstTrigger = decision.repeatCount === decision.maxRepeat + 1;
|
|
14785
14877
|
return {
|
|
14786
14878
|
reason: "loop_guard",
|
|
14787
14879
|
message: `Tool loop guard stopped repeated failing calls to "${toolCall.function.name}" ` + `after ${decision.repeatCount} attempts (limit ${decision.maxRepeat}). ` + "Adjust tool arguments and retry.",
|
|
@@ -14789,7 +14881,8 @@ function evaluateToolLoopGuard(toolLoopGuard, toolCall) {
|
|
|
14789
14881
|
fingerprint: decision.fingerprint,
|
|
14790
14882
|
repeatCount: decision.repeatCount,
|
|
14791
14883
|
maxRepeat: decision.maxRepeat,
|
|
14792
|
-
errorClass: decision.errorClass
|
|
14884
|
+
errorClass: decision.errorClass,
|
|
14885
|
+
soft: isFirstTrigger
|
|
14793
14886
|
};
|
|
14794
14887
|
}
|
|
14795
14888
|
function createSchemaValidationTermination(toolCall, validation) {
|
|
@@ -14822,12 +14915,14 @@ function evaluateSchemaValidationLoopGuard(toolLoopGuard, toolCall, validation)
|
|
|
14822
14915
|
if (!decision.tracked || !decision.triggered) {
|
|
14823
14916
|
return null;
|
|
14824
14917
|
}
|
|
14825
|
-
|
|
14918
|
+
const isFirstTrigger = decision.repeatCount === decision.maxRepeat + 1;
|
|
14919
|
+
log17.debug("Tool loop guard triggered on schema validation", {
|
|
14826
14920
|
tool: toolCall.function.name,
|
|
14827
14921
|
fingerprint: decision.fingerprint,
|
|
14828
14922
|
repeatCount: decision.repeatCount,
|
|
14829
14923
|
maxRepeat: decision.maxRepeat,
|
|
14830
|
-
validationSignature
|
|
14924
|
+
validationSignature,
|
|
14925
|
+
soft: isFirstTrigger
|
|
14831
14926
|
});
|
|
14832
14927
|
return {
|
|
14833
14928
|
reason: "loop_guard",
|
|
@@ -14836,7 +14931,8 @@ function evaluateSchemaValidationLoopGuard(toolLoopGuard, toolCall, validation)
|
|
|
14836
14931
|
fingerprint: decision.fingerprint,
|
|
14837
14932
|
repeatCount: decision.repeatCount,
|
|
14838
14933
|
maxRepeat: decision.maxRepeat,
|
|
14839
|
-
errorClass: decision.errorClass
|
|
14934
|
+
errorClass: decision.errorClass,
|
|
14935
|
+
soft: isFirstTrigger
|
|
14840
14936
|
};
|
|
14841
14937
|
}
|
|
14842
14938
|
function buildValidationSignature(validation) {
|
|
@@ -14894,6 +14990,25 @@ function createNonFatalSchemaValidationHintChunk(meta, toolCall, validation) {
|
|
|
14894
14990
|
]
|
|
14895
14991
|
};
|
|
14896
14992
|
}
|
|
14993
|
+
function createLoopGuardHintChunk(meta, toolCall, termination) {
|
|
14994
|
+
const content = `Tool "${toolCall.function.name}" has been temporarily blocked after ` + `${termination.repeatCount} repeated ${termination.errorClass} failures. ` + "Do not retry this tool. Use a different approach to complete the task.";
|
|
14995
|
+
return {
|
|
14996
|
+
id: meta.id,
|
|
14997
|
+
object: "chat.completion.chunk",
|
|
14998
|
+
created: meta.created,
|
|
14999
|
+
model: meta.model,
|
|
15000
|
+
choices: [
|
|
15001
|
+
{
|
|
15002
|
+
index: 0,
|
|
15003
|
+
delta: {
|
|
15004
|
+
role: "assistant",
|
|
15005
|
+
content
|
|
15006
|
+
},
|
|
15007
|
+
finish_reason: null
|
|
15008
|
+
}
|
|
15009
|
+
]
|
|
15010
|
+
};
|
|
15011
|
+
}
|
|
14897
15012
|
function safeArgTypeSummary(event) {
|
|
14898
15013
|
try {
|
|
14899
15014
|
let raw;
|
|
@@ -15373,15 +15488,16 @@ function evaluateWithFingerprints(toolName, errorClass, strictFingerprint, coars
|
|
|
15373
15488
|
tracked: false
|
|
15374
15489
|
};
|
|
15375
15490
|
}
|
|
15491
|
+
const isExplorationTool = EXPLORATION_TOOLS.has(toolName.toLowerCase());
|
|
15492
|
+
const effectiveMaxRepeat = isExplorationTool ? maxRepeat * EXPLORATION_LIMIT_MULTIPLIER : maxRepeat;
|
|
15376
15493
|
const strictRepeatCount = (strictCounts.get(strictFingerprint) ?? 0) + 1;
|
|
15377
15494
|
strictCounts.set(strictFingerprint, strictRepeatCount);
|
|
15378
|
-
const strictTriggered = strictRepeatCount >
|
|
15379
|
-
const isExplorationTool = EXPLORATION_TOOLS.has(toolName.toLowerCase());
|
|
15495
|
+
const strictTriggered = strictRepeatCount > effectiveMaxRepeat;
|
|
15380
15496
|
if (isExplorationTool) {
|
|
15381
15497
|
return {
|
|
15382
15498
|
fingerprint: strictFingerprint,
|
|
15383
15499
|
repeatCount: strictRepeatCount,
|
|
15384
|
-
maxRepeat,
|
|
15500
|
+
maxRepeat: effectiveMaxRepeat,
|
|
15385
15501
|
errorClass,
|
|
15386
15502
|
triggered: strictTriggered,
|
|
15387
15503
|
tracked: true
|
|
@@ -15506,7 +15622,8 @@ var init_tool_loop_guard = __esm(() => {
|
|
|
15506
15622
|
"semsearch",
|
|
15507
15623
|
"bash",
|
|
15508
15624
|
"shell",
|
|
15509
|
-
"webfetch"
|
|
15625
|
+
"webfetch",
|
|
15626
|
+
"task"
|
|
15510
15627
|
]);
|
|
15511
15628
|
});
|
|
15512
15629
|
|
|
@@ -15543,7 +15660,7 @@ function debugLogToFile2(message, data) {
|
|
|
15543
15660
|
appendFileSync3(DEBUG_LOG_FILE2, logLine);
|
|
15544
15661
|
} catch {}
|
|
15545
15662
|
}
|
|
15546
|
-
function buildAvailableToolsSystemMessage(lastToolNames, lastToolMap, mcpToolDefs, mcpToolSummaries) {
|
|
15663
|
+
function buildAvailableToolsSystemMessage(lastToolNames, lastToolMap, mcpToolDefs, mcpToolSummaries, subagentNames = []) {
|
|
15547
15664
|
const parts = [];
|
|
15548
15665
|
if (lastToolNames.length > 0 || lastToolMap.length > 0) {
|
|
15549
15666
|
const names = lastToolNames.join(", ");
|
|
@@ -15578,6 +15695,9 @@ function buildAvailableToolsSystemMessage(lastToolNames, lastToolMap, mcpToolDef
|
|
|
15578
15695
|
parts.push(lines.join(`
|
|
15579
15696
|
`));
|
|
15580
15697
|
}
|
|
15698
|
+
if (subagentNames.length > 0) {
|
|
15699
|
+
parts.push(`When calling the task tool, set subagent_type to one of: ${subagentNames.join(", ")}. Do not omit this parameter.`);
|
|
15700
|
+
}
|
|
15581
15701
|
return parts.length > 0 ? parts.join(`
|
|
15582
15702
|
|
|
15583
15703
|
`) : null;
|
|
@@ -15949,7 +16069,8 @@ async function ensureCursorProxyServer(workspaceDirectory, toolRouter) {
|
|
|
15949
16069
|
const toolSchemaMap = buildToolSchemaMap(tools);
|
|
15950
16070
|
const toolLoopGuard = createToolLoopGuard(messages, TOOL_LOOP_MAX_REPEAT);
|
|
15951
16071
|
const boundaryContext = createBoundaryRuntimeContext("bun-handler");
|
|
15952
|
-
const
|
|
16072
|
+
const subagentNames = readSubagentNames();
|
|
16073
|
+
const prompt = buildPromptFromMessages(messages, tools, subagentNames);
|
|
15953
16074
|
const model = boundaryContext.run("normalizeRuntimeModel", (boundary) => boundary.normalizeRuntimeModel(body?.model));
|
|
15954
16075
|
const msgSummaryBun = messages.map((m, i) => {
|
|
15955
16076
|
const role = m?.role ?? "?";
|
|
@@ -16377,7 +16498,8 @@ async function ensureCursorProxyServer(workspaceDirectory, toolRouter) {
|
|
|
16377
16498
|
const toolSchemaMap = buildToolSchemaMap(tools);
|
|
16378
16499
|
const toolLoopGuard = createToolLoopGuard(messages, TOOL_LOOP_MAX_REPEAT);
|
|
16379
16500
|
const boundaryContext = createBoundaryRuntimeContext("node-handler");
|
|
16380
|
-
const
|
|
16501
|
+
const subagentNames = readSubagentNames();
|
|
16502
|
+
const prompt = buildPromptFromMessages(messages, tools, subagentNames);
|
|
16381
16503
|
const model = boundaryContext.run("normalizeRuntimeModel", (boundary) => boundary.normalizeRuntimeModel(bodyData?.model));
|
|
16382
16504
|
const msgSummary = messages.map((m, i) => {
|
|
16383
16505
|
const role = m?.role ?? "?";
|
|
@@ -17180,7 +17302,8 @@ var log19, DEBUG_LOG_DIR2, DEBUG_LOG_FILE2, CURSOR_PROVIDER_ID2 = "cursor-acp",
|
|
|
17180
17302
|
async "experimental.chat.system.transform"(input, output) {
|
|
17181
17303
|
if (!toolsEnabled)
|
|
17182
17304
|
return;
|
|
17183
|
-
const
|
|
17305
|
+
const subagentNames = readSubagentNames();
|
|
17306
|
+
const systemMessage = buildAvailableToolsSystemMessage(lastToolNames, lastToolMap, mcpToolDefs, mcpToolSummaries, subagentNames);
|
|
17184
17307
|
if (!systemMessage)
|
|
17185
17308
|
return;
|
|
17186
17309
|
output.system = output.system || [];
|
package/dist/plugin-entry.js
CHANGED
|
@@ -948,7 +948,7 @@ function debugLogToFile(message, data) {
|
|
|
948
948
|
log4.debug(message, data);
|
|
949
949
|
}
|
|
950
950
|
}
|
|
951
|
-
function buildPromptFromMessages(messages, tools) {
|
|
951
|
+
function buildPromptFromMessages(messages, tools, subagentNames = []) {
|
|
952
952
|
const messageSummary = messages.map((m, i) => {
|
|
953
953
|
const role = m?.role ?? "?";
|
|
954
954
|
const hasToolCalls = Array.isArray(m?.tool_calls) ? m.tool_calls.length : 0;
|
|
@@ -1006,6 +1006,13 @@ function buildPromptFromMessages(messages, tools) {
|
|
|
1006
1006
|
|
|
1007
1007
|
Available tools:
|
|
1008
1008
|
${toolDescs}`);
|
|
1009
|
+
const hasTaskTool = tools.some((t) => {
|
|
1010
|
+
const name = (t?.function?.name ?? t?.name ?? "").toLowerCase();
|
|
1011
|
+
return name === "task";
|
|
1012
|
+
});
|
|
1013
|
+
if (hasTaskTool && subagentNames.length > 0) {
|
|
1014
|
+
lines.push(`When calling the task tool, set subagent_type to one of: ${subagentNames.join(", ")}. Do not omit this parameter.`);
|
|
1015
|
+
}
|
|
1009
1016
|
}
|
|
1010
1017
|
for (const message of messages) {
|
|
1011
1018
|
const role = typeof message.role === "string" ? message.role : "user";
|
|
@@ -1908,6 +1915,42 @@ function readMcpConfigs(deps = {}) {
|
|
|
1908
1915
|
}
|
|
1909
1916
|
return configs;
|
|
1910
1917
|
}
|
|
1918
|
+
function readSubagentNames(deps = {}) {
|
|
1919
|
+
let raw;
|
|
1920
|
+
if (deps.configJson != null) {
|
|
1921
|
+
raw = deps.configJson;
|
|
1922
|
+
} else {
|
|
1923
|
+
const exists = deps.existsSync ?? nodeExistsSync2;
|
|
1924
|
+
const readFile = deps.readFileSync ?? nodeReadFileSync2;
|
|
1925
|
+
const configPath = resolveOpenCodeConfigPath(deps.env ?? process.env);
|
|
1926
|
+
if (!exists(configPath))
|
|
1927
|
+
return ["general-purpose"];
|
|
1928
|
+
try {
|
|
1929
|
+
raw = readFile(configPath, "utf8");
|
|
1930
|
+
} catch {
|
|
1931
|
+
return ["general-purpose"];
|
|
1932
|
+
}
|
|
1933
|
+
}
|
|
1934
|
+
let parsed;
|
|
1935
|
+
try {
|
|
1936
|
+
parsed = JSON.parse(raw);
|
|
1937
|
+
} catch {
|
|
1938
|
+
return ["general-purpose"];
|
|
1939
|
+
}
|
|
1940
|
+
const agentSection = parsed.agent;
|
|
1941
|
+
if (!agentSection || typeof agentSection !== "object" || Array.isArray(agentSection)) {
|
|
1942
|
+
return ["general-purpose"];
|
|
1943
|
+
}
|
|
1944
|
+
const agents = agentSection;
|
|
1945
|
+
const names = Object.keys(agents);
|
|
1946
|
+
if (names.length === 0)
|
|
1947
|
+
return ["general-purpose"];
|
|
1948
|
+
const subagentNames = names.filter((name) => {
|
|
1949
|
+
const entry = agents[name];
|
|
1950
|
+
return entry && typeof entry === "object" && !Array.isArray(entry) && entry.mode === "subagent";
|
|
1951
|
+
});
|
|
1952
|
+
return subagentNames.length > 0 ? subagentNames : names;
|
|
1953
|
+
}
|
|
1911
1954
|
function isStringRecord(v) {
|
|
1912
1955
|
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
1913
1956
|
}
|
|
@@ -14535,6 +14578,15 @@ async function handleToolLoopEventLegacy(options) {
|
|
|
14535
14578
|
if (compat.validation.hasSchema && !compat.validation.ok) {
|
|
14536
14579
|
const validationTermination = evaluateSchemaValidationLoopGuard(toolLoopGuard, normalizedToolCall, compat.validation);
|
|
14537
14580
|
if (validationTermination) {
|
|
14581
|
+
if (validationTermination.soft) {
|
|
14582
|
+
const hintChunk = createLoopGuardHintChunk(responseMeta, normalizedToolCall, validationTermination);
|
|
14583
|
+
log17.debug("Soft-blocking schema validation loop guard in legacy (emitting hint)", {
|
|
14584
|
+
tool: normalizedToolCall.function.name,
|
|
14585
|
+
fingerprint: validationTermination.fingerprint
|
|
14586
|
+
});
|
|
14587
|
+
await onToolResult(hintChunk);
|
|
14588
|
+
return { intercepted: false, skipConverter: true };
|
|
14589
|
+
}
|
|
14538
14590
|
return { intercepted: false, skipConverter: true, terminate: validationTermination };
|
|
14539
14591
|
}
|
|
14540
14592
|
const reroutedWrite = tryRerouteEditToWrite(normalizedToolCall, compat.normalizedArgs, allowedToolNames, toolSchemaMap);
|
|
@@ -14558,6 +14610,15 @@ async function handleToolLoopEventLegacy(options) {
|
|
|
14558
14610
|
}
|
|
14559
14611
|
const termination = evaluateToolLoopGuard(toolLoopGuard, normalizedToolCall);
|
|
14560
14612
|
if (termination) {
|
|
14613
|
+
if (termination.soft) {
|
|
14614
|
+
const hintChunk = createLoopGuardHintChunk(responseMeta, normalizedToolCall, termination);
|
|
14615
|
+
log17.debug("Soft-blocking tool loop guard in legacy (emitting hint)", {
|
|
14616
|
+
tool: normalizedToolCall.function.name,
|
|
14617
|
+
fingerprint: termination.fingerprint
|
|
14618
|
+
});
|
|
14619
|
+
await onToolResult(hintChunk);
|
|
14620
|
+
return { intercepted: false, skipConverter: true };
|
|
14621
|
+
}
|
|
14561
14622
|
return { intercepted: false, skipConverter: true, terminate: termination };
|
|
14562
14623
|
}
|
|
14563
14624
|
await onInterceptedToolCall(normalizedToolCall);
|
|
@@ -14654,10 +14715,30 @@ async function handleToolLoopEventV1(options) {
|
|
|
14654
14715
|
});
|
|
14655
14716
|
const validationTermination = evaluateSchemaValidationLoopGuard(toolLoopGuard, normalizedToolCall, compat.validation);
|
|
14656
14717
|
if (validationTermination) {
|
|
14718
|
+
if (validationTermination.soft) {
|
|
14719
|
+
const hintChunk = createLoopGuardHintChunk(responseMeta, normalizedToolCall, validationTermination);
|
|
14720
|
+
log17.debug("Soft-blocking schema validation loop guard (emitting hint)", {
|
|
14721
|
+
tool: normalizedToolCall.function.name,
|
|
14722
|
+
fingerprint: validationTermination.fingerprint,
|
|
14723
|
+
repeatCount: validationTermination.repeatCount
|
|
14724
|
+
});
|
|
14725
|
+
await onToolResult(hintChunk);
|
|
14726
|
+
return { intercepted: false, skipConverter: true };
|
|
14727
|
+
}
|
|
14657
14728
|
return { intercepted: false, skipConverter: true, terminate: validationTermination };
|
|
14658
14729
|
}
|
|
14659
14730
|
const termination2 = evaluateToolLoopGuard(toolLoopGuard, normalizedToolCall);
|
|
14660
14731
|
if (termination2) {
|
|
14732
|
+
if (termination2.soft) {
|
|
14733
|
+
const hintChunk = createLoopGuardHintChunk(responseMeta, normalizedToolCall, termination2);
|
|
14734
|
+
log17.debug("Soft-blocking tool loop guard in validation path (emitting hint)", {
|
|
14735
|
+
tool: normalizedToolCall.function.name,
|
|
14736
|
+
fingerprint: termination2.fingerprint,
|
|
14737
|
+
repeatCount: termination2.repeatCount
|
|
14738
|
+
});
|
|
14739
|
+
await onToolResult(hintChunk);
|
|
14740
|
+
return { intercepted: false, skipConverter: true };
|
|
14741
|
+
}
|
|
14661
14742
|
return { intercepted: false, skipConverter: true, terminate: termination2 };
|
|
14662
14743
|
}
|
|
14663
14744
|
const reroutedWrite = tryRerouteEditToWrite(normalizedToolCall, compat.normalizedArgs, allowedToolNames, toolSchemaMap);
|
|
@@ -14712,6 +14793,16 @@ async function handleToolLoopEventV1(options) {
|
|
|
14712
14793
|
}
|
|
14713
14794
|
const termination = evaluateToolLoopGuard(toolLoopGuard, normalizedToolCall);
|
|
14714
14795
|
if (termination) {
|
|
14796
|
+
if (termination.soft) {
|
|
14797
|
+
const hintChunk = createLoopGuardHintChunk(responseMeta, normalizedToolCall, termination);
|
|
14798
|
+
log17.debug("Soft-blocking tool loop guard (emitting hint)", {
|
|
14799
|
+
tool: normalizedToolCall.function.name,
|
|
14800
|
+
fingerprint: termination.fingerprint,
|
|
14801
|
+
repeatCount: termination.repeatCount
|
|
14802
|
+
});
|
|
14803
|
+
await onToolResult(hintChunk);
|
|
14804
|
+
return { intercepted: false, skipConverter: true };
|
|
14805
|
+
}
|
|
14715
14806
|
return { intercepted: false, skipConverter: true, terminate: termination };
|
|
14716
14807
|
}
|
|
14717
14808
|
await onInterceptedToolCall(normalizedToolCall);
|
|
@@ -14782,6 +14873,7 @@ function evaluateToolLoopGuard(toolLoopGuard, toolCall) {
|
|
|
14782
14873
|
silent: true
|
|
14783
14874
|
};
|
|
14784
14875
|
}
|
|
14876
|
+
const isFirstTrigger = decision.repeatCount === decision.maxRepeat + 1;
|
|
14785
14877
|
return {
|
|
14786
14878
|
reason: "loop_guard",
|
|
14787
14879
|
message: `Tool loop guard stopped repeated failing calls to "${toolCall.function.name}" ` + `after ${decision.repeatCount} attempts (limit ${decision.maxRepeat}). ` + "Adjust tool arguments and retry.",
|
|
@@ -14789,7 +14881,8 @@ function evaluateToolLoopGuard(toolLoopGuard, toolCall) {
|
|
|
14789
14881
|
fingerprint: decision.fingerprint,
|
|
14790
14882
|
repeatCount: decision.repeatCount,
|
|
14791
14883
|
maxRepeat: decision.maxRepeat,
|
|
14792
|
-
errorClass: decision.errorClass
|
|
14884
|
+
errorClass: decision.errorClass,
|
|
14885
|
+
soft: isFirstTrigger
|
|
14793
14886
|
};
|
|
14794
14887
|
}
|
|
14795
14888
|
function createSchemaValidationTermination(toolCall, validation) {
|
|
@@ -14822,12 +14915,14 @@ function evaluateSchemaValidationLoopGuard(toolLoopGuard, toolCall, validation)
|
|
|
14822
14915
|
if (!decision.tracked || !decision.triggered) {
|
|
14823
14916
|
return null;
|
|
14824
14917
|
}
|
|
14825
|
-
|
|
14918
|
+
const isFirstTrigger = decision.repeatCount === decision.maxRepeat + 1;
|
|
14919
|
+
log17.debug("Tool loop guard triggered on schema validation", {
|
|
14826
14920
|
tool: toolCall.function.name,
|
|
14827
14921
|
fingerprint: decision.fingerprint,
|
|
14828
14922
|
repeatCount: decision.repeatCount,
|
|
14829
14923
|
maxRepeat: decision.maxRepeat,
|
|
14830
|
-
validationSignature
|
|
14924
|
+
validationSignature,
|
|
14925
|
+
soft: isFirstTrigger
|
|
14831
14926
|
});
|
|
14832
14927
|
return {
|
|
14833
14928
|
reason: "loop_guard",
|
|
@@ -14836,7 +14931,8 @@ function evaluateSchemaValidationLoopGuard(toolLoopGuard, toolCall, validation)
|
|
|
14836
14931
|
fingerprint: decision.fingerprint,
|
|
14837
14932
|
repeatCount: decision.repeatCount,
|
|
14838
14933
|
maxRepeat: decision.maxRepeat,
|
|
14839
|
-
errorClass: decision.errorClass
|
|
14934
|
+
errorClass: decision.errorClass,
|
|
14935
|
+
soft: isFirstTrigger
|
|
14840
14936
|
};
|
|
14841
14937
|
}
|
|
14842
14938
|
function buildValidationSignature(validation) {
|
|
@@ -14894,6 +14990,25 @@ function createNonFatalSchemaValidationHintChunk(meta, toolCall, validation) {
|
|
|
14894
14990
|
]
|
|
14895
14991
|
};
|
|
14896
14992
|
}
|
|
14993
|
+
function createLoopGuardHintChunk(meta, toolCall, termination) {
|
|
14994
|
+
const content = `Tool "${toolCall.function.name}" has been temporarily blocked after ` + `${termination.repeatCount} repeated ${termination.errorClass} failures. ` + "Do not retry this tool. Use a different approach to complete the task.";
|
|
14995
|
+
return {
|
|
14996
|
+
id: meta.id,
|
|
14997
|
+
object: "chat.completion.chunk",
|
|
14998
|
+
created: meta.created,
|
|
14999
|
+
model: meta.model,
|
|
15000
|
+
choices: [
|
|
15001
|
+
{
|
|
15002
|
+
index: 0,
|
|
15003
|
+
delta: {
|
|
15004
|
+
role: "assistant",
|
|
15005
|
+
content
|
|
15006
|
+
},
|
|
15007
|
+
finish_reason: null
|
|
15008
|
+
}
|
|
15009
|
+
]
|
|
15010
|
+
};
|
|
15011
|
+
}
|
|
14897
15012
|
function safeArgTypeSummary(event) {
|
|
14898
15013
|
try {
|
|
14899
15014
|
let raw;
|
|
@@ -15373,15 +15488,16 @@ function evaluateWithFingerprints(toolName, errorClass, strictFingerprint, coars
|
|
|
15373
15488
|
tracked: false
|
|
15374
15489
|
};
|
|
15375
15490
|
}
|
|
15491
|
+
const isExplorationTool = EXPLORATION_TOOLS.has(toolName.toLowerCase());
|
|
15492
|
+
const effectiveMaxRepeat = isExplorationTool ? maxRepeat * EXPLORATION_LIMIT_MULTIPLIER : maxRepeat;
|
|
15376
15493
|
const strictRepeatCount = (strictCounts.get(strictFingerprint) ?? 0) + 1;
|
|
15377
15494
|
strictCounts.set(strictFingerprint, strictRepeatCount);
|
|
15378
|
-
const strictTriggered = strictRepeatCount >
|
|
15379
|
-
const isExplorationTool = EXPLORATION_TOOLS.has(toolName.toLowerCase());
|
|
15495
|
+
const strictTriggered = strictRepeatCount > effectiveMaxRepeat;
|
|
15380
15496
|
if (isExplorationTool) {
|
|
15381
15497
|
return {
|
|
15382
15498
|
fingerprint: strictFingerprint,
|
|
15383
15499
|
repeatCount: strictRepeatCount,
|
|
15384
|
-
maxRepeat,
|
|
15500
|
+
maxRepeat: effectiveMaxRepeat,
|
|
15385
15501
|
errorClass,
|
|
15386
15502
|
triggered: strictTriggered,
|
|
15387
15503
|
tracked: true
|
|
@@ -15506,7 +15622,8 @@ var init_tool_loop_guard = __esm(() => {
|
|
|
15506
15622
|
"semsearch",
|
|
15507
15623
|
"bash",
|
|
15508
15624
|
"shell",
|
|
15509
|
-
"webfetch"
|
|
15625
|
+
"webfetch",
|
|
15626
|
+
"task"
|
|
15510
15627
|
]);
|
|
15511
15628
|
});
|
|
15512
15629
|
|
|
@@ -15543,7 +15660,7 @@ function debugLogToFile2(message, data) {
|
|
|
15543
15660
|
appendFileSync3(DEBUG_LOG_FILE2, logLine);
|
|
15544
15661
|
} catch {}
|
|
15545
15662
|
}
|
|
15546
|
-
function buildAvailableToolsSystemMessage(lastToolNames, lastToolMap, mcpToolDefs, mcpToolSummaries) {
|
|
15663
|
+
function buildAvailableToolsSystemMessage(lastToolNames, lastToolMap, mcpToolDefs, mcpToolSummaries, subagentNames = []) {
|
|
15547
15664
|
const parts = [];
|
|
15548
15665
|
if (lastToolNames.length > 0 || lastToolMap.length > 0) {
|
|
15549
15666
|
const names = lastToolNames.join(", ");
|
|
@@ -15578,6 +15695,9 @@ function buildAvailableToolsSystemMessage(lastToolNames, lastToolMap, mcpToolDef
|
|
|
15578
15695
|
parts.push(lines.join(`
|
|
15579
15696
|
`));
|
|
15580
15697
|
}
|
|
15698
|
+
if (subagentNames.length > 0) {
|
|
15699
|
+
parts.push(`When calling the task tool, set subagent_type to one of: ${subagentNames.join(", ")}. Do not omit this parameter.`);
|
|
15700
|
+
}
|
|
15581
15701
|
return parts.length > 0 ? parts.join(`
|
|
15582
15702
|
|
|
15583
15703
|
`) : null;
|
|
@@ -15949,7 +16069,8 @@ async function ensureCursorProxyServer(workspaceDirectory, toolRouter) {
|
|
|
15949
16069
|
const toolSchemaMap = buildToolSchemaMap(tools);
|
|
15950
16070
|
const toolLoopGuard = createToolLoopGuard(messages, TOOL_LOOP_MAX_REPEAT);
|
|
15951
16071
|
const boundaryContext = createBoundaryRuntimeContext("bun-handler");
|
|
15952
|
-
const
|
|
16072
|
+
const subagentNames = readSubagentNames();
|
|
16073
|
+
const prompt = buildPromptFromMessages(messages, tools, subagentNames);
|
|
15953
16074
|
const model = boundaryContext.run("normalizeRuntimeModel", (boundary) => boundary.normalizeRuntimeModel(body?.model));
|
|
15954
16075
|
const msgSummaryBun = messages.map((m, i) => {
|
|
15955
16076
|
const role = m?.role ?? "?";
|
|
@@ -16377,7 +16498,8 @@ async function ensureCursorProxyServer(workspaceDirectory, toolRouter) {
|
|
|
16377
16498
|
const toolSchemaMap = buildToolSchemaMap(tools);
|
|
16378
16499
|
const toolLoopGuard = createToolLoopGuard(messages, TOOL_LOOP_MAX_REPEAT);
|
|
16379
16500
|
const boundaryContext = createBoundaryRuntimeContext("node-handler");
|
|
16380
|
-
const
|
|
16501
|
+
const subagentNames = readSubagentNames();
|
|
16502
|
+
const prompt = buildPromptFromMessages(messages, tools, subagentNames);
|
|
16381
16503
|
const model = boundaryContext.run("normalizeRuntimeModel", (boundary) => boundary.normalizeRuntimeModel(bodyData?.model));
|
|
16382
16504
|
const msgSummary = messages.map((m, i) => {
|
|
16383
16505
|
const role = m?.role ?? "?";
|
|
@@ -17180,7 +17302,8 @@ var log19, DEBUG_LOG_DIR2, DEBUG_LOG_FILE2, CURSOR_PROVIDER_ID2 = "cursor-acp",
|
|
|
17180
17302
|
async "experimental.chat.system.transform"(input, output) {
|
|
17181
17303
|
if (!toolsEnabled)
|
|
17182
17304
|
return;
|
|
17183
|
-
const
|
|
17305
|
+
const subagentNames = readSubagentNames();
|
|
17306
|
+
const systemMessage = buildAvailableToolsSystemMessage(lastToolNames, lastToolMap, mcpToolDefs, mcpToolSummaries, subagentNames);
|
|
17184
17307
|
if (!systemMessage)
|
|
17185
17308
|
return;
|
|
17186
17309
|
output.system = output.system || [];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rama_nigg/open-cursor",
|
|
3
|
-
"version": "2.3.
|
|
3
|
+
"version": "2.3.20",
|
|
4
4
|
"description": "No prompt limits. No broken streams. Full thinking + tool support. Your Cursor subscription, properly integrated.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/plugin-entry.js",
|
package/src/mcp/config.ts
CHANGED
|
@@ -93,6 +93,55 @@ export function readMcpConfigs(deps: ReadMcpConfigsDeps = {}): McpServerConfig[]
|
|
|
93
93
|
return configs;
|
|
94
94
|
}
|
|
95
95
|
|
|
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
|
+
|
|
96
145
|
function isStringRecord(v: unknown): v is Record<string, string> {
|
|
97
146
|
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
98
147
|
}
|
package/src/plugin.ts
CHANGED
|
@@ -25,7 +25,7 @@ import { ToolRouter } from "./tools/router.js";
|
|
|
25
25
|
import { SkillLoader } from "./tools/skills/loader.js";
|
|
26
26
|
import { SkillResolver } from "./tools/skills/resolver.js";
|
|
27
27
|
import { autoRefreshModels } from "./models/sync.js";
|
|
28
|
-
import { readMcpConfigs } from "./mcp/config.js";
|
|
28
|
+
import { readMcpConfigs, readSubagentNames } from "./mcp/config.js";
|
|
29
29
|
import { McpClientManager } from "./mcp/client-manager.js";
|
|
30
30
|
import { buildMcpToolHookEntries, buildMcpToolDefinitions } from "./mcp/tool-bridge.js";
|
|
31
31
|
import { createOpencodeClient } from "@opencode-ai/sdk";
|
|
@@ -92,6 +92,7 @@ export function buildAvailableToolsSystemMessage(
|
|
|
92
92
|
lastToolMap: Array<{ id: string; name: string }>,
|
|
93
93
|
mcpToolDefs: any[],
|
|
94
94
|
mcpToolSummaries?: McpToolSummary[],
|
|
95
|
+
subagentNames: string[] = [],
|
|
95
96
|
): string | null {
|
|
96
97
|
const parts: string[] = [];
|
|
97
98
|
|
|
@@ -132,6 +133,12 @@ export function buildAvailableToolsSystemMessage(
|
|
|
132
133
|
parts.push(lines.join("\n"));
|
|
133
134
|
}
|
|
134
135
|
|
|
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
|
+
|
|
135
142
|
return parts.length > 0 ? parts.join("\n\n") : null;
|
|
136
143
|
}
|
|
137
144
|
|
|
@@ -628,7 +635,8 @@ async function ensureCursorProxyServer(workspaceDirectory: string, toolRouter?:
|
|
|
628
635
|
const toolLoopGuard = createToolLoopGuard(messages, TOOL_LOOP_MAX_REPEAT);
|
|
629
636
|
const boundaryContext = createBoundaryRuntimeContext("bun-handler");
|
|
630
637
|
|
|
631
|
-
const
|
|
638
|
+
const subagentNames = readSubagentNames();
|
|
639
|
+
const prompt = buildPromptFromMessages(messages, tools, subagentNames);
|
|
632
640
|
const model = boundaryContext.run("normalizeRuntimeModel", (boundary) =>
|
|
633
641
|
boundary.normalizeRuntimeModel(body?.model),
|
|
634
642
|
);
|
|
@@ -1092,7 +1100,8 @@ async function ensureCursorProxyServer(workspaceDirectory: string, toolRouter?:
|
|
|
1092
1100
|
const toolLoopGuard = createToolLoopGuard(messages, TOOL_LOOP_MAX_REPEAT);
|
|
1093
1101
|
const boundaryContext = createBoundaryRuntimeContext("node-handler");
|
|
1094
1102
|
|
|
1095
|
-
const
|
|
1103
|
+
const subagentNames = readSubagentNames();
|
|
1104
|
+
const prompt = buildPromptFromMessages(messages, tools, subagentNames);
|
|
1096
1105
|
const model = boundaryContext.run("normalizeRuntimeModel", (boundary) =>
|
|
1097
1106
|
boundary.normalizeRuntimeModel(bodyData?.model),
|
|
1098
1107
|
);
|
|
@@ -2058,7 +2067,11 @@ export const CursorPlugin: Plugin = async ({ $, directory, worktree, client, ser
|
|
|
2058
2067
|
|
|
2059
2068
|
async "experimental.chat.system.transform"(input: any, output: { system: string[] }) {
|
|
2060
2069
|
if (!toolsEnabled) return;
|
|
2061
|
-
const
|
|
2070
|
+
const subagentNames = readSubagentNames();
|
|
2071
|
+
const systemMessage = buildAvailableToolsSystemMessage(
|
|
2072
|
+
lastToolNames, lastToolMap, mcpToolDefs, mcpToolSummaries,
|
|
2073
|
+
subagentNames,
|
|
2074
|
+
);
|
|
2062
2075
|
if (!systemMessage) return;
|
|
2063
2076
|
output.system = output.system || [];
|
|
2064
2077
|
output.system.push(systemMessage);
|
|
@@ -59,6 +59,7 @@ export interface ToolLoopGuardTermination {
|
|
|
59
59
|
maxRepeat: number;
|
|
60
60
|
errorClass: string;
|
|
61
61
|
silent?: boolean;
|
|
62
|
+
soft?: boolean;
|
|
62
63
|
}
|
|
63
64
|
|
|
64
65
|
export interface ToolSchemaValidationTermination {
|
|
@@ -177,6 +178,15 @@ export async function handleToolLoopEventLegacy(
|
|
|
177
178
|
compat.validation,
|
|
178
179
|
);
|
|
179
180
|
if (validationTermination) {
|
|
181
|
+
if (validationTermination.soft) {
|
|
182
|
+
const hintChunk = createLoopGuardHintChunk(responseMeta, normalizedToolCall, validationTermination);
|
|
183
|
+
log.debug("Soft-blocking schema validation loop guard in legacy (emitting hint)", {
|
|
184
|
+
tool: normalizedToolCall.function.name,
|
|
185
|
+
fingerprint: validationTermination.fingerprint,
|
|
186
|
+
});
|
|
187
|
+
await onToolResult(hintChunk);
|
|
188
|
+
return { intercepted: false, skipConverter: true };
|
|
189
|
+
}
|
|
180
190
|
return { intercepted: false, skipConverter: true, terminate: validationTermination };
|
|
181
191
|
}
|
|
182
192
|
|
|
@@ -211,6 +221,15 @@ export async function handleToolLoopEventLegacy(
|
|
|
211
221
|
|
|
212
222
|
const termination = evaluateToolLoopGuard(toolLoopGuard, normalizedToolCall);
|
|
213
223
|
if (termination) {
|
|
224
|
+
if (termination.soft) {
|
|
225
|
+
const hintChunk = createLoopGuardHintChunk(responseMeta, normalizedToolCall, termination);
|
|
226
|
+
log.debug("Soft-blocking tool loop guard in legacy (emitting hint)", {
|
|
227
|
+
tool: normalizedToolCall.function.name,
|
|
228
|
+
fingerprint: termination.fingerprint,
|
|
229
|
+
});
|
|
230
|
+
await onToolResult(hintChunk);
|
|
231
|
+
return { intercepted: false, skipConverter: true };
|
|
232
|
+
}
|
|
214
233
|
return { intercepted: false, skipConverter: true, terminate: termination };
|
|
215
234
|
}
|
|
216
235
|
await onInterceptedToolCall(normalizedToolCall);
|
|
@@ -341,10 +360,30 @@ export async function handleToolLoopEventV1(
|
|
|
341
360
|
compat.validation,
|
|
342
361
|
);
|
|
343
362
|
if (validationTermination) {
|
|
363
|
+
if (validationTermination.soft) {
|
|
364
|
+
const hintChunk = createLoopGuardHintChunk(responseMeta, normalizedToolCall, validationTermination);
|
|
365
|
+
log.debug("Soft-blocking schema validation loop guard (emitting hint)", {
|
|
366
|
+
tool: normalizedToolCall.function.name,
|
|
367
|
+
fingerprint: validationTermination.fingerprint,
|
|
368
|
+
repeatCount: validationTermination.repeatCount,
|
|
369
|
+
});
|
|
370
|
+
await onToolResult(hintChunk);
|
|
371
|
+
return { intercepted: false, skipConverter: true };
|
|
372
|
+
}
|
|
344
373
|
return { intercepted: false, skipConverter: true, terminate: validationTermination };
|
|
345
374
|
}
|
|
346
375
|
const termination = evaluateToolLoopGuard(toolLoopGuard, normalizedToolCall);
|
|
347
376
|
if (termination) {
|
|
377
|
+
if (termination.soft) {
|
|
378
|
+
const hintChunk = createLoopGuardHintChunk(responseMeta, normalizedToolCall, termination);
|
|
379
|
+
log.debug("Soft-blocking tool loop guard in validation path (emitting hint)", {
|
|
380
|
+
tool: normalizedToolCall.function.name,
|
|
381
|
+
fingerprint: termination.fingerprint,
|
|
382
|
+
repeatCount: termination.repeatCount,
|
|
383
|
+
});
|
|
384
|
+
await onToolResult(hintChunk);
|
|
385
|
+
return { intercepted: false, skipConverter: true };
|
|
386
|
+
}
|
|
348
387
|
return { intercepted: false, skipConverter: true, terminate: termination };
|
|
349
388
|
}
|
|
350
389
|
const reroutedWrite = tryRerouteEditToWrite(
|
|
@@ -415,6 +454,16 @@ export async function handleToolLoopEventV1(
|
|
|
415
454
|
|
|
416
455
|
const termination = evaluateToolLoopGuard(toolLoopGuard, normalizedToolCall);
|
|
417
456
|
if (termination) {
|
|
457
|
+
if (termination.soft) {
|
|
458
|
+
const hintChunk = createLoopGuardHintChunk(responseMeta, normalizedToolCall, termination);
|
|
459
|
+
log.debug("Soft-blocking tool loop guard (emitting hint)", {
|
|
460
|
+
tool: normalizedToolCall.function.name,
|
|
461
|
+
fingerprint: termination.fingerprint,
|
|
462
|
+
repeatCount: termination.repeatCount,
|
|
463
|
+
});
|
|
464
|
+
await onToolResult(hintChunk);
|
|
465
|
+
return { intercepted: false, skipConverter: true };
|
|
466
|
+
}
|
|
418
467
|
return { intercepted: false, skipConverter: true, terminate: termination };
|
|
419
468
|
}
|
|
420
469
|
await onInterceptedToolCall(normalizedToolCall);
|
|
@@ -515,6 +564,11 @@ function evaluateToolLoopGuard(
|
|
|
515
564
|
};
|
|
516
565
|
}
|
|
517
566
|
|
|
567
|
+
// First trigger (repeatCount exactly one over threshold): soft block.
|
|
568
|
+
// Emit a hint to the model instead of killing the stream.
|
|
569
|
+
// If the model ignores the hint and retries, subsequent triggers are hard kills.
|
|
570
|
+
const isFirstTrigger = decision.repeatCount === decision.maxRepeat + 1;
|
|
571
|
+
|
|
518
572
|
return {
|
|
519
573
|
reason: "loop_guard",
|
|
520
574
|
message: `Tool loop guard stopped repeated failing calls to "${toolCall.function.name}" `
|
|
@@ -525,6 +579,7 @@ function evaluateToolLoopGuard(
|
|
|
525
579
|
repeatCount: decision.repeatCount,
|
|
526
580
|
maxRepeat: decision.maxRepeat,
|
|
527
581
|
errorClass: decision.errorClass,
|
|
582
|
+
soft: isFirstTrigger,
|
|
528
583
|
};
|
|
529
584
|
}
|
|
530
585
|
|
|
@@ -570,12 +625,15 @@ function evaluateSchemaValidationLoopGuard(
|
|
|
570
625
|
return null;
|
|
571
626
|
}
|
|
572
627
|
|
|
573
|
-
|
|
628
|
+
const isFirstTrigger = decision.repeatCount === decision.maxRepeat + 1;
|
|
629
|
+
|
|
630
|
+
log.debug("Tool loop guard triggered on schema validation", {
|
|
574
631
|
tool: toolCall.function.name,
|
|
575
632
|
fingerprint: decision.fingerprint,
|
|
576
633
|
repeatCount: decision.repeatCount,
|
|
577
634
|
maxRepeat: decision.maxRepeat,
|
|
578
635
|
validationSignature,
|
|
636
|
+
soft: isFirstTrigger,
|
|
579
637
|
});
|
|
580
638
|
return {
|
|
581
639
|
reason: "loop_guard",
|
|
@@ -588,6 +646,7 @@ function evaluateSchemaValidationLoopGuard(
|
|
|
588
646
|
repeatCount: decision.repeatCount,
|
|
589
647
|
maxRepeat: decision.maxRepeat,
|
|
590
648
|
errorClass: decision.errorClass,
|
|
649
|
+
soft: isFirstTrigger,
|
|
591
650
|
};
|
|
592
651
|
}
|
|
593
652
|
|
|
@@ -663,6 +722,35 @@ function createNonFatalSchemaValidationHintChunk(
|
|
|
663
722
|
};
|
|
664
723
|
}
|
|
665
724
|
|
|
725
|
+
type LoopGuardHintChunk = NonFatalSchemaValidationResultChunk;
|
|
726
|
+
|
|
727
|
+
function createLoopGuardHintChunk(
|
|
728
|
+
meta: { id: string; created: number; model: string },
|
|
729
|
+
toolCall: OpenAiToolCall,
|
|
730
|
+
termination: ToolLoopGuardTermination,
|
|
731
|
+
): LoopGuardHintChunk {
|
|
732
|
+
const content =
|
|
733
|
+
`Tool "${toolCall.function.name}" has been temporarily blocked after `
|
|
734
|
+
+ `${termination.repeatCount} repeated ${termination.errorClass} failures. `
|
|
735
|
+
+ "Do not retry this tool. Use a different approach to complete the task.";
|
|
736
|
+
return {
|
|
737
|
+
id: meta.id,
|
|
738
|
+
object: "chat.completion.chunk",
|
|
739
|
+
created: meta.created,
|
|
740
|
+
model: meta.model,
|
|
741
|
+
choices: [
|
|
742
|
+
{
|
|
743
|
+
index: 0,
|
|
744
|
+
delta: {
|
|
745
|
+
role: "assistant",
|
|
746
|
+
content,
|
|
747
|
+
},
|
|
748
|
+
finish_reason: null,
|
|
749
|
+
},
|
|
750
|
+
],
|
|
751
|
+
};
|
|
752
|
+
}
|
|
753
|
+
|
|
666
754
|
function safeArgTypeSummary(event: StreamJsonToolCallEvent): Record<string, string> {
|
|
667
755
|
try {
|
|
668
756
|
let raw: unknown;
|
|
@@ -44,6 +44,7 @@ const EXPLORATION_TOOLS = new Set([
|
|
|
44
44
|
"bash",
|
|
45
45
|
"shell",
|
|
46
46
|
"webfetch",
|
|
47
|
+
"task",
|
|
47
48
|
]);
|
|
48
49
|
|
|
49
50
|
export interface ToolLoopGuardDecision {
|
|
@@ -511,16 +512,20 @@ function evaluateWithFingerprints(
|
|
|
511
512
|
};
|
|
512
513
|
}
|
|
513
514
|
|
|
515
|
+
const isExplorationTool = EXPLORATION_TOOLS.has(toolName.toLowerCase());
|
|
516
|
+
const effectiveMaxRepeat = isExplorationTool
|
|
517
|
+
? maxRepeat * EXPLORATION_LIMIT_MULTIPLIER
|
|
518
|
+
: maxRepeat;
|
|
519
|
+
|
|
514
520
|
const strictRepeatCount = (strictCounts.get(strictFingerprint) ?? 0) + 1;
|
|
515
521
|
strictCounts.set(strictFingerprint, strictRepeatCount);
|
|
516
|
-
const strictTriggered = strictRepeatCount >
|
|
522
|
+
const strictTriggered = strictRepeatCount > effectiveMaxRepeat;
|
|
517
523
|
|
|
518
|
-
const isExplorationTool = EXPLORATION_TOOLS.has(toolName.toLowerCase());
|
|
519
524
|
if (isExplorationTool) {
|
|
520
525
|
return {
|
|
521
526
|
fingerprint: strictFingerprint,
|
|
522
527
|
repeatCount: strictRepeatCount,
|
|
523
|
-
maxRepeat,
|
|
528
|
+
maxRepeat: effectiveMaxRepeat,
|
|
524
529
|
errorClass,
|
|
525
530
|
triggered: strictTriggered,
|
|
526
531
|
tracked: true,
|
|
@@ -36,7 +36,7 @@ function debugLogToFile(message: string, data: any): void {
|
|
|
36
36
|
* Handles role:"tool" result messages and assistant tool_calls that
|
|
37
37
|
* plain text flattening would silently drop.
|
|
38
38
|
*/
|
|
39
|
-
export function buildPromptFromMessages(messages: Array<any>, tools: Array<any
|
|
39
|
+
export function buildPromptFromMessages(messages: Array<any>, tools: Array<any>, subagentNames: string[] = []): string {
|
|
40
40
|
// DEBUG: Log incoming message structure to file for root cause analysis
|
|
41
41
|
const messageSummary = messages.map((m: any, i: number) => {
|
|
42
42
|
const role = m?.role ?? "?";
|
|
@@ -98,6 +98,15 @@ export function buildPromptFromMessages(messages: Array<any>, tools: Array<any>)
|
|
|
98
98
|
`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` +
|
|
99
99
|
`Tool guidance: prefer write/edit for file changes; use bash mainly to run commands/tests.\n\nAvailable tools:\n${toolDescs}`,
|
|
100
100
|
);
|
|
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
|
+
}
|
|
101
110
|
}
|
|
102
111
|
|
|
103
112
|
for (const message of messages) {
|