feat: 工具执行超时可配置 + 修复硬编码盖过工具自定义超时的 bug

1. 新增 agent.toolExecutionTimeoutMs 配置(默认 120s,范围 10~600s),AgentSettings 加输入框

2. 修复 engine.ts 硬编码 120s 会盖过 delegate-task(5min)/web-search(5min) 等工具自定义 timeoutMs 的 bug:改为取 max(配置值, tool.timeoutMs)

3. main.ts 启动时从 ConfigService 加载;handlers.ts 监听 config:set 即时更新
This commit is contained in:
thzxx
2026-07-12 14:51:12 +08:00
parent 6d31498e6a
commit 3858f5ede3
5 changed files with 27 additions and 4 deletions
+5 -1
View File
@@ -49,6 +49,7 @@ const DEFAULT_CONFIG: AgentLoopConfig = {
maxTokens: 63488, maxTokens: 63488,
thinkingEnabled: true, thinkingEnabled: true,
thinkingEffort: 'high', 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; let toolResult: MetonaToolResult;
try { try {
toolResult = await Promise.race([ toolResult = await Promise.race([
+2
View File
@@ -73,6 +73,8 @@ export interface AgentLoopConfig {
thinkingEffort: 'low' | 'medium' | 'high' | 'max'; thinkingEffort: 'low' | 'medium' | 'high' | 'max';
/** Ollama 上下文窗口大小(num_ctx),其他 Provider 忽略 */ /** Ollama 上下文窗口大小(num_ctx),其他 Provider 忽略 */
contextLength?: number; contextLength?: number;
/** 工具执行兜底超时(ms,默认 120000),实际取 max(此值, tool.timeoutMs) */
toolExecutionTimeoutMs?: number;
} }
// ===== Agent Loop 输出 ===== // ===== Agent Loop 输出 =====
+5 -3
View File
@@ -505,6 +505,10 @@ export function registerAllIPCHandlers(
// 工具确认超时变更,即时应用到 ConfirmationHook // 工具确认超时变更,即时应用到 ConfirmationHook
confirmationHook.setConfirmationTimeout(value as number); confirmationHook.setConfirmationTimeout(value as number);
log.info(`[CONFIG] Confirmation timeout updated to ${value}ms`); 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 个核心文件状态 + 自动目录状态 // 获取当前工作空间详情:路径 + 4 个核心文件状态 + 自动目录状态
ipcMain.handle('workspace:getInfo', async () => { ipcMain.handle('workspace:getInfo', async () => {
const { existsSync, statSync } = await import('fs'); const { existsSync, statSync, readFileSync, readdirSync } = await import('fs');
const workspacePath = workspaceService.getPath(); const workspacePath = workspaceService.getPath();
const files = workspaceService.reload(); // 同步外部可能的手动修改 const files = workspaceService.reload(); // 同步外部可能的手动修改
@@ -690,7 +694,6 @@ export function registerAllIPCHandlers(
size = stat.size; size = stat.size;
mtime = stat.mtimeMs; mtime = stat.mtimeMs;
// 截取前 500 字符作为预览 // 截取前 500 字符作为预览
const { readFileSync } = await import('fs');
const content = readFileSync(filePath, 'utf-8'); const content = readFileSync(filePath, 'utf-8');
preview = content.length > 500 ? content.slice(0, 500) + '\n...(已截断)' : content; preview = content.length > 500 ? content.slice(0, 500) + '\n...(已截断)' : content;
} }
@@ -718,7 +721,6 @@ export function registerAllIPCHandlers(
exists = true; exists = true;
const stat = statSync(dirPath); const stat = statSync(dirPath);
if (stat.isDirectory()) { if (stat.isDirectory()) {
const { readdirSync } = await import('fs');
fileCount = readdirSync(dirPath).length; fileCount = readdirSync(dirPath).length;
} }
} }
+2
View File
@@ -228,6 +228,7 @@ async function initialize(): Promise<void> {
const agentTimeout = configService.get<number>('agent.totalTimeoutMs'); const agentTimeout = configService.get<number>('agent.totalTimeoutMs');
const agentThinkingEnabled = configService.get<boolean>('agent.enableThinking'); const agentThinkingEnabled = configService.get<boolean>('agent.enableThinking');
const agentThinkingEffort = configService.get<string>('agent.thinkingEffort') as 'low' | 'medium' | 'high' | 'max' | null; const agentThinkingEffort = configService.get<string>('agent.thinkingEffort') as 'low' | 'medium' | 'high' | 'max' | null;
const toolExecTimeout = configService.get<number>('agent.toolExecutionTimeoutMs');
const agentLoop = new AgentLoopEngine( const agentLoop = new AgentLoopEngine(
{ {
maxIterations: agentMaxIter ?? 20, maxIterations: agentMaxIter ?? 20,
@@ -235,6 +236,7 @@ async function initialize(): Promise<void> {
contextLength: ollamaNumCtx ?? undefined, contextLength: ollamaNumCtx ?? undefined,
thinkingEnabled: agentThinkingEnabled ?? true, thinkingEnabled: agentThinkingEnabled ?? true,
thinkingEffort: agentThinkingEffort ?? 'high', thinkingEffort: agentThinkingEffort ?? 'high',
toolExecutionTimeoutMs: toolExecTimeout ?? 120_000,
}, },
adapter, toolRegistry, preToolHooks, postToolHooks, adapter, toolRegistry, preToolHooks, postToolHooks,
); );
+13
View File
@@ -369,6 +369,7 @@ function AgentSettings() {
const [thinking, setThinking] = useConfig('agent.enableThinking', true); const [thinking, setThinking] = useConfig('agent.enableThinking', true);
const [thinkingEffort, setThinkingEffort] = useConfig('agent.thinkingEffort', 'high'); const [thinkingEffort, setThinkingEffort] = useConfig('agent.thinkingEffort', 'high');
const [confirmTimeout, setConfirmTimeout] = useConfig('agent.confirmationTimeoutMs', 120000); 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]; 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 } }} slotProps={{ htmlInput: { min: 120, max: 3600, step: 30 } }}
/> />
<TextField
size="small"
label="工具执行超时(秒)"
type="number"
value={toolExecTimeout / 1000}
onChange={(e) => {
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 秒)"
/>
<TextField <TextField
size="small" size="small"
label="工具确认超时(秒)" label="工具确认超时(秒)"