mirror of
https://github.com/JackChen-me/open-multi-agent.git
synced 2026-10-22 02:53:26 -05:00
184 lines
6.3 KiB
TypeScript
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
|
|
}
|