Files
openclaw/src/acp/runtime/errors.ts
Jordan Baker 86c1622a3a fix(acp): propagate AcpRuntimeError detail through lifecycle boundary
Summary:
- Preserve AcpRuntimeError detail across the ACP lifecycle boundary.
- Redact non-Error lifecycle failure strings and add gateway/agent propagation coverage.
- Align rebased CLI command-hint formatting with current main.

Verification:
- pnpm check:test-types
- pnpm test src/acp/runtime/errors.test.ts src/agents/command/attempt-execution.error-propagation.test.ts src/gateway/server.agent.gateway-server-agent-b.test.ts
- CI exact head c96d63298b green

Co-authored-by: Jordan Baker <23538+hexsprite@users.noreply.github.com>
2026-05-09 23:08:30 -04:00

125 lines
3.9 KiB
TypeScript

import { stringifyNonErrorCause } from "../../infra/errors.js";
import { redactSensitiveText } from "../../logging/redact.js";
export const ACP_ERROR_CODES = [
"ACP_BACKEND_MISSING",
"ACP_BACKEND_UNAVAILABLE",
"ACP_BACKEND_UNSUPPORTED_CONTROL",
"ACP_DISPATCH_DISABLED",
"ACP_INVALID_RUNTIME_OPTION",
"ACP_SESSION_INIT_FAILED",
"ACP_TURN_FAILED",
] as const;
export type AcpRuntimeErrorCode = (typeof ACP_ERROR_CODES)[number];
const ACP_ERROR_CODE_SET = new Set<AcpRuntimeErrorCode>(ACP_ERROR_CODES);
export class AcpRuntimeError extends Error {
readonly code: AcpRuntimeErrorCode;
override readonly cause?: unknown;
constructor(code: AcpRuntimeErrorCode, message: string, options?: { cause?: unknown }) {
super(message);
this.name = "AcpRuntimeError";
this.code = code;
this.cause = options?.cause;
}
}
function getForeignAcpRuntimeError(value: unknown): {
code: AcpRuntimeErrorCode;
message: string;
} | null {
if (!(value instanceof Error)) {
return null;
}
const code = (value as { code?: unknown }).code;
if (typeof code !== "string" || !ACP_ERROR_CODE_SET.has(code as AcpRuntimeErrorCode)) {
return null;
}
return {
code: code as AcpRuntimeErrorCode,
message: value.message,
};
}
export function isAcpRuntimeError(value: unknown): value is AcpRuntimeError {
return value instanceof AcpRuntimeError || getForeignAcpRuntimeError(value) !== null;
}
export function toAcpRuntimeError(params: {
error: unknown;
fallbackCode: AcpRuntimeErrorCode;
fallbackMessage: string;
}): AcpRuntimeError {
if (params.error instanceof AcpRuntimeError) {
return params.error;
}
const foreignAcpRuntimeError = getForeignAcpRuntimeError(params.error);
if (foreignAcpRuntimeError) {
return new AcpRuntimeError(foreignAcpRuntimeError.code, foreignAcpRuntimeError.message, {
cause: params.error,
});
}
if (params.error instanceof Error) {
return new AcpRuntimeError(params.fallbackCode, params.error.message, {
cause: params.error,
});
}
return new AcpRuntimeError(params.fallbackCode, params.fallbackMessage, {
cause: params.error,
});
}
/**
* Render an error and its `.cause` chain as a single human-readable line for
* logs, lifecycle events, and tool results. Format is
* `Name [code]: message <- Name [code]: message <- ...`. Number codes also
* appear, so JSON-RPC error codes like `-32603` survive into surfaces that
* downstream consumers see (gateway logs, telegram replies, tool_result text).
*
* Depth is capped to defend against self-referential `.cause` cycles.
*/
export function formatAcpErrorChain(error: unknown): string {
if (!(error instanceof Error)) {
return redactSensitiveText(String(error));
}
const segments: string[] = [renderSingleError(error)];
let current: unknown = (error as unknown as { cause?: unknown }).cause;
let depth = 0;
while (current !== undefined && current !== null && depth < 8) {
if (current instanceof Error) {
segments.push(renderSingleError(current));
current = (current as unknown as { cause?: unknown }).cause;
} else {
segments.push(stringifyNonErrorCause(current));
current = undefined;
}
depth += 1;
}
return redactSensitiveText(segments.join(" <- "));
}
function renderSingleError(error: Error): string {
const codeValue = (error as unknown as { code?: unknown }).code;
const codeSuffix =
typeof codeValue === "string" || typeof codeValue === "number" ? ` [${codeValue}]` : "";
return `${error.name}${codeSuffix}: ${error.message}`;
}
export async function withAcpRuntimeErrorBoundary<T>(params: {
run: () => Promise<T>;
fallbackCode: AcpRuntimeErrorCode;
fallbackMessage: string;
}): Promise<T> {
try {
return await params.run();
} catch (error) {
throw toAcpRuntimeError({
error,
fallbackCode: params.fallbackCode,
fallbackMessage: params.fallbackMessage,
});
}
}