diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index e9e57b209..a4402d511 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -1,6 +1,7 @@ # Changelog ## [Unreleased] + ### Added - Added `isolated` option to run tasks in isolated git worktrees @@ -9,6 +10,13 @@ ### Changed +- Updated bash tool timeout parameter to default 300 seconds with auto-conversion from milliseconds +- Updated python tool timeout parameter from timeout_ms to timeout with default 30 seconds +- Updated ssh tool timeout parameter to default 60 seconds with auto-conversion from milliseconds +- Updated gemini-image tool timeout parameter from timeout_seconds to timeout with default 120 seconds +- Updated web-fetch tool timeout parameter to remove maximum limit and improve handling +- Updated web-search tool parameter from num_results to limit for result count +- Improved task output formatting to display single variables inline without tree structure - Updated MCP tool name handling to use direct server and tool name properties instead of parsing normalized names - Improved MCP tool metadata extraction to use explicit mcpToolName and mcpServerName properties - Updated edit tool parameters from camelCase to snake_case (oldText → old_text, newText → new_text) @@ -26,6 +34,11 @@ - Removed model parameter from gemini-image tool to use provider defaults - Improved variable display in task output with humanized keys for single variables +### Fixed + +- Added timeout clamping to reasonable ranges across all tools (1s to 3600s for bash/ssh, 1s to 600s for python/gemini-image) +- Fixed timeout parameter handling to auto-convert milliseconds to seconds when value exceeds 1000 + ## [7.0.0] - 2026-01-21 ### Added diff --git a/packages/coding-agent/src/core/tools/bash.ts b/packages/coding-agent/src/core/tools/bash.ts index 60d628aca..254c44c23 100644 --- a/packages/coding-agent/src/core/tools/bash.ts +++ b/packages/coding-agent/src/core/tools/bash.ts @@ -19,7 +19,7 @@ export const BASH_DEFAULT_PREVIEW_LINES = 10; const bashSchema = Type.Object({ command: Type.String({ description: "Command to execute" }), - timeout: Type.Optional(Type.Number({ description: "Timeout in seconds" })), + timeout: Type.Optional(Type.Number({ description: "Timeout in seconds (default: 300)" })), cwd: Type.Optional(Type.String({ description: "Working directory (default: cwd)" })), }); @@ -51,7 +51,7 @@ export class BashTool implements AgentTool { public async execute( _toolCallId: string, - { command, timeout, cwd }: { command: string; timeout?: number; cwd?: string }, + { command, timeout: rawTimeout = 300, cwd }: { command: string; timeout?: number; cwd?: string }, signal?: AbortSignal, onUpdate?: AgentToolUpdateCallback, ctx?: AgentToolContext, @@ -82,12 +82,18 @@ export class BashTool implements AgentTool { throw new Error(`Working directory is not a directory: ${commandCwd}`); } + // Auto-convert milliseconds to seconds if value > 1000 (16+ min is unreasonable) + let timeoutSec = rawTimeout > 1000 ? rawTimeout / 1000 : rawTimeout; + // Clamp to reasonable range: 1s - 3600s (1 hour) + timeoutSec = Math.max(1, Math.min(3600, timeoutSec)); + const timeoutMs = timeoutSec * 1000; + // Track output for streaming updates let currentOutput = ""; const executorOptions: BashExecutorOptions = { cwd: commandCwd, - timeout: timeout ? timeout * 1000 : undefined, // Convert to milliseconds + timeout: timeoutMs, signal, onChunk: (chunk) => { currentOutput += chunk; diff --git a/packages/coding-agent/src/core/tools/gemini-image.ts b/packages/coding-agent/src/core/tools/gemini-image.ts index 0367f7ce8..7170e4288 100644 --- a/packages/coding-agent/src/core/tools/gemini-image.ts +++ b/packages/coding-agent/src/core/tools/gemini-image.ts @@ -105,13 +105,7 @@ const baseImageSchema = Type.Object( description: "Optional input images for edits or variations.", }), ), - timeout_seconds: Type.Optional( - Type.Number({ - description: `Request timeout in seconds (default: ${DEFAULT_TIMEOUT_SECONDS}).`, - minimum: 1, - maximum: 600, - }), - ), + timeout: Type.Optional(Type.Number({ description: "Timeout in seconds (default: 120)" })), }, { additionalProperties: false }, ); @@ -480,7 +474,11 @@ export const geminiImageTool: CustomTool 1000 (16+ min is unreasonable) + let timeoutSeconds = rawTimeout > 1000 ? rawTimeout / 1000 : rawTimeout; + // Clamp to reasonable range: 1s - 600s (10 min) + timeoutSeconds = Math.max(1, Math.min(600, timeoutSeconds)); const requestSignal = createRequestSignal(signal, timeoutSeconds); if (provider === "openrouter") { diff --git a/packages/coding-agent/src/core/tools/python.ts b/packages/coding-agent/src/core/tools/python.ts index 26ff14165..abc02ef51 100644 --- a/packages/coding-agent/src/core/tools/python.ts +++ b/packages/coding-agent/src/core/tools/python.ts @@ -46,7 +46,7 @@ export const pythonSchema = Type.Object({ }), { description: "Cells to execute sequentially in persistent kernel" }, ), - timeout_ms: Type.Optional(Type.Number({ description: "Timeout in ms (default: 30000)" })), + timeout: Type.Optional(Type.Number({ description: "Timeout in seconds (default: 30)" })), cwd: Type.Optional(Type.String({ description: "Working directory (default: cwd)" })), reset: Type.Optional(Type.Boolean({ description: "Restart kernel before execution" })), }); @@ -166,7 +166,12 @@ export class PythonTool implements AgentTool { throw new Error("Python tool requires a session when not using proxy executor"); } - const { cells, timeout_ms = 30000, cwd, reset } = params; + const { cells, timeout: rawTimeout = 30, cwd, reset } = params; + // Auto-convert milliseconds to seconds if value > 1000 (16+ min is unreasonable) + let timeoutSec = rawTimeout > 1000 ? rawTimeout / 1000 : rawTimeout; + // Clamp to reasonable range: 1s - 600s (10 min) + timeoutSec = Math.max(1, Math.min(600, timeoutSec)); + const timeoutMs = timeoutSec * 1000; const controller = new AbortController(); const onAbort = () => controller.abort(); signal?.addEventListener("abort", onAbort, { once: true }); @@ -256,7 +261,7 @@ export class PythonTool implements AgentTool { const sessionId = sessionFile ? `session:${sessionFile}:cwd:${commandCwd}` : `cwd:${commandCwd}`; const baseExecutorOptions: Omit = { cwd: commandCwd, - timeoutMs: timeout_ms, + timeoutMs, signal: controller.signal, sessionId, kernelMode: this.session.settings?.getPythonKernelMode?.() ?? "session", diff --git a/packages/coding-agent/src/core/tools/ssh.ts b/packages/coding-agent/src/core/tools/ssh.ts index 40c61d171..83dd92d47 100644 --- a/packages/coding-agent/src/core/tools/ssh.ts +++ b/packages/coding-agent/src/core/tools/ssh.ts @@ -20,7 +20,7 @@ const sshSchema = Type.Object({ host: Type.String({ description: "Host name from ssh.json or .ssh.json" }), command: Type.String({ description: "Command to execute on the remote host" }), cwd: Type.Optional(Type.String({ description: "Remote working directory (optional)" })), - timeout: Type.Optional(Type.Number({ description: "Timeout in seconds (optional, no default timeout)" })), + timeout: Type.Optional(Type.Number({ description: "Timeout in seconds (default: 60)" })), }); export interface SSHToolDetails { @@ -143,7 +143,7 @@ export class SshTool implements AgentTool { public async execute( _toolCallId: string, - { host, command, cwd, timeout }: SshToolParams, + { host, command, cwd, timeout: rawTimeout = 60 }: SshToolParams, signal?: AbortSignal, onUpdate?: AgentToolUpdateCallback, _ctx?: AgentToolContext, @@ -159,10 +159,17 @@ export class SshTool implements AgentTool { const hostInfo = await ensureHostInfo(hostConfig); const remoteCommand = buildRemoteCommand(command, cwd, hostInfo); + + // Auto-convert milliseconds to seconds if value > 1000 (16+ min is unreasonable) + let timeoutSec = rawTimeout > 1000 ? rawTimeout / 1000 : rawTimeout; + // Clamp to reasonable range: 1s - 3600s (1 hour) + timeoutSec = Math.max(1, Math.min(3600, timeoutSec)); + const timeoutMs = timeoutSec * 1000; + let currentOutput = ""; const result = await executeSSH(hostConfig, remoteCommand, { - timeout: timeout ? timeout * 1000 : undefined, + timeout: timeoutMs, signal, compatEnabled: hostInfo.compatEnabled, onChunk: (chunk) => { diff --git a/packages/coding-agent/src/core/tools/task/render.ts b/packages/coding-agent/src/core/tools/task/render.ts index 4263e52c5..b082db7e6 100644 --- a/packages/coding-agent/src/core/tools/task/render.ts +++ b/packages/coding-agent/src/core/tools/task/render.ts @@ -278,7 +278,16 @@ function renderOutputSection( function formatVarsInline(vars: Record, theme: Theme): string { const entries = Object.entries(vars); - if (entries.length === 0) return "Vars: none"; + if (entries.length === 0) return "No variables"; + + // Single variable: show inline as "Key: value" without tree structure + if (entries.length === 1) { + const [key, value] = entries[0]; + const humanKey = humanizeKey(key); + const displayValue = `"${truncate(value, 32, theme.format.ellipsis)}"`; + return `${humanKey}: ${displayValue}`; + } + const pairs = entries.map(([key, value]) => `${key}=${truncate(value, 24, theme.format.ellipsis)}`); return `Vars: ${pairs.join(", ")}`; } diff --git a/packages/coding-agent/src/core/tools/task/worker.ts b/packages/coding-agent/src/core/tools/task/worker.ts index d78825ee0..9654fd0a3 100644 --- a/packages/coding-agent/src/core/tools/task/worker.ts +++ b/packages/coding-agent/src/core/tools/task/worker.ts @@ -366,7 +366,7 @@ function createMCPProxyTool(metadata: MCPToolMetadata): CustomTool { } function getPythonCallTimeoutMs(params: PythonToolParams): number | undefined { - const timeout = params.timeout_ms; + const timeout = params.timeout; if (typeof timeout === "number" && Number.isFinite(timeout) && timeout > 0) { return Math.max(1000, Math.round(timeout * 1000) + 1000); } diff --git a/packages/coding-agent/src/core/tools/web-fetch.ts b/packages/coding-agent/src/core/tools/web-fetch.ts index 68906fbac..fd37955b2 100644 --- a/packages/coding-agent/src/core/tools/web-fetch.ts +++ b/packages/coding-agent/src/core/tools/web-fetch.ts @@ -24,10 +24,6 @@ import { convertWithMarkitdown, fetchBinary } from "./web-scrapers/utils"; // Types and Constants // ============================================================================= -const MIN_TIMEOUT = 1_000; -const DEFAULT_TIMEOUT = 20_000; -const MAX_TIMEOUT = 45_000; - // Convertible document types (markitdown supported) const CONVERTIBLE_MIMES = new Set([ "application/pdf", @@ -246,7 +242,7 @@ async function tryMdSuffix(url: string, timeout: number, signal?: AbortSignal): if (signal?.aborted) { return null; } - const result = await loadPage(candidate, { timeout: Math.min(timeout, MAX_TIMEOUT), signal }); + const result = await loadPage(candidate, { timeout, signal }); if (result.ok && result.content.trim().length > 100 && !looksLikeHtml(result.content)) { return result.content; } @@ -873,7 +869,7 @@ async function renderUrl( const webFetchSchema = Type.Object({ url: Type.String({ description: "URL to fetch" }), - timeout: Type.Optional(Type.Number({ description: "Timeout in seconds (default: 20, max: 45)" })), + timeout: Type.Optional(Type.Number({ description: "Timeout in seconds (default: 20)" })), raw: Type.Optional(Type.Boolean({ description: "Return raw HTML without transforms" })), }); @@ -903,15 +899,18 @@ export class WebFetchTool implements AgentTool, _context?: AgentToolContext, ): Promise> { - const { url, timeout = DEFAULT_TIMEOUT, raw = false } = params; + const { url, timeout: rawTimeout = 20, raw = false } = params; + + // Auto-convert milliseconds to seconds if value > 1000 (16+ min is unreasonable) + const timeoutSec = rawTimeout > 1000 ? rawTimeout / 1000 : rawTimeout; + + // Clamp to valid range (seconds) + const effectiveTimeout = Math.min(Math.max(timeoutSec, 1), 45); if (signal?.aborted) { throw new Error("Operation aborted"); } - // Clamp timeout - const effectiveTimeout = Math.min(Math.max(timeout, MIN_TIMEOUT), MAX_TIMEOUT); - const result = await renderUrl(url, effectiveTimeout, raw, signal); // Format output diff --git a/packages/coding-agent/src/core/tools/web-search/index.ts b/packages/coding-agent/src/core/tools/web-search/index.ts index 8e3e8d075..fcb755e6d 100644 --- a/packages/coding-agent/src/core/tools/web-search/index.ts +++ b/packages/coding-agent/src/core/tools/web-search/index.ts @@ -41,7 +41,7 @@ export const webSearchSchema = Type.Object({ description: "Search provider (default: auto)", }), ), - num_results: Type.Optional(Type.Number({ description: "Max results to return" })), + limit: Type.Optional(Type.Number({ description: "Max results to return" })), // Common (Anthropic & Perplexity) system_prompt: Type.Optional(Type.String({ description: "System prompt for response style" })), @@ -76,7 +76,7 @@ export const webSearchSchema = Type.Object({ export type WebSearchParams = { query: string; provider?: "auto" | "exa" | "anthropic" | "perplexity"; - num_results?: number; + limit?: number; // Anthropic system_prompt?: string; max_tokens?: number; @@ -272,14 +272,14 @@ async function executeWebSearch( if (provider === "exa") { response = await searchExa({ query: params.query, - num_results: params.num_results, + num_results: params.limit, }); } else if (provider === "anthropic") { response = await searchAnthropic({ query: params.query, system_prompt: params.system_prompt, max_tokens: params.max_tokens, - num_results: params.num_results, + num_results: params.limit, }); } else { response = await searchPerplexity({ @@ -290,7 +290,7 @@ async function executeWebSearch( search_domain_filter: params.search_domain_filter, search_context_size: params.search_context_size, return_related_questions: params.return_related_questions, - num_results: params.num_results, + num_results: params.limit, }); } diff --git a/packages/coding-agent/test/tools/python-execution.test.ts b/packages/coding-agent/test/tools/python-execution.test.ts index 93eaa1492..dee7906e1 100644 --- a/packages/coding-agent/test/tools/python-execution.test.ts +++ b/packages/coding-agent/test/tools/python-execution.test.ts @@ -40,7 +40,7 @@ describe("python tool execution", () => { const tool = new PythonTool(createSession(tempDir.path)); const result = await tool.execute( "call-id", - { cells: [{ code: "print('hi')" }], timeout_ms: 5000, cwd: tempDir.path, reset: true }, + { cells: [{ code: "print('hi')" }], timeout: 5, cwd: tempDir.path, reset: true }, undefined, undefined, undefined,