@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.
@@ -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
- log17.warn("Tool loop guard triggered on schema validation", {
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 > maxRepeat;
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 prompt = buildPromptFromMessages(messages, tools);
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 prompt = buildPromptFromMessages(messages, tools);
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 systemMessage = buildAvailableToolsSystemMessage(lastToolNames, lastToolMap, mcpToolDefs, mcpToolSummaries);
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 || [];
@@ -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
- log17.warn("Tool loop guard triggered on schema validation", {
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 > maxRepeat;
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 prompt = buildPromptFromMessages(messages, tools);
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 prompt = buildPromptFromMessages(messages, tools);
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 systemMessage = buildAvailableToolsSystemMessage(lastToolNames, lastToolMap, mcpToolDefs, mcpToolSummaries);
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.18",
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 prompt = buildPromptFromMessages(messages, tools);
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 prompt = buildPromptFromMessages(messages, tools);
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 systemMessage = buildAvailableToolsSystemMessage(lastToolNames, lastToolMap, mcpToolDefs, mcpToolSummaries);
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
- log.warn("Tool loop guard triggered on schema validation", {
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 > maxRepeat;
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>): string {
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) {