const DEEPLINE_ERROR_BRAND = Symbol.for('deepline.error.v1'); export const TOOL_EXECUTION_ERROR_BRAND = Symbol.for( 'deepline.tool-execution-error.v1', ); const PROVIDER_TRANSIENT_ERROR_BRAND = Symbol.for( 'deepline.provider-transient-error.v1', ); export const LEGACY_TOOL_EXECUTION_ERROR_SCHEMA_VERSION = 0 as const; export const TOOL_EXECUTION_ERROR_SCHEMA_VERSION = 1 as const; export const SUPPORTED_TOOL_EXECUTION_ERROR_SCHEMA_VERSIONS = [ LEGACY_TOOL_EXECUTION_ERROR_SCHEMA_VERSION, TOOL_EXECUTION_ERROR_SCHEMA_VERSION, ] as const; export type ToolExecutionErrorSchemaVersion = (typeof SUPPORTED_TOOL_EXECUTION_ERROR_SCHEMA_VERSIONS)[number]; export const TOOL_EXECUTION_ERROR_SCHEMA_HEADER = 'x-deepline-tool-error-schema'; const MAX_IDENTIFIER_LENGTH = 200; const MAX_CODE_LENGTH = 160; /** * The boundary responsible for a failed tool call. * * Use `provider` to distinguish a provider answer from caller input and * Deepline infrastructure. `unknown` fails closed and must not trigger a * waterfall fallback. * * @sdkReference errors 020 */ export type ToolExecutionErrorOrigin = | 'caller' | 'provider' | 'deepline' | 'unknown'; /** * The stable reason family for a failed tool call. * * Branch on this field only after narrowing to `ToolExecutionError`. Catch * `ProviderTransientError` when the policy is simply “try the next read * provider”; it is the safer and shorter waterfall contract. * * @sdkReference errors 030 */ export type ToolExecutionErrorCategory = | 'validation' | 'authentication' | 'authorization' | 'rate_limit' | 'network' | 'upstream' | 'billing' | 'conflict' | 'internal' | 'unknown'; /** * The transport failure observed when `category` is `network`. * * This is `null` for failures that are not network failures. * * @sdkReference errors 040 */ export type ToolExecutionNetworkKind = | 'timeout' | 'dns' | 'connect' | 'reset' | 'unavailable' | 'unknown'; /** * The request boundary on which a network failure occurred. * * `deepline_to_provider` is provider-side. Client and runtime scopes are * Deepline transport failures and never qualify as provider fallthrough. * * @sdkReference errors 050 */ export type ToolExecutionNetworkScope = | 'client_to_deepline' | 'runtime_to_deepline' | 'deepline_to_provider'; /** * Portable version-1 `tool_error` payload. * * This allowlisted shape crosses the API, runtime, and SDK boundaries. * `message` remains on the Error object and is deliberately not a policy * field. * * @sdkReference errors 064 */ export type ToolExecutionFailureV1 = { /** Payload version. */ schemaVersion: typeof TOOL_EXECUTION_ERROR_SCHEMA_VERSION; /** Public tool id passed to `tools.execute`. */ toolId: string; /** Provider responsible for the operation, or `null`. */ provider: string | null; /** Provider operation name, or `null`. */ operation: string | null; /** Stable machine-readable failure code, or `null`. */ code: string | null; /** Boundary responsible for the failure. */ origin: ToolExecutionErrorOrigin; /** Stable reason family. */ category: ToolExecutionErrorCategory; /** Whether repeating the same semantic call is delivery-safe. */ retryable: boolean; /** HTTP status when one exists, or `null`. */ statusCode: number | null; /** Provider or Deepline request id, or `null`. */ requestId: string | null; /** Suggested same-call retry delay in milliseconds, or `null`. */ retryAfterMs: number | null; /** Network failure kind, or `null`. */ networkKind: ToolExecutionNetworkKind | null; /** Network boundary that failed, or `null`. */ networkScope: ToolExecutionNetworkScope | null; }; /** * Constructor input for a structured tool failure. * * Deepline creates these values while decoding the versioned wire payload. * Customer code normally reads `ToolExecutionError` fields instead of * constructing an error. * * @sdkReference errors 065 */ export type ToolExecutionErrorOptions = Omit< ToolExecutionFailureV1, 'schemaVersion' > & { /** * Local diagnostic context inherited from DeeplineError. This is not part of * the portable failure payload and is intentionally omitted by serialization. */ details?: Record; }; /** * Provider-owned failure categories that may fall through to another read * provider. * * @sdkReference errors 060 */ export type ProviderTransientErrorCategory = | 'rate_limit' | 'network' | 'upstream'; function nativeInstanceOf(constructor: object, value: unknown): boolean { return Reflect.apply(Function.prototype[Symbol.hasInstance], constructor, [ value, ]) as boolean; } function hasBrand(value: unknown, brand: symbol): boolean { return ( typeof value === 'object' && value !== null && (value as Record)[brand] === true ); } function applyBrand(value: object, brand: symbol): void { if (hasBrand(value, brand)) return; Object.defineProperty(value, brand, { configurable: false, enumerable: false, writable: false, value: true, }); } /** * Base error class shared by the SDK and play runtime. * * The global brand preserves `instanceof DeeplineError` when a bundled play * and the runtime load separate physical copies of this module. * * @sdkReference errors 010 */ export class DeeplineError extends Error { /** HTTP status when the failure crossed an HTTP boundary. */ statusCode?: number; /** Stable machine-readable error code when one exists. */ code?: string; /** Local diagnostic context; not a portable error contract. */ details?: Record; /** * Construct a Deepline error. * * SDK and runtime code construct these errors. Application and Play code * normally catches the public subclasses instead. * * @param message Human-readable failure summary. * @param statusCode HTTP status when one exists. * @param code Stable machine-readable code when one exists. * @param details Local diagnostic context; never a portable error contract. */ constructor( message: string, statusCode?: number, code?: string, details?: Record, ) { super(message); this.name = 'DeeplineError'; this.statusCode = statusCode; this.code = code; this.details = details; applyBrand(this, DEEPLINE_ERROR_BRAND); } static [Symbol.hasInstance](value: unknown): boolean { // Subclasses inherit static methods. Delegate subclass checks to the native // prototype algorithm so AuthError never accidentally matches ConfigError. if (this !== DeeplineError) return nativeInstanceOf(this, value); return hasBrand(value, DEEPLINE_ERROR_BRAND); } } /** * A failed `tools.execute` call with stable, allowlisted provenance. * * `retryable` means Deepline's delivery/idempotency contract says it is safe * to repeat the same semantic call. It does not describe durable receipt * repairability and does not make arbitrary side-effecting fallbacks safe. * * In a Play, catch `ProviderTransientError` to continue a read waterfall and * let every other `ToolExecutionError` remain loud. In an SDK client, catch * this base class when you need structured diagnostics for every tool failure. * * @sdkReference errors 070 */ export class ToolExecutionError extends DeeplineError { /** Public tool id passed to `tools.execute`. */ readonly toolId: string; /** Provider responsible for the operation, or `null` when unattributed. */ readonly provider: string | null; /** Provider operation name, or `null` when unavailable. */ readonly operation: string | null; /** Boundary responsible for the failure. */ readonly origin: ToolExecutionErrorOrigin; /** Stable reason family for policy and diagnostics. */ readonly category: ToolExecutionErrorCategory; /** * Whether repeating the same semantic call is delivery-safe. * * This does not mean the error may be ignored. Waterfall fallthrough is * represented by `ProviderTransientError`. */ readonly retryable: boolean; /** Provider or Deepline request id, or `null` when unavailable. */ readonly requestId: string | null; /** Suggested same-call retry delay in milliseconds, or `null`. */ readonly retryAfterMs: number | null; /** Network failure kind, or `null` for non-network failures. */ readonly networkKind: ToolExecutionNetworkKind | null; /** Network boundary that failed, or `null` for non-network failures. */ readonly networkScope: ToolExecutionNetworkScope | null; /** * Construct a structured tool error. * * Deepline constructs this from the versioned `tool_error` payload. * Application and Play code should catch it rather than create it. */ constructor(message: string, options: ToolExecutionErrorOptions) { super( message, options.statusCode ?? undefined, options.code ?? undefined, options.details, ); this.name = 'ToolExecutionError'; this.toolId = options.toolId; this.provider = options.provider; this.operation = options.operation; this.origin = options.origin; this.category = options.category; this.retryable = options.retryable; this.requestId = options.requestId; this.retryAfterMs = options.retryAfterMs; this.networkKind = options.networkKind; this.networkScope = options.networkScope; applyBrand(this, TOOL_EXECUTION_ERROR_BRAND); if (isProviderTransientFailure(options)) { applyBrand(this, PROVIDER_TRANSIENT_ERROR_BRAND); } } static [Symbol.hasInstance](value: unknown): boolean { if (this !== ToolExecutionError) return nativeInstanceOf(this, value); return hasBrand(value, TOOL_EXECUTION_ERROR_BRAND); } } /** * Brands a compatibility subclass (notably SDK ToolRateLimitError) as a tool * failure without changing its existing prototype chain. */ export function brandAsToolExecutionError(value: object): void { applyBrand(value, TOOL_EXECUTION_ERROR_BRAND); } export function isProviderTransientFailure(input: { origin: ToolExecutionErrorOrigin; category: ToolExecutionErrorCategory; retryable: boolean; }): input is { origin: 'provider'; category: ProviderTransientErrorCategory; retryable: boolean; } { return ( input.origin === 'provider' && (input.category === 'rate_limit' || input.category === 'network' || input.category === 'upstream') ); } /** * A provider-owned transient failure that is safe to handle as an empty * waterfall leg. Validation, auth, billing, Deepline, and unknown failures * never satisfy this type. * * `retryable` remains independent: it says whether the same semantic call may * be repeated safely. Falling through to a different read provider depends on * this class, not on `retryable`. * * @sdkReference errors 080 */ export class ProviderTransientError extends ToolExecutionError { /** Provider attribution is guaranteed for this subtype. */ override readonly origin = 'provider' as const; /** Provider failure category that made this error eligible for fallthrough. */ declare readonly category: ProviderTransientErrorCategory; /** Constructed by Deepline when a provider-owned transient failure arrives. */ constructor( message: string, options: Omit & { category: ProviderTransientErrorCategory; }, ) { super(message, { ...options, origin: 'provider', category: options.category, }); this.name = 'ProviderTransientError'; this.category = options.category; applyBrand(this, PROVIDER_TRANSIENT_ERROR_BRAND); } static [Symbol.hasInstance](value: unknown): boolean { if (this !== ProviderTransientError) return nativeInstanceOf(this, value); return hasBrand(value, PROVIDER_TRANSIENT_ERROR_BRAND); } } /** Why a provider cannot serve the current read request. */ export type ProviderUnavailableReason = | ProviderTransientErrorCategory | 'account_capacity' | 'credentials_missing'; /** * A provider failure that permits a read waterfall to try its next provider. * A missing provider connection is included because another provider may still * serve the read. Invalid credentials, caller input, and Deepline billing * failures remain loud. */ export type ProviderUnavailableError = | ProviderTransientError | (ToolExecutionError & { readonly origin: 'provider'; readonly code: 'PROVIDER_ACCOUNT_CAPACITY'; }) | (ToolExecutionError & { readonly origin: 'caller'; readonly code: 'INTEGRATION_CREDENTIALS_MISSING'; }); /** * Return the provider-specific reason a read cannot run right now. * * `null` means this error must stay loud: it is caller input, an invalid * customer credential, Deepline billing, or an internal failure. A missing * provider connection is different: an explicit read waterfall may continue * to a configured fallback and record the unavailable leg. */ export function getProviderUnavailableReason( error: unknown, ): ProviderUnavailableReason | null { if (error instanceof ProviderTransientError) return error.category; if ( error instanceof ToolExecutionError && error.origin === 'provider' && error.code === 'PROVIDER_ACCOUNT_CAPACITY' ) { return 'account_capacity'; } if ( error instanceof ToolExecutionError && error.origin === 'caller' && error.code === 'INTEGRATION_CREDENTIALS_MISSING' ) { return 'credentials_missing'; } return null; } /** * Whether a provider cannot serve this read right now. * * Use this in an explicit `catch` to advance a read-only waterfall. For * diagnostics, use `getProviderUnavailableReason(error)`. */ export function isProviderUnavailable( error: unknown, ): error is ProviderUnavailableError { return getProviderUnavailableReason(error) !== null; } /** @deprecated Use isProviderUnavailable. */ export const isProviderWaterfallUnavailableError = isProviderUnavailable; /** Brand an internal compatibility error after its normalized fields exist. */ export function brandAsProviderTransientError(value: object): void { applyBrand(value, PROVIDER_TRANSIENT_ERROR_BRAND); } function boundedString( value: unknown, maxLength = MAX_IDENTIFIER_LENGTH, ): string | null { if (typeof value !== 'string') return null; const normalized = value.trim(); return normalized ? normalized.slice(0, maxLength) : null; } function finiteNonNegativeInteger(value: unknown): number | null { return typeof value === 'number' && Number.isFinite(value) && value >= 0 && Number.isInteger(value) ? value : null; } export function normalizeToolExecutionOrigin( value: unknown, input?: { provider?: string | null; operation?: string | null }, ): ToolExecutionErrorOrigin { const normalized = boundedString(value)?.toLowerCase(); if (normalized === 'caller') return 'caller'; if (normalized === 'provider' || normalized === 'provider_network') { return 'provider'; } // Older integration adapters use "upstream" for a provider-owned failure. // Only accept that alias when the execution boundary identifies the provider // action; an unscoped upstream error fails closed. if ( normalized === 'upstream' && boundedString(input?.provider) && boundedString(input?.operation) ) { return 'provider'; } if ( normalized === 'deepline' || normalized === 'deepline_billing' || normalized === 'deepline_rate_limit' || normalized === 'internal' || normalized === 'server' ) { return 'deepline'; } if (normalized === 'provider_account') return 'provider'; return 'unknown'; } export function normalizeToolExecutionCategory( value: unknown, ): ToolExecutionErrorCategory { switch (boundedString(value)?.toLowerCase()) { case 'validation': case 'authorization': case 'rate_limit': case 'network': case 'upstream': case 'billing': case 'conflict': case 'internal': return boundedString(value)!.toLowerCase() as ToolExecutionErrorCategory; case 'authentication': case 'provider_auth': case 'provider_account': return 'authentication'; case 'execution': case 'parse': case 'stub': case 'internal_native_search_performance': return 'internal'; default: return 'unknown'; } } function normalizeNetworkKind(value: unknown): ToolExecutionNetworkKind | null { switch (boundedString(value)?.toLowerCase()) { case 'timeout': case 'dns': case 'connect': case 'reset': case 'unavailable': case 'unknown': return boundedString(value)!.toLowerCase() as ToolExecutionNetworkKind; default: return null; } } function normalizeNetworkScope( value: unknown, ): ToolExecutionNetworkScope | null { switch (boundedString(value)?.toLowerCase()) { case 'client_to_deepline': case 'runtime_to_deepline': case 'deepline_to_provider': return boundedString(value)!.toLowerCase() as ToolExecutionNetworkScope; default: return null; } } function isRecord(value: unknown): value is Record { return value !== null && typeof value === 'object' && !Array.isArray(value); } export function normalizeToolExecutionFailure( value: unknown, ): ToolExecutionFailureV1 | null { if ( !isRecord(value) || value.schemaVersion !== TOOL_EXECUTION_ERROR_SCHEMA_VERSION ) { return null; } const toolId = boundedString(value.toolId); if (!toolId) return null; const provider = boundedString(value.provider); const operation = boundedString(value.operation); const origin = normalizeToolExecutionOrigin(value.origin, { provider, operation, }); const category = normalizeToolExecutionCategory(value.category); const trustworthy = origin !== 'unknown' && category !== 'unknown' && typeof value.retryable === 'boolean'; return { schemaVersion: TOOL_EXECUTION_ERROR_SCHEMA_VERSION, toolId, provider, operation, code: boundedString(value.code, MAX_CODE_LENGTH), origin, category, retryable: trustworthy ? value.retryable === true : false, statusCode: finiteNonNegativeInteger(value.statusCode), requestId: boundedString(value.requestId), retryAfterMs: finiteNonNegativeInteger(value.retryAfterMs), networkKind: normalizeNetworkKind(value.networkKind), networkScope: normalizeNetworkScope(value.networkScope), }; } export function serializeToolExecutionFailure( error: unknown, ): ToolExecutionFailureV1 | null { if (!(error instanceof ToolExecutionError)) return null; return normalizeToolExecutionFailure({ schemaVersion: TOOL_EXECUTION_ERROR_SCHEMA_VERSION, toolId: error.toolId, provider: error.provider, operation: error.operation, code: error.code ?? null, origin: error.origin, category: error.category, retryable: error.retryable, statusCode: error.statusCode ?? null, requestId: error.requestId, retryAfterMs: error.retryAfterMs, networkKind: error.networkKind, networkScope: error.networkScope, }); } export function deserializeToolExecutionFailure( message: string, value: unknown, acceptedSchemaVersion: ToolExecutionErrorSchemaVersion, ): ToolExecutionError | null { if (acceptedSchemaVersion === LEGACY_TOOL_EXECUTION_ERROR_SCHEMA_VERSION) { return null; } const failure = normalizeToolExecutionFailure(value); if (!failure) return null; if (isProviderTransientFailure(failure)) { return new ProviderTransientError(message, failure); } return new ToolExecutionError(message, failure); }