import { DEFAULT_TOOL_EXECUTION_TIMEOUT_SEC } from "../api/constants/tool-execution.js"; import type { ToolExecutionResult } from "./types.js"; const TIMEOUT_SENTINEL = Symbol("tool-timeout"); /** * Convert a config-provided seconds value to a safe milliseconds value, * falling back to the default if the input is NaN, non-finite, zero, or negative. * * `fallbackMs` lets callers governed by a different budget (e.g. the inline * grant wait) keep their own floor instead of inheriting the tool-execution * default. */ export function safeTimeoutMs( sec: unknown, fallbackMs: number = DEFAULT_TOOL_EXECUTION_TIMEOUT_SEC * 1000, ): number { const n = Number(sec); if (!Number.isFinite(n) || n <= 0) { return fallbackMs; } return n * 1000; } /** * Race a tool execution promise against a timeout. Returns a timeout error * result instead of throwing so the agent loop can continue gracefully. */ export async function executeWithTimeout( promise: Promise, timeoutMs: number, toolName: string, ): Promise { // Guard against NaN/invalid values that would cause setTimeout to fire immediately const safeMs = Number.isFinite(timeoutMs) && timeoutMs > 0 ? timeoutMs : DEFAULT_TOOL_EXECUTION_TIMEOUT_SEC * 1000; let timeoutHandle: ReturnType; const timeoutPromise = new Promise((resolve) => { timeoutHandle = setTimeout(() => resolve(TIMEOUT_SENTINEL), safeMs); }); try { const result = await Promise.race([promise, timeoutPromise]); if (result === TIMEOUT_SENTINEL) { const sec = Math.round(safeMs / 1000); return { content: `Tool "${toolName}" timed out after ${sec}s. The operation may still be running in the background. Consider increasing timeouts.toolExecutionTimeoutSec in the config.`, isError: true, }; } return result; } finally { clearTimeout(timeoutHandle!); } }