CoolFace
Datasetpublic

basant307/AI_Governance_Project

sourceHugging Faceapache-2.0updated 2mo agoView on Hugging Face
0likes45downloads
errors.ts311 linesDownload Raw Back to utils
1/**2 * @license3 * Copyright 2025 Google LLC4 * SPDX-License-Identifier: Apache-2.05 */6 7import type { Config } from '@qwen-code/qwen-code-core';8import {9  OutputFormat,10  JsonFormatter,11  parseAndFormatApiError,12  FatalTurnLimitedError,13  FatalCancellationError,14  FatalBudgetExceededError,15  ToolErrorType,16  createDebugLogger,17} from '@qwen-code/qwen-code-core';18import type { BudgetExceeded } from './runBudget.js';19import { runExitCleanup } from './cleanup.js';20import { writeStderrLine } from './stdioHelpers.js';21 22const debugLogger = createDebugLogger('CLI_ERRORS');23 24/**25 * Marker thrown when a producer has already formatted the error message and26 * written it to stderr — the downstream `handleError` should propagate the27 * exit code without printing or reformatting again.28 *29 * The non-interactive runner uses this when an upstream API error event30 * arrives mid-stream: it formats with parseAndFormatApiError, writes once,31 * and then throws. Without this marker, handleError would call32 * parseAndFormatApiError a second time on the (now formatted) Error.message,33 * yielding "[API Error: [API Error: ...]]" plus a duplicate stderr line.34 */35export class AlreadyReportedError extends Error {36  /** Exit code to surface — defaults to 1 for generic upstream failures. */37  exitCode: number;38 39  constructor(message: string, exitCode = 1) {40    super(message);41    this.name = 'AlreadyReportedError';42    this.exitCode = exitCode;43  }44}45 46export function getErrorMessage(error: unknown): string {47  if (error instanceof Error) {48    return error.message;49  }50 51  // Handle objects with message property (error-like objects)52  if (53    error !== null &&54    typeof error === 'object' &&55    'message' in error &&56    typeof (error as { message: unknown }).message === 'string'57  ) {58    return (error as { message: string }).message;59  }60 61  // Handle plain objects by stringifying them62  if (error !== null && typeof error === 'object') {63    try {64      const stringified = JSON.stringify(error);65      // JSON.stringify can return undefined for objects with toJSON() returning undefined66      return stringified ?? String(error);67    } catch {68      // If JSON.stringify fails (circular reference, etc.), fall back to String69      return String(error);70    }71  }72 73  return String(error);74}75 76interface ErrorWithCode extends Error {77  exitCode?: number;78  code?: string | number;79  status?: string | number;80}81 82/**83 * Extracts the appropriate error code from an error object.84 */85function extractErrorCode(error: unknown): string | number {86  const errorWithCode = error as ErrorWithCode;87 88  // Prioritize exitCode for FatalError types, fall back to other codes89  if (typeof errorWithCode.exitCode === 'number') {90    return errorWithCode.exitCode;91  }92  if (errorWithCode.code !== undefined) {93    return errorWithCode.code;94  }95  if (errorWithCode.status !== undefined) {96    return errorWithCode.status;97  }98 99  return 1; // Default exit code100}101 102/**103 * Converts an error code to a numeric exit code.104 */105function getNumericExitCode(errorCode: string | number): number {106  return typeof errorCode === 'number' ? errorCode : 1;107}108 109/**110 * Drains pending cleanup before terminating. Routing every "we're about111 * to die" path through here keeps async exit-side I/O (chat-recording112 * flush, telemetry shutdown, MCP disconnect) from being skipped — the113 * earlier sync writes were inherently bounded so a bare `process.exit`114 * was safe; with the async-jsonl change it is not.115 */116// Guards against double-entry when two terminating paths race (e.g. SIGINT117// fires `handleCancellationError` while a stream rejection routes through118// `handleError`): only the first caller drains cleanup + exits; the second119// suspends forever in the unresolved promise and gets killed when the first120// caller's process.exit fires.121let exiting = false;122 123async function exitAfterCleanup(code: number): Promise<never> {124  if (exiting) return new Promise<never>(() => {});125  exiting = true;126  await runExitCleanup();127  // `return` so process.exit's `never` narrows the function's terminating128  // statement — without it TS reports "function returning 'never' cannot129  // have a reachable end point" because await doesn't propagate `never`.130  return process.exit(code);131}132 133/** Test-only — reset the exit-once latch between cases. */134export function _resetExitLatchForTest(): void {135  exiting = false;136}137 138/**139 * Handles errors consistently for both JSON and text output formats.140 * In JSON mode, outputs formatted JSON error and exits.141 * In text mode, outputs error message and re-throws.142 */143export async function handleError(144  error: unknown,145  config: Config,146  customErrorCode?: string | number,147): Promise<never> {148  // Producers that already wrote a formatted message to stderr (see149  // AlreadyReportedError above) should not be reprinted or reformatted here.150  // In TEXT mode this short-circuits straight to a clean re-throw; in JSON151  // mode we still emit the structured payload exactly once so machine152  // consumers don't lose the error.153  if (error instanceof AlreadyReportedError) {154    if (config.getOutputFormat() === OutputFormat.JSON) {155      const formatter = new JsonFormatter();156      const errorCode = customErrorCode ?? error.exitCode;157      const formattedError = formatter.formatError(error, errorCode);158      writeStderrLine(formattedError);159      return exitAfterCleanup(getNumericExitCode(errorCode));160    }161    await runExitCleanup();162    throw error;163  }164 165  const errorMessage = parseAndFormatApiError(166    error,167    config.getContentGeneratorConfig()?.authType,168  );169 170  if (config.getOutputFormat() === OutputFormat.JSON) {171    const formatter = new JsonFormatter();172    const errorCode = customErrorCode ?? extractErrorCode(error);173 174    const formattedError = formatter.formatError(175      error instanceof Error ? error : new Error(getErrorMessage(error)),176      errorCode,177    );178 179    writeStderrLine(formattedError);180    return exitAfterCleanup(getNumericExitCode(errorCode));181  } else {182    writeStderrLine(errorMessage);183    // Drain queued writes before re-throwing so the unhandled rejection184    // path doesn't lose chat-recording records that are still in the queue.185    await runExitCleanup();186    throw error;187  }188}189 190/**191 * Handles tool execution errors specifically.192 * In JSON/STREAM_JSON mode, outputs error message to stderr only and does not exit.193 * The error will be properly formatted in the tool_result block by the adapter,194 * allowing the session to continue so the LLM can decide what to do next.195 * In text mode, outputs error message to stderr only.196 *197 * @param toolName - Name of the tool that failed198 * @param toolError - The error that occurred during tool execution199 * @param config - Configuration object200 * @param errorCode - Optional error code201 * @param resultDisplay - Optional display message for the error202 */203export function handleToolError(204  toolName: string,205  toolError: Error,206  config: Config,207  errorCode?: string | number,208  resultDisplay?: string,209): void {210  // Check if this is a permission denied error in non-interactive mode211  const isExecutionDenied = errorCode === ToolErrorType.EXECUTION_DENIED;212  const isNonInteractive = !config.isInteractive();213  const isTextMode = config.getOutputFormat() === OutputFormat.TEXT;214 215  // Show warning for permission denied errors in non-interactive text mode216  if (isExecutionDenied && isNonInteractive && isTextMode) {217    const warningMessage =218      `Warning: Tool "${toolName}" requires user approval but cannot execute in non-interactive mode.\n` +219      `To enable automatic tool execution, use the -y flag (YOLO mode):\n` +220      `Example: qwen -p 'your prompt' -y\n\n`;221    process.stderr.write(warningMessage);222  }223 224  debugLogger.error(225    `Error executing tool ${toolName}: ${resultDisplay || toolError.message}`,226  );227}228 229/**230 * Handles cancellation/abort signals consistently.231 */232export async function handleCancellationError(config: Config): Promise<never> {233  const cancellationError = new FatalCancellationError('Operation cancelled.');234 235  if (config.getOutputFormat() === OutputFormat.JSON) {236    const formatter = new JsonFormatter();237    const formattedError = formatter.formatError(238      cancellationError,239      cancellationError.exitCode,240    );241 242    writeStderrLine(formattedError);243  } else {244    writeStderrLine(cancellationError.message);245  }246  return exitAfterCleanup(cancellationError.exitCode);247}248 249/**250 * Handles max session turns exceeded consistently.251 *252 * When `--json-schema` is active the error gets an extra hint pointing at the253 * common reasons a structured-output run never terminated: the model never254 * called `structured_output`, the tool was denied by `permissions.deny` /255 * `--exclude-tools`, or the schema is unsatisfiable. Without this, all three256 * failure modes surface as the same generic "increase maxSessionTurns" line257 * even though the fix is a permissions / schema change, not a turns bump.258 */259export async function handleMaxTurnsExceededError(260  config: Config,261): Promise<never> {262  const baseMessage =263    'Reached max session turns for this session. Increase the number of turns by specifying maxSessionTurns in settings.json.';264  const jsonSchemaActive = config.getJsonSchema?.() !== undefined;265  const message = jsonSchemaActive266    ? `${baseMessage}\nNote: --json-schema is active. If the model never called structured_output, verify it isn't denied by permissions.deny / --exclude-tools and that the schema is satisfiable.`267    : baseMessage;268  const maxTurnsError = new FatalTurnLimitedError(message);269 270  if (config.getOutputFormat() === OutputFormat.JSON) {271    const formatter = new JsonFormatter();272    const formattedError = formatter.formatError(273      maxTurnsError,274      maxTurnsError.exitCode,275    );276 277    writeStderrLine(formattedError);278  } else {279    writeStderrLine(maxTurnsError.message);280  }281  return exitAfterCleanup(maxTurnsError.exitCode);282}283 284/**285 * Emits the structured "run aborted by budget" error and exits. Used by286 * the non-interactive run loop when `--max-wall-time` or `--max-tool-calls`287 * fires (see `RunBudgetEnforcer`). Exit code is 55, distinct from the288 * turn-cap exit code 53 and SIGINT's 130 so CI scripts can branch on the289 * reason.290 *291 * The output shape intentionally mirrors `handleMaxTurnsExceededError` /292 * `handleCancellationError`: structured JSON only on `OutputFormat.JSON`293 * and plain stderr for everything else (incl. STREAM_JSON). Emitting a294 * structured envelope on STREAM_JSON too is a real gap, but it's a295 * codebase-wide convention question that affects cancel / max-turns296 * equally, not a budget-specific decision.297 */298export async function handleBudgetExceededError(299  config: Config,300  exceeded: BudgetExceeded,301): Promise<never> {302  const fatal = new FatalBudgetExceededError(exceeded.message);303  if (config.getOutputFormat() === OutputFormat.JSON) {304    const formatter = new JsonFormatter();305    writeStderrLine(formatter.formatError(fatal, fatal.exitCode));306  } else {307    writeStderrLine(fatal.message);308  }309  return exitAfterCleanup(fatal.exitCode);310}311 
basant307/AI_Governance_Project · CoolFace