fix(agent): detect repeated tool call loops and make max_iterations configurable
Local LLMs often get stuck calling the same tool repeatedly because they lack the sophistication to synthesize results. The agent loop had no safeguard — it re-executed whatever the model requested up to 10 times. Add fingerprint-based loop detection: if the same tool+args combination repeats 3 consecutive times, break the loop and return the last results. Also add agents.max_iterations to the config schema so the iteration limit is user-configurable (default: 10).
This commit is contained in:
@@ -143,6 +143,13 @@ export class NativeAgent {
|
||||
content: m.content,
|
||||
}));
|
||||
|
||||
// Track consecutive identical tool call fingerprints to detect loops.
|
||||
// Local LLMs are especially prone to repeatedly requesting the same tool call.
|
||||
let lastFingerprint: string | undefined;
|
||||
let consecutiveRepeats = 0;
|
||||
const maxConsecutiveRepeats = 3;
|
||||
let lastToolResults: string[] = [];
|
||||
|
||||
for (let iteration = 0; iteration < this.maxIterations; iteration++) {
|
||||
// Build request — cast loopMessages to Message[] because the underlying
|
||||
// model client will pass them through to the API which accepts structured content.
|
||||
@@ -170,6 +177,19 @@ export class NativeAgent {
|
||||
return finalContent;
|
||||
}
|
||||
|
||||
// Check for repeated tool calls — build a fingerprint from tool names + args
|
||||
const fingerprint = response.toolCalls
|
||||
.map(tc => `${tc.name}:${JSON.stringify(tc.args)}`)
|
||||
.sort()
|
||||
.join('|');
|
||||
|
||||
if (fingerprint === lastFingerprint) {
|
||||
consecutiveRepeats++;
|
||||
} else {
|
||||
consecutiveRepeats = 1;
|
||||
lastFingerprint = fingerprint;
|
||||
}
|
||||
|
||||
// Build the assistant message with tool_use content blocks
|
||||
const assistantContent: unknown[] = [];
|
||||
if (response.content) {
|
||||
@@ -187,6 +207,7 @@ export class NativeAgent {
|
||||
|
||||
// Execute each tool call and collect results
|
||||
const toolResultBlocks: unknown[] = [];
|
||||
lastToolResults = [];
|
||||
for (const tc of response.toolCalls) {
|
||||
const internalName = this.toolRegistry!.getByApiName(tc.name)?.name ?? tc.name;
|
||||
this.onToolUse?.({ type: 'start', tool: internalName, args: tc.args });
|
||||
@@ -195,16 +216,31 @@ export class NativeAgent {
|
||||
|
||||
this.onToolUse?.({ type: 'end', tool: internalName, result });
|
||||
|
||||
const resultContent = result.success ? result.output : (result.error ?? 'Unknown error');
|
||||
toolResultBlocks.push({
|
||||
type: 'tool_result',
|
||||
tool_use_id: tc.id,
|
||||
content: result.success ? result.output : (result.error ?? 'Unknown error'),
|
||||
content: resultContent,
|
||||
is_error: !result.success,
|
||||
});
|
||||
if (result.success && result.output) {
|
||||
lastToolResults.push(result.output);
|
||||
}
|
||||
}
|
||||
|
||||
// Add tool results as a user message
|
||||
loopMessages.push({ role: 'user', content: toolResultBlocks });
|
||||
|
||||
// Break out if the model is stuck in a repeated tool call loop
|
||||
if (consecutiveRepeats >= maxConsecutiveRepeats) {
|
||||
const toolOutput = lastToolResults.length > 0
|
||||
? lastToolResults.join('\n\n')
|
||||
: 'No results available.';
|
||||
const breakMsg = `Tool loop detected (same tool called ${consecutiveRepeats} times). Returning last results:\n\n${toolOutput}`;
|
||||
const assistantMsg: Message = { role: 'assistant', content: breakMsg };
|
||||
this.addToHistory(assistantMsg);
|
||||
return breakMsg;
|
||||
}
|
||||
}
|
||||
|
||||
// Max iterations reached
|
||||
|
||||
Reference in New Issue
Block a user