diff --git a/CHANGELOG.md b/CHANGELOG.md index b44c2763..08670987 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,14 @@ # Changelog +## 2026-08-21 + +### Changes + +- A browser command that fails now reports a short error instead of Playwright's full retry log. + Click, hover, key press, form and back/reset failures used to carry every wait and retry line, + the same reason repeated once per attempt, and terminal color codes — around 850 characters for + one disabled button. What is left is the element the locator resolved to and the reason it could + not be acted on, so the AI reads the blocker instead of scrolling past bookkeeping. ## 2026-08-20 ### Changes diff --git a/src/ai/tester.ts b/src/ai/tester.ts index 2b81ea60..52236355 100644 --- a/src/ai/tester.ts +++ b/src/ai/tester.ts @@ -15,6 +15,7 @@ import { detectFocusArea } from '../utils/aria.ts'; import { ErrorPageError, isErrorPage } from '../utils/error-page.ts'; import { createDebug, tag } from '../utils/logger.ts'; import { loop } from '../utils/loop.ts'; +import { compactErrorMessage } from '../utils/strings.ts'; import type { Agent, AgentDeps } from './agent.ts'; import type { Captain } from './captain.ts'; import type { Conversation } from './conversation.ts'; @@ -941,7 +942,7 @@ export class Tester extends TaskAgent implements Agent { }; if (resetAction.lastError) { - result.error = resetAction.lastError.toString(); + result.error = compactErrorMessage(resetAction.lastError); } return result; diff --git a/src/ai/tools.ts b/src/ai/tools.ts index 201d126f..b24089ef 100644 --- a/src/ai/tools.ts +++ b/src/ai/tools.ts @@ -8,6 +8,7 @@ import { type Task, TestResult } from '../test-plan.js'; import { LARGE_ARIA_CHANGE_THRESHOLD } from '../utils/aria.ts'; import { isFatalBrowserError } from '../utils/browser-errors.ts'; import { createDebug, tag } from '../utils/logger.js'; +import { compactErrorMessage } from '../utils/strings.ts'; import { pause } from '../utils/loop.js'; import { WebElement } from '../utils/web-element.ts'; import type { ToolDeps } from './agent.ts'; @@ -94,7 +95,7 @@ export function createCodeceptJSTools({ explorer, stateManager, ai }: ToolDeps, const success = await action.attempt(command, explanation); const attempt: { command: string; success: boolean; error?: string } = { command, success }; - if (action.lastError) attempt.error = action.lastError.toString(); + if (action.lastError) attempt.error = errorText(action.lastError); attempts.push(attempt); if (success) { @@ -120,7 +121,7 @@ export function createCodeceptJSTools({ explorer, stateManager, ai }: ToolDeps, for (const retryCmd of retryCommands) { if (!(await action.attempt(retryCmd, explanation))) { - attempts.push({ command: retryCmd, success: false, error: action.lastError?.toString() }); + attempts.push({ command: retryCmd, success: false, error: errorText(action.lastError) }); continue; } const toolResult = await ActionResult.fromState(stateManager.getCurrentState()!).toToolResult(previousState, retryCmd); @@ -198,7 +199,7 @@ export function createCodeceptJSTools({ explorer, stateManager, ai }: ToolDeps, for (const command of commands) { const success = await action.attempt(command, explanation); const attempt: { command: string; success: boolean; error?: string } = { command, success }; - if (action.lastError) attempt.error = action.lastError.toString(); + if (action.lastError) attempt.error = errorText(action.lastError); attempts.push(attempt); if (!success) continue; @@ -281,7 +282,7 @@ export function createCodeceptJSTools({ explorer, stateManager, ai }: ToolDeps, ); } - const errorMsg = `pressKey fallback to type() failed: ${action.lastError?.toString()}`; + const errorMsg = `pressKey fallback to type() failed: ${errorText(action.lastError)}`; await commitNote(activeNote, TestResult.FAILED, toolResult, action); return failedToolResult('pressKey', errorMsg, { ...toolResult, @@ -327,7 +328,7 @@ export function createCodeceptJSTools({ explorer, stateManager, ai }: ToolDeps, ); } - const errorMsg = `pressKey() failed: ${action.lastError?.toString()}`; + const errorMsg = `pressKey() failed: ${errorText(action.lastError)}`; await commitNote(activeNote, TestResult.FAILED, toolResult, action); return failedToolResult('pressKey', errorMsg, { ...toolResult, @@ -407,7 +408,7 @@ export function createCodeceptJSTools({ explorer, stateManager, ai }: ToolDeps, const toolResult = await ActionResult.fromState(stateManager.getCurrentState()!).toToolResult(previousState, formLocator); if (action.lastError) { - const message = action.lastError ? String(action.lastError) : 'Unknown error'; + const message = errorText(action.lastError); await commitNote(activeNote, TestResult.FAILED, toolResult, action); let formSuggestion = 'Look into error message and identify which commands passed and which failed. Continue execution using step-by-step approach using click() and form() tools.'; @@ -950,7 +951,7 @@ export function createAgentTools({ explorer, stateManager, ai, researcher, navig } const failData: Record = { suggestion: 'Try reset() to return to the starting page.' }; - if (action.lastError) failData.error = action.lastError.toString(); + if (action.lastError) failData.error = errorText(action.lastError); return failedToolResult('back', `Failed to navigate back to ${targetUrl}`, failData); }, }), @@ -1152,7 +1153,7 @@ function transformContainsCommand(command: string): string { } function errorText(error: unknown): string { - if (error instanceof Error) return error.toString(); + if (error instanceof Error) return compactErrorMessage(error); return 'Unknown error occurred'; } diff --git a/src/utils/strings.ts b/src/utils/strings.ts index 4803e45d..da9c1897 100644 --- a/src/utils/strings.ts +++ b/src/utils/strings.ts @@ -1,4 +1,5 @@ import { createHash } from 'node:crypto'; +import stripAnsi from 'strip-ansi'; export function truncateJson(input: any): string { if (!input) return ''; @@ -40,3 +41,38 @@ export function safeFilename(name: string, ext = '', maxBytes = 240): string { } return truncated + suffix + ext; } + +export function truncate(text: string, max: number): string { + if (text.length <= max) return text; + return `${text.slice(0, max - 3)}...`; +} + +const MAX_COMPACT_ERROR = 400; + +export function compactErrorMessage(error: unknown): string { + let text = stripAnsi(String(error)); + for (const strip of STRIP_STRATEGIES) { + text = strip(text); + } + return truncate(text, MAX_COMPACT_ERROR); +} + +function stripCallLog(text: string): string { + const CALL_LOG = 'Call log:'; + const NOISE = ['attempting', 'retrying', 'waiting']; + + const [headline, ...log] = text.split(CALL_LOG); + if (!log.length) return text; + + const lines = new Set(); + for (const line of log.join(CALL_LOG).split('\n')) { + const cleaned = normalizeInlineText(line); + if (!cleaned) continue; + if (NOISE.some((noise) => cleaned.includes(noise))) continue; + lines.add(cleaned); + } + + return [headline.trim(), ...lines].join(' '); +} + +const STRIP_STRATEGIES = [stripCallLog];