diff --git a/electron/harness/agent-loop/engine.ts b/electron/harness/agent-loop/engine.ts index 664d3a2..a19f977 100644 --- a/electron/harness/agent-loop/engine.ts +++ b/electron/harness/agent-loop/engine.ts @@ -49,6 +49,7 @@ const DEFAULT_CONFIG: AgentLoopConfig = { maxTokens: 63488, thinkingEnabled: true, thinkingEffort: 'high', + toolExecutionTimeoutMs: 120_000, }; /** @@ -574,7 +575,10 @@ export class AgentLoopEngine extends EventEmitter { } // 执行工具(带超时) - const toolTimeout = 120_000; // 单个工具最大 2 分钟 + // 兜底超时取 max(配置值, 工具自定义 timeoutMs),确保工具能跑满自己声明的超时 + const configuredTimeout = this.config.toolExecutionTimeoutMs ?? 120_000; + const toolDef = this.toolRegistry.get(toolCall.name)?.definition; + const toolTimeout = Math.max(configuredTimeout, toolDef?.timeoutMs ?? 0); let toolResult: MetonaToolResult; try { toolResult = await Promise.race([ diff --git a/electron/harness/agent-loop/types.ts b/electron/harness/agent-loop/types.ts index 2b6d1b3..8deaedb 100644 --- a/electron/harness/agent-loop/types.ts +++ b/electron/harness/agent-loop/types.ts @@ -73,6 +73,8 @@ export interface AgentLoopConfig { thinkingEffort: 'low' | 'medium' | 'high' | 'max'; /** Ollama 上下文窗口大小(num_ctx),其他 Provider 忽略 */ contextLength?: number; + /** 工具执行兜底超时(ms,默认 120000),实际取 max(此值, tool.timeoutMs) */ + toolExecutionTimeoutMs?: number; } // ===== Agent Loop 输出 ===== diff --git a/electron/ipc/handlers.ts b/electron/ipc/handlers.ts index cff1541..b2c2220 100644 --- a/electron/ipc/handlers.ts +++ b/electron/ipc/handlers.ts @@ -505,6 +505,10 @@ export function registerAllIPCHandlers( // 工具确认超时变更,即时应用到 ConfirmationHook confirmationHook.setConfirmationTimeout(value as number); log.info(`[CONFIG] Confirmation timeout updated to ${value}ms`); + } else if (key === 'agent.toolExecutionTimeoutMs') { + // 工具执行兜底超时变更,即时应用到 Engine + agentLoop.updateConfig({ toolExecutionTimeoutMs: value as number }); + log.info(`[CONFIG] Tool execution timeout updated to ${value}ms`); } // 日志级别变更时即时应用 @@ -670,7 +674,7 @@ export function registerAllIPCHandlers( // 获取当前工作空间详情:路径 + 4 个核心文件状态 + 自动目录状态 ipcMain.handle('workspace:getInfo', async () => { - const { existsSync, statSync } = await import('fs'); + const { existsSync, statSync, readFileSync, readdirSync } = await import('fs'); const workspacePath = workspaceService.getPath(); const files = workspaceService.reload(); // 同步外部可能的手动修改 @@ -690,7 +694,6 @@ export function registerAllIPCHandlers( size = stat.size; mtime = stat.mtimeMs; // 截取前 500 字符作为预览 - const { readFileSync } = await import('fs'); const content = readFileSync(filePath, 'utf-8'); preview = content.length > 500 ? content.slice(0, 500) + '\n...(已截断)' : content; } @@ -718,7 +721,6 @@ export function registerAllIPCHandlers( exists = true; const stat = statSync(dirPath); if (stat.isDirectory()) { - const { readdirSync } = await import('fs'); fileCount = readdirSync(dirPath).length; } } diff --git a/electron/main.ts b/electron/main.ts index e074654..8e1d3e6 100644 --- a/electron/main.ts +++ b/electron/main.ts @@ -228,6 +228,7 @@ async function initialize(): Promise { const agentTimeout = configService.get('agent.totalTimeoutMs'); const agentThinkingEnabled = configService.get('agent.enableThinking'); const agentThinkingEffort = configService.get('agent.thinkingEffort') as 'low' | 'medium' | 'high' | 'max' | null; + const toolExecTimeout = configService.get('agent.toolExecutionTimeoutMs'); const agentLoop = new AgentLoopEngine( { maxIterations: agentMaxIter ?? 20, @@ -235,6 +236,7 @@ async function initialize(): Promise { contextLength: ollamaNumCtx ?? undefined, thinkingEnabled: agentThinkingEnabled ?? true, thinkingEffort: agentThinkingEffort ?? 'high', + toolExecutionTimeoutMs: toolExecTimeout ?? 120_000, }, adapter, toolRegistry, preToolHooks, postToolHooks, ); diff --git a/src/components/settings/SettingsModal.tsx b/src/components/settings/SettingsModal.tsx index 462f5ec..6435953 100644 --- a/src/components/settings/SettingsModal.tsx +++ b/src/components/settings/SettingsModal.tsx @@ -369,6 +369,7 @@ function AgentSettings() { const [thinking, setThinking] = useConfig('agent.enableThinking', true); const [thinkingEffort, setThinkingEffort] = useConfig('agent.thinkingEffort', 'high'); const [confirmTimeout, setConfirmTimeout] = useConfig('agent.confirmationTimeoutMs', 120000); + const [toolExecTimeout, setToolExecTimeout] = useConfig('agent.toolExecutionTimeoutMs', 120000); const MAX_ITER_OPTIONS = [10, 20, 50, 85, 128, 256, 512]; @@ -394,6 +395,18 @@ function AgentSettings() { }} slotProps={{ htmlInput: { min: 120, max: 3600, step: 30 } }} /> + { + const v = Number(e.target.value); + if (v >= 10 && v <= 600) setToolExecTimeout(v * 1000); + }} + slotProps={{ htmlInput: { min: 10, max: 600, step: 10 } }} + helperText="单个工具执行的最大时长,超时自动终止(10~600 秒)" + />