fix: abort agent when stuck in tool-call loop (#185)
Add a configurable maxToolCalls safeguard (default: 100) that aborts the
session when the agent enters an infinite tool-calling loop. The stream
watchdog didn't catch this because the stream was active (sending
tool_call events), just not productive.
Configurable via lettabot.yaml:
features:
maxToolCalls: 100
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -165,6 +165,9 @@ export function configToEnv(config: LettaBotConfig): Record<string, string> {
|
||||
if (config.features?.heartbeat?.enabled) {
|
||||
env.HEARTBEAT_INTERVAL_MIN = String(config.features.heartbeat.intervalMin || 30);
|
||||
}
|
||||
if (config.features?.maxToolCalls !== undefined) {
|
||||
env.MAX_TOOL_CALLS = String(config.features.maxToolCalls);
|
||||
}
|
||||
|
||||
// Integrations - Google (Gmail polling)
|
||||
if (config.integrations?.google?.enabled && config.integrations.google.account) {
|
||||
|
||||
@@ -43,6 +43,7 @@ export interface LettaBotConfig {
|
||||
enabled: boolean;
|
||||
intervalMin?: number;
|
||||
};
|
||||
maxToolCalls?: number; // Abort if agent calls this many tools in one turn (default: 100)
|
||||
};
|
||||
|
||||
// Integrations (Google Workspace, etc.)
|
||||
|
||||
@@ -485,6 +485,15 @@ export class LettaBot {
|
||||
await finalizeMessage();
|
||||
}
|
||||
|
||||
// Detect tool-call loops: abort if agent calls too many tools without producing a result
|
||||
const maxToolCalls = this.config.maxToolCalls ?? 100;
|
||||
if (streamMsg.type === 'tool_call' && (msgTypeCounts['tool_call'] || 0) >= maxToolCalls) {
|
||||
console.error(`[Bot] Agent stuck in tool loop (${msgTypeCounts['tool_call']} tool calls, limit=${maxToolCalls}), aborting`);
|
||||
session.abort().catch(() => {});
|
||||
response = '(Agent got stuck in a tool loop and was stopped. Try sending your message again.)';
|
||||
break;
|
||||
}
|
||||
|
||||
// Log meaningful events (always, not just on type change for tools)
|
||||
if (streamMsg.type === 'tool_call') {
|
||||
const toolName = (streamMsg as any).toolName || 'unknown';
|
||||
|
||||
@@ -121,10 +121,13 @@ export interface BotConfig {
|
||||
model?: string; // e.g., 'anthropic/claude-sonnet-4-5-20250929'
|
||||
agentName?: string; // Name for the agent (set via API after creation)
|
||||
allowedTools: string[];
|
||||
|
||||
|
||||
// Skills
|
||||
skills?: SkillsConfig;
|
||||
|
||||
|
||||
// Safety
|
||||
maxToolCalls?: number; // Abort if agent calls this many tools in one turn (default: 100)
|
||||
|
||||
// Security
|
||||
allowedUsers?: string[]; // Empty = allow all
|
||||
}
|
||||
|
||||
@@ -329,6 +329,7 @@ async function main() {
|
||||
model: config.model,
|
||||
agentName: process.env.AGENT_NAME || 'LettaBot',
|
||||
allowedTools: config.allowedTools,
|
||||
maxToolCalls: process.env.MAX_TOOL_CALLS ? Number(process.env.MAX_TOOL_CALLS) : undefined,
|
||||
skills: {
|
||||
cronEnabled: config.cronEnabled,
|
||||
googleEnabled: config.polling.gmail.enabled,
|
||||
|
||||
Reference in New Issue
Block a user