Files
trebuchet/apps/worker/src/temporal/workflow-errors.ts
T
ezl-keygraph 1f6dfd7e17 feat: extract pipeline core for library consumption (#282)
* feat: extract pipeline core for library consumption

* fix: chmod workspace directory for container write access

* fix: resolve playwright output dir relative to deliverables parent

* feat: add multi-provider LLM support via ProviderConfig

* fix: resolve model overrides via options.model, remove unused model env passthrough

* fix: use ANTHROPIC_AUTH_TOKEN for custom base URL and router auth

* fix: skip env-based credential validation when providerConfig is present

* fix: support large UID/GID values for AD/LDAP users in container
2026-04-10 04:53:36 +05:30

122 lines
4.1 KiB
TypeScript

// Copyright (C) 2025 Keygraph, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License version 3
// as published by the Free Software Foundation.
/**
* Workflow error formatting utilities.
* Pure functions with no side effects — safe for Temporal workflow sandbox.
*/
import { ErrorCode } from '../types/errors.js';
/**
* Maps an ApplicationFailure type string to a structured ErrorCode.
*
* Activities classify errors via classifyErrorForTemporal() and throw
* ApplicationFailure with a type string. This function maps those strings
* to stable ErrorCode values so consumers can switch on codes instead of
* string-matching error messages.
*/
const ERROR_TYPE_TO_CODE: Record<string, ErrorCode> = {
AuthenticationError: ErrorCode.AUTH_FAILED,
BillingError: ErrorCode.BILLING_ERROR,
RateLimitError: ErrorCode.API_RATE_LIMITED,
ConfigurationError: ErrorCode.CONFIG_VALIDATION_FAILED,
OutputValidationError: ErrorCode.OUTPUT_VALIDATION_FAILED,
AgentExecutionError: ErrorCode.AGENT_EXECUTION_FAILED,
GitError: ErrorCode.GIT_CHECKPOINT_FAILED,
InvalidTargetError: ErrorCode.TARGET_UNREACHABLE,
};
export function classifyErrorCode(error: unknown): ErrorCode | undefined {
let current: unknown = error;
while (current instanceof Error) {
if ('type' in current && typeof (current as { type: unknown }).type === 'string') {
const code = ERROR_TYPE_TO_CODE[(current as { type: string }).type];
if (code) return code;
}
current = (current as { cause?: unknown }).cause;
}
return undefined;
}
/** Maps Temporal error type strings to actionable remediation hints. */
const REMEDIATION_HINTS: Record<string, string> = {
AuthenticationError: 'Verify ANTHROPIC_API_KEY or CLAUDE_CODE_OAUTH_TOKEN in .env is valid and not expired.',
ConfigurationError: 'Check your CONFIG file path and contents.',
BillingError: 'Check your Anthropic billing dashboard. Add credits or wait for spending cap reset.',
GitError: 'Check repository path and git state.',
InvalidTargetError: 'Verify the target URL is correct and accessible.',
PermissionError: 'Check file and network permissions.',
ExecutionLimitError: 'Agent exceeded maximum turns or budget. Review prompt complexity.',
};
/**
* Walk the .cause chain to find the innermost error with a .type property.
* Temporal wraps ApplicationFailure in ActivityFailure — the useful info is inside.
*
* Uses duck-typing because workflow code cannot import @temporalio/activity types.
*/
function unwrapActivityError(error: unknown): {
message: string;
type: string | null;
} {
let current: unknown = error;
let typed: { message: string; type: string } | null = null;
while (current instanceof Error) {
if ('type' in current && typeof (current as { type: unknown }).type === 'string') {
typed = {
message: current.message,
type: (current as { type: string }).type,
};
}
current = (current as { cause?: unknown }).cause;
}
if (typed) {
return typed;
}
return {
message: error instanceof Error ? error.message : String(error),
type: null,
};
}
/**
* Format a structured error string from workflow catch context.
* Segments are delimited by | for multi-line rendering by WorkflowLogger.
*/
export function formatWorkflowError(error: unknown, currentPhase: string | null, currentAgent: string | null): string {
const unwrapped = unwrapActivityError(error);
// Phase context (first segment)
let phaseContext = 'Pipeline failed';
if (currentPhase && currentAgent && currentPhase !== currentAgent) {
phaseContext = `${currentPhase} failed (agent: ${currentAgent})`;
} else if (currentPhase) {
phaseContext = `${currentPhase} failed`;
}
const segments: string[] = [phaseContext];
if (unwrapped.type) {
segments.push(unwrapped.type);
}
// Sanitize pipe characters from message to preserve delimiter format
segments.push(unwrapped.message.replaceAll('|', '/'));
if (unwrapped.type) {
const hint = REMEDIATION_HINTS[unwrapped.type];
if (hint) {
segments.push(`Hint: ${hint}`);
}
}
return segments.join('|');
}