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:
@@ -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([
|
||||||
|
|||||||
@@ -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 输出 =====
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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="工具确认超时(秒)"
|
||||||
|
|||||||
Reference in New Issue
Block a user