/ utils / toolErrors.ts
toolErrors.ts
  1  import type { ZodError } from 'zod/v4'
  2  import { AbortError, ShellError } from './errors.js'
  3  import { INTERRUPT_MESSAGE_FOR_TOOL_USE } from './messages.js'
  4  
  5  export function formatError(error: unknown): string {
  6    if (error instanceof AbortError) {
  7      return error.message || INTERRUPT_MESSAGE_FOR_TOOL_USE
  8    }
  9    if (!(error instanceof Error)) {
 10      return String(error)
 11    }
 12    const parts = getErrorParts(error)
 13    const fullMessage =
 14      parts.filter(Boolean).join('\n').trim() || 'Command failed with no output'
 15    if (fullMessage.length <= 10000) {
 16      return fullMessage
 17    }
 18    const halfLength = 5000
 19    const start = fullMessage.slice(0, halfLength)
 20    const end = fullMessage.slice(-halfLength)
 21    return `${start}\n\n... [${fullMessage.length - 10000} characters truncated] ...\n\n${end}`
 22  }
 23  
 24  export function getErrorParts(error: Error): string[] {
 25    if (error instanceof ShellError) {
 26      return [
 27        `Exit code ${error.code}`,
 28        error.interrupted ? INTERRUPT_MESSAGE_FOR_TOOL_USE : '',
 29        error.stderr,
 30        error.stdout,
 31      ]
 32    }
 33    const parts = [error.message]
 34    if ('stderr' in error && typeof error.stderr === 'string') {
 35      parts.push(error.stderr)
 36    }
 37    if ('stdout' in error && typeof error.stdout === 'string') {
 38      parts.push(error.stdout)
 39    }
 40    return parts
 41  }
 42  
 43  /**
 44   * Formats a Zod validation path into a readable string
 45   * e.g., ['todos', 0, 'activeForm'] => 'todos[0].activeForm'
 46   */
 47  function formatValidationPath(path: PropertyKey[]): string {
 48    if (path.length === 0) return ''
 49  
 50    return path.reduce((acc, segment, index) => {
 51      const segmentStr = String(segment)
 52      if (typeof segment === 'number') {
 53        return `${String(acc)}[${segmentStr}]`
 54      }
 55      return index === 0 ? segmentStr : `${String(acc)}.${segmentStr}`
 56    }, '') as string
 57  }
 58  
 59  /**
 60   * Converts Zod validation errors into a human-readable and LLM friendly error message
 61   *
 62   * @param toolName The name of the tool that failed validation
 63   * @param error The Zod error object
 64   * @returns A formatted error message string
 65   */
 66  export function formatZodValidationError(
 67    toolName: string,
 68    error: ZodError,
 69  ): string {
 70    const missingParams = error.issues
 71      .filter(
 72        err =>
 73          err.code === 'invalid_type' &&
 74          err.message.includes('received undefined'),
 75      )
 76      .map(err => formatValidationPath(err.path))
 77  
 78    const unexpectedParams = error.issues
 79      .filter(err => err.code === 'unrecognized_keys')
 80      .flatMap(err => err.keys)
 81  
 82    const typeMismatchParams = error.issues
 83      .filter(
 84        err =>
 85          err.code === 'invalid_type' &&
 86          !err.message.includes('received undefined'),
 87      )
 88      .map(err => {
 89        const typeErr = err as { expected: string }
 90        const receivedMatch = err.message.match(/received (\w+)/)
 91        const received = receivedMatch ? receivedMatch[1] : 'unknown'
 92        return {
 93          param: formatValidationPath(err.path),
 94          expected: typeErr.expected,
 95          received,
 96        }
 97      })
 98  
 99    // Default to original error message if we can't create a better one
100    let errorContent = error.message
101  
102    // Build a human-readable error message
103    const errorParts = []
104  
105    if (missingParams.length > 0) {
106      const missingParamErrors = missingParams.map(
107        param => `The required parameter \`${param}\` is missing`,
108      )
109      errorParts.push(...missingParamErrors)
110    }
111  
112    if (unexpectedParams.length > 0) {
113      const unexpectedParamErrors = unexpectedParams.map(
114        param => `An unexpected parameter \`${param}\` was provided`,
115      )
116      errorParts.push(...unexpectedParamErrors)
117    }
118  
119    if (typeMismatchParams.length > 0) {
120      const typeErrors = typeMismatchParams.map(
121        ({ param, expected, received }) =>
122          `The parameter \`${param}\` type is expected as \`${expected}\` but provided as \`${received}\``,
123      )
124      errorParts.push(...typeErrors)
125    }
126  
127    if (errorParts.length > 0) {
128      errorContent = `${toolName} failed due to the following ${errorParts.length > 1 ? 'issues' : 'issue'}:\n${errorParts.join('\n')}`
129    }
130  
131    return errorContent
132  }