diff --git a/packages/core/src/agent/registry.ts b/packages/core/src/agent/registry.ts index 51090d6..fbd5ebe 100644 --- a/packages/core/src/agent/registry.ts +++ b/packages/core/src/agent/registry.ts @@ -156,8 +156,8 @@ export class AgentRegistry { * 应用全局配置到 Agent */ private applyGlobalConfig(agent: AgentInfo): AgentInfo { - // 合并 maxSteps - const maxSteps = agent.maxSteps ?? this.globalConfig?.maxSteps ?? 10; + // 合并 maxSteps(默认 50,提供足够的执行空间) + const maxSteps = agent.maxSteps ?? this.globalConfig?.maxSteps ?? 50; // 合并模型配置 const model = { diff --git a/packages/core/src/core/agent.ts b/packages/core/src/core/agent.ts index 69f8038..f37d4fb 100644 --- a/packages/core/src/core/agent.ts +++ b/packages/core/src/core/agent.ts @@ -22,6 +22,11 @@ import { loadVisionConfig } from '../utils/config.js'; import { getProviderRegistry, resolveApiKey } from '../provider/index.js'; import { getHookManager } from '../hooks/index.js'; import { getGitManager } from '../git/index.js'; +import { + createDoomLoopDetector, + type DoomLoopDetector, + DOOM_LOOP_WARNING, +} from './doom-loop.js'; /** * 工具调用开始事件信息 @@ -43,6 +48,14 @@ export interface ToolEndInfo { duration?: number; } +/** + * Doom Loop 检测事件信息 + */ +export interface DoomLoopInfo { + toolName: string; + count: number; +} + /** * Agent.chat() 选项 */ @@ -52,6 +65,8 @@ export interface AgentChatOptions { onToolStart?: (info: ToolStartInfo) => void; /** 工具执行完成回调 */ onToolEnd?: (info: ToolEndInfo) => void; + /** Doom Loop 检测回调 */ + onDoomLoop?: (info: DoomLoopInfo) => void; abortSignal?: AbortSignal; } @@ -72,6 +87,9 @@ export class Agent { // 压缩管理器 private compressionManager: CompressionManager; + // Doom Loop 检测器 + private doomLoopDetector: DoomLoopDetector = createDoomLoopDetector(); + // 当前 Agent 模式(null 表示默认模式) private currentAgentMode: AgentInfo | null = null; @@ -348,10 +366,15 @@ export class Agent { async chat(userMessage: string | UserInput, options?: AgentChatOptions | ((text: string) => void)): Promise { // 兼容旧的 onStream 参数 const opts: AgentChatOptions = typeof options === 'function' ? { onStream: options } : (options || {}); - const { onStream, onToolStart, onToolEnd, abortSignal } = opts; + const { onStream, onToolStart, onToolEnd, onDoomLoop, abortSignal } = opts; + + // 重置 doom loop 检测器(每次对话开始时) + this.doomLoopDetector.reset(); // 工具调用时间跟踪 const toolStartTimes = new Map(); + // Doom loop 检测状态 + let doomLoopTriggered = false; // 处理带图片的消息 let processedMessage = userMessage; @@ -422,13 +445,16 @@ export class Agent { if (onStream) { // 流式模式 + // 获取当前 Agent 的 maxSteps 配置(默认 50) + const maxSteps = this.currentAgentMode?.maxSteps ?? 50; + const result = streamText({ model: this.getModel(this.config.model), system: this.config.systemPrompt, messages: this.conversationHistory, - tools: vercelTools, + tools: doomLoopTriggered ? {} : vercelTools, // doom loop 时禁用工具 maxOutputTokens: this.config.maxTokens, - stopWhen: stepCountIs(10), // 允许最多 10 轮工具调用 + stopWhen: stepCountIs(maxSteps), abortSignal, // 支持取消 onChunk: ({ chunk }) => { if (chunk.type === 'tool-call') { @@ -436,6 +462,25 @@ export class Agent { const toolCallChunk = chunk as { toolCallId: string; toolName: string; input: unknown }; const toolCallId = toolCallChunk.toolCallId || `tool-${Date.now()}`; + // Doom Loop 检测:记录工具调用 + this.doomLoopDetector.record( + toolCallChunk.toolName, + toolCallChunk.input + ); + + // 检查是否触发 doom loop + if (this.doomLoopDetector.isTriggered() && !doomLoopTriggered) { + doomLoopTriggered = true; + const toolName = this.doomLoopDetector.getLastToolName() || toolCallChunk.toolName; + + // 通知回调 + onDoomLoop?.({ toolName, count: 3 }); + + // 输出警告 + onStream?.(`\n[警告: 检测到 Doom Loop - ${toolName} 被重复调用]\n`); + onStream?.(DOOM_LOOP_WARNING); + } + // 记录开始时间 toolStartTimes.set(toolCallId, Date.now()); @@ -534,13 +579,16 @@ export class Agent { } } else { // 非流式模式 + // 获取当前 Agent 的 maxSteps 配置(默认 50) + const maxSteps = this.currentAgentMode?.maxSteps ?? 50; + const result = await generateText({ model: this.getModel(this.config.model), system: this.config.systemPrompt, messages: this.conversationHistory, tools: vercelTools, maxOutputTokens: this.config.maxTokens, - stopWhen: stepCountIs(10), // 允许最多 10 轮工具调用 + stopWhen: stepCountIs(maxSteps), abortSignal, // 支持取消 }); diff --git a/packages/core/src/core/doom-loop.ts b/packages/core/src/core/doom-loop.ts new file mode 100644 index 0000000..69ba5c8 --- /dev/null +++ b/packages/core/src/core/doom-loop.ts @@ -0,0 +1,136 @@ +/** + * Doom Loop 检测器 + * + * 检测模型是否陷入重复工具调用的死循环。 + * 参考 OpenCode 和 OpenHands 的实现。 + */ + +import { createHash } from 'crypto'; + +/** + * 工具调用记录 + */ +export interface ToolCallRecord { + toolName: string; + inputHash: string; +} + +/** + * Doom Loop 检测器接口 + */ +export interface DoomLoopDetector { + /** 记录工具调用 */ + record(toolName: string, input: unknown): void; + /** 检测是否触发 doom loop */ + isTriggered(): boolean; + /** 重置记录(新对话时调用) */ + reset(): void; + /** 获取当前状态描述(用于日志) */ + getStatus(): string; + /** 获取最后重复的工具名称(触发时使用) */ + getLastToolName(): string | null; +} + +/** + * Doom Loop 检测阈值 + * 连续 N 次相同的工具调用(相同工具名 + 相同参数)触发检测 + */ +export const DOOM_LOOP_THRESHOLD = 3; + +/** + * 计算输入参数的哈希值 + */ +function hashInput(input: unknown): string { + const str = JSON.stringify(input, Object.keys(input as object).sort()); + return createHash('md5').update(str).digest('hex').slice(0, 16); +} + +/** + * 创建 Doom Loop 检测器 + */ +export function createDoomLoopDetector(threshold = DOOM_LOOP_THRESHOLD): DoomLoopDetector { + const history: ToolCallRecord[] = []; + let triggered = false; + let lastToolName: string | null = null; + + return { + record(toolName: string, input: unknown): void { + const inputHash = hashInput(input); + history.push({ toolName, inputHash }); + + // 只保留最近 threshold 条记录 + if (history.length > threshold) { + history.shift(); + } + + // 检测是否触发 + if (history.length >= threshold) { + const lastN = history.slice(-threshold); + const first = lastN[0]; + const allSame = lastN.every( + (call) => call.toolName === first.toolName && call.inputHash === first.inputHash + ); + + if (allSame) { + triggered = true; + lastToolName = first.toolName; + } + } + }, + + isTriggered(): boolean { + return triggered; + }, + + reset(): void { + history.length = 0; + triggered = false; + lastToolName = null; + }, + + getStatus(): string { + if (triggered) { + return `Doom loop detected: ${lastToolName} called ${threshold} times with same input`; + } + return `Normal: ${history.length} calls recorded`; + }, + + getLastToolName(): string | null { + return lastToolName; + }, + }; +} + +/** + * MAX_STEPS 达到时的警告提示 + * 注入到 system prompt 中,让模型输出总结 + */ +export const MAX_STEPS_WARNING = ` +CRITICAL - MAXIMUM STEPS REACHED + +The maximum number of steps allowed for this task has been reached. Tools are disabled until next user input. Respond with text only. + +STRICT REQUIREMENTS: +1. Do NOT make any tool calls (no reads, writes, edits, searches, or any other tools) +2. MUST provide a text response summarizing work done so far +3. This constraint overrides ALL other instructions + +Please summarize: +1. What has been completed +2. What remains to be done +3. Any recommendations for next steps +`; + +/** + * Doom Loop 触发时的警告提示 + */ +export const DOOM_LOOP_WARNING = ` +WARNING - DOOM LOOP DETECTED + +The same tool call has been repeated multiple times with identical inputs, indicating a potential infinite loop. Tools are temporarily disabled. + +Please: +1. Explain what you were trying to accomplish +2. Describe why the repeated calls might not be working +3. Suggest an alternative approach +`; diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 604c792..c28b0e1 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -1,5 +1,14 @@ export { Agent } from './core/agent.js'; -export type { AgentChatOptions, ToolStartInfo, ToolEndInfo } from './core/agent.js'; +export type { AgentChatOptions, ToolStartInfo, ToolEndInfo, DoomLoopInfo } from './core/agent.js'; + +// Doom Loop Detection +export { + createDoomLoopDetector, + DOOM_LOOP_THRESHOLD, + DOOM_LOOP_WARNING, + MAX_STEPS_WARNING, +} from './core/doom-loop.js'; +export type { DoomLoopDetector } from './core/doom-loop.js'; export { toolRegistry, todoManager, initTaskContext, updateTaskDescription, updateSkillDescription } from './tools/index.js'; export { loadConfig, saveConfig, getConfig, loadVisionConfig, ConfigurationError } from './utils/config.js'; export type { VisionConfig } from './utils/config.js';