Files
open-multi-agent/packages/core/src/errors.ts
T

184 lines
6.3 KiB
TypeScript

/**
* @fileoverview Framework-specific error classes.
*/
import type { SemanticRoutingAssessment, TaskRequirementIssue } from './types.js'
/**
* Raised before task execution when a task has no eligible agent or its
* explicit assignee does not satisfy the task's hard requirements.
*/
export class InvalidTaskRequirementsError extends Error {
readonly code = 'INVALID_TASK_REQUIREMENTS'
constructor(readonly issues: readonly TaskRequirementIssue[]) {
super(
`Task requirements are unsatisfied: ${issues
.map((issue) => `${issue.code} for "${issue.taskTitle}"`)
.join(', ')}.`,
)
this.name = 'InvalidTaskRequirementsError'
}
}
/**
* Raised when an agent or orchestrator run exceeds its configured token budget.
*/
export class TokenBudgetExceededError extends Error {
readonly code = 'TOKEN_BUDGET_EXCEEDED'
constructor(
readonly agent: string,
readonly tokensUsed: number,
readonly budget: number,
) {
super(`Agent "${agent}" exceeded token budget: ${tokensUsed} tokens used (budget: ${budget})`)
this.name = 'TokenBudgetExceededError'
}
}
/**
* Raised when an orchestrator run exceeds its configured estimated cost budget.
*/
export class CostBudgetExceededError extends Error {
readonly code = 'COST_BUDGET_EXCEEDED'
constructor(
readonly agent: string,
readonly costUsed: number,
readonly budget: number,
) {
super(`Agent "${agent}" exceeded cost budget: ${costUsed} estimated cost used (budget: ${budget})`)
this.name = 'CostBudgetExceededError'
}
}
/**
* Raised when a single LLM call (one `adapter.chat()` request) exceeds the
* per-call deadline configured via {@link AgentConfig.callTimeoutMs}.
*
* Distinct from a whole-run timeout ({@link AgentConfig.timeoutMs}) and from a
* caller-supplied `abortSignal` cancellation: the runner only raises this when
* its own per-call deadline fired and the caller's signal did not, so a stalled
* provider is observable and tellable apart from a deliberate abort.
*/
export class LLMCallTimeoutError extends Error {
readonly code = 'LLM_CALL_TIMEOUT'
constructor(
/** The per-call deadline, in milliseconds, that was exceeded. */
readonly timeoutMs: number,
/** Name of the agent whose call timed out, when known. */
readonly agent?: string,
) {
super(
agent !== undefined
? `Agent "${agent}" LLM call exceeded per-call timeout of ${timeoutMs}ms`
: `LLM call exceeded per-call timeout of ${timeoutMs}ms`,
)
this.name = 'LLMCallTimeoutError'
}
}
/** Raised when routing infrastructure exceeds its configured deadline. */
export class RoutingTimeoutError extends Error {
readonly code = 'ROUTING_TIMEOUT'
constructor(
readonly timeoutMs: number,
readonly stage: 'router' | 'profiler',
) {
super(`Execution routing ${stage} exceeded its timeout of ${timeoutMs}ms`)
this.name = 'RoutingTimeoutError'
}
}
/** Raised when the semantic profiler cannot produce a valid task profile. */
export class RoutingProfilerFailedError extends Error {
readonly code = 'ROUTING_PROFILER_FAILED'
constructor(
message: string,
readonly cause?: unknown,
) {
super(message)
this.name = 'RoutingProfilerFailedError'
}
}
/**
* Raised before execution when inferred high-risk semantics need an explicit
* governance topology rather than an automatic model-selected route.
*/
export class RoutingDeclarationRequiredError extends Error {
readonly code = 'ROUTING_DECLARATION_REQUIRED'
constructor(
readonly reasons: readonly string[],
readonly assessment?: SemanticRoutingAssessment,
) {
super(
'Hybrid execution routing requires an explicit governance declaration: '
+ reasons.join('; '),
)
this.name = 'RoutingDeclarationRequiredError'
}
}
/**
* Raised when a message list passed to an adapter violates the
* {@link LLMMessage}[] contract (e.g. a `content` that isn't a `ContentBlock[]`).
* Surfaced at the adapter entry so the violation fails loudly instead of
* crashing deep in provider-specific message conversion.
*/
export class InvalidMessageError extends Error {
readonly code = 'INVALID_MESSAGE'
constructor(message: string) {
super(message)
this.name = 'InvalidMessageError'
}
}
/**
* Read an HTTP-style status code off an unknown error, if present. Provider
* SDK errors (`Anthropic.APIError`, `OpenAI.APIError`) expose it as `.status`;
* some libraries use `.statusCode`. Returns `undefined` for network/unknown
* errors that carry no numeric status.
*/
function extractStatus(error: unknown): number | undefined {
if (typeof error !== 'object' || error === null) return undefined
const record = error as { status?: unknown; statusCode?: unknown }
const status = record.status ?? record.statusCode
return typeof status === 'number' && Number.isFinite(status) ? status : undefined
}
/**
* Classify an error as retryable (transient — another attempt might succeed)
* or terminal (a retry cannot help).
*
* Conservative by design: returns `true` (retryable) unless the error is
* *provably* terminal, so turning retry on never silently stops retrying an
* error class that was retried before — it only skips attempts that are
* pointless. Terminal cases are exhausted-budget, malformed input, an aborted
* call, and 4xx client errors other than 408/409/429. Everything else —
* network blips (no status), request timeouts (408), conflicts (409), rate
* limits (429), and all 5xx server errors — is retryable.
*/
export function isRetryableError(error: unknown): boolean {
if (error instanceof InvalidTaskRequirementsError) return false
if (error instanceof TokenBudgetExceededError) return false
if (error instanceof CostBudgetExceededError) return false
if (error instanceof InvalidMessageError) return false
if (error instanceof LLMCallTimeoutError) return true
if (error instanceof RoutingTimeoutError) return true
if (error instanceof RoutingProfilerFailedError) return false
if (error instanceof RoutingDeclarationRequiredError) return false
if (error instanceof Error && error.name === 'AbortError') return false
const status = extractStatus(error)
if (status === undefined) return true
if (status === 408 || status === 409 || status === 429) return true
if (status >= 400 && status < 500) return false
return true
}