diff --git a/AGENTS.md b/AGENTS.md index 0243a082a..92afc67cb 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -148,6 +148,38 @@ See `docs/TESTING.md` for details. New features must include telemetry instrumentation. See `src/cli/telemetry/README.md` for how to add metrics. +## Error Types + +Custom error types are encouraged — each distinct error class appears as its own category in telemetry, giving more +granular failure data. All errors live in `src/lib/errors/types.ts`. + +To add a new error: + +1. Define a class in `src/lib/errors/types.ts` extending `BaseError` with a default `errorSource`: + - `'user'` — user-fixable (bad input, missing credentials, invalid config) + - `'client'` — our bug or unexpected local failure + - `'service'` — remote service issue (timeout, connection failure, server error) + - `'unknown'` — source cannot be determined + +```ts +export class MyNewError extends BaseError { + constructor(message: string, options?: BaseErrorOptions) { + super(message, { defaultSource: 'user', ...options }); + } +} +``` + +Callers can override the source per throw site: + +```ts +throw new MyNewError('not found', { errorSource: 'service', cause: originalErr }); +``` + +2. Add `'MyNewError'` to the `ErrorName` enum in `src/cli/telemetry/schemas/common-shapes.ts`. + +That's it. The telemetry client reads `errorSource` and `name` directly from `BaseError` instances — no classification +logic to update. + ## Multi-Partition Support (GovCloud, China) The CLI supports multiple AWS partitions (commercial, GovCloud, China) through a central utility at diff --git a/src/cli/aws/__tests__/account-extended.test.ts b/src/cli/aws/__tests__/account-extended.test.ts index 2cf3dd085..c859634a8 100644 --- a/src/cli/aws/__tests__/account-extended.test.ts +++ b/src/cli/aws/__tests__/account-extended.test.ts @@ -1,4 +1,5 @@ -import { AwsCredentialsError, detectAccount, getCredentialProvider, validateAwsCredentials } from '../account.js'; +import { AwsCredentialsError } from '../../../lib/errors/types.js'; +import { detectAccount, getCredentialProvider, validateAwsCredentials } from '../account.js'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; const { mockSend } = vi.hoisted(() => ({ diff --git a/src/cli/aws/__tests__/account.test.ts b/src/cli/aws/__tests__/account.test.ts index 2c7c56cbc..e74a3eaf9 100644 --- a/src/cli/aws/__tests__/account.test.ts +++ b/src/cli/aws/__tests__/account.test.ts @@ -1,4 +1,4 @@ -import { AwsCredentialsError } from '../account.js'; +import { AwsCredentialsError } from '../../../lib/errors/types.js'; import { describe, expect, it } from 'vitest'; describe('AwsCredentialsError', () => { diff --git a/src/cli/aws/account.ts b/src/cli/aws/account.ts index 705d25cec..8e962cfb3 100644 --- a/src/cli/aws/account.ts +++ b/src/cli/aws/account.ts @@ -1,3 +1,4 @@ +import { AwsCredentialsError } from '../../lib/errors/types.js'; import { getAwsLoginGuidance } from '../external-requirements/checks'; import { GetCallerIdentityCommand, STSClient } from '@aws-sdk/client-sts'; import { fromEnv, fromNodeProviderChain } from '@aws-sdk/credential-providers'; @@ -13,21 +14,6 @@ export function getCredentialProvider(): AwsCredentialIdentityProvider { return hasEnvCreds ? fromEnv() : fromNodeProviderChain(); } -/** - * Error thrown when AWS credentials are not configured or invalid. - * Supports both a short message (for interactive mode) and detailed message (for CLI mode). - */ -export class AwsCredentialsError extends Error { - /** Short message suitable for interactive mode where UI handles recovery */ - readonly shortMessage: string; - - constructor(shortMessage: string, detailedMessage?: string) { - super(detailedMessage ?? shortMessage); - this.name = 'AwsCredentialsError'; - this.shortMessage = shortMessage; - } -} - /** * Get AWS account ID using STS GetCallerIdentity with detailed error handling. * Throws AwsCredentialsError with helpful messages for common credential issues. diff --git a/src/cli/commands/import/phase2-import.ts b/src/cli/commands/import/phase2-import.ts index c980110bd..a862ac5b3 100644 --- a/src/cli/commands/import/phase2-import.ts +++ b/src/cli/commands/import/phase2-import.ts @@ -1,4 +1,5 @@ -import { PollExhaustedError, PollTimeoutError, isThrottlingError, poll } from '../../../lib/utils/polling'; +import { PollExhaustedError, PollTimeoutError } from '../../../lib/errors/types'; +import { isThrottlingError, poll } from '../../../lib/utils/polling'; import { getCredentialProvider } from '../../aws/account'; import type { CfnTemplate } from './template-utils'; import { buildImportTemplate } from './template-utils'; diff --git a/src/cli/operations/dev/__tests__/invoke-a2a.test.ts b/src/cli/operations/dev/__tests__/invoke-a2a.test.ts index 9428177f6..743c34fc1 100644 --- a/src/cli/operations/dev/__tests__/invoke-a2a.test.ts +++ b/src/cli/operations/dev/__tests__/invoke-a2a.test.ts @@ -1,4 +1,4 @@ -import { ServerError } from '../invoke'; +import { ServerError } from '../../../../lib/errors/types'; import { invokeA2AStreaming } from '../invoke-a2a'; import { beforeEach, describe, expect, it, vi } from 'vitest'; diff --git a/src/cli/operations/dev/__tests__/invoke-mcp.test.ts b/src/cli/operations/dev/__tests__/invoke-mcp.test.ts index 1ca0758ed..ccba4559c 100644 --- a/src/cli/operations/dev/__tests__/invoke-mcp.test.ts +++ b/src/cli/operations/dev/__tests__/invoke-mcp.test.ts @@ -1,4 +1,4 @@ -import { ServerError } from '../invoke'; +import { ServerError } from '../../../../lib/errors/types'; import { callMcpTool, listMcpTools } from '../invoke-mcp'; import { beforeEach, describe, expect, it, vi } from 'vitest'; diff --git a/src/cli/operations/dev/index.ts b/src/cli/operations/dev/index.ts index 2522ef21c..e05d67a01 100644 --- a/src/cli/operations/dev/index.ts +++ b/src/cli/operations/dev/index.ts @@ -11,7 +11,7 @@ export { export { getDevConfig, getDevSupportedAgents, getAgentPort, loadProjectConfig, type DevConfig } from './config'; -export { ConnectionError, ServerError, invokeAgent, invokeAgentStreaming, invokeForProtocol } from './invoke'; +export { invokeAgent, invokeAgentStreaming, invokeForProtocol } from './invoke'; export { invokeA2AStreaming, fetchA2AAgentCard, type A2AAgentCard } from './invoke-a2a'; diff --git a/src/cli/operations/dev/invoke-a2a.ts b/src/cli/operations/dev/invoke-a2a.ts index ec2a192ea..2b3d28f1d 100644 --- a/src/cli/operations/dev/invoke-a2a.ts +++ b/src/cli/operations/dev/invoke-a2a.ts @@ -1,4 +1,5 @@ -import { ConnectionError, type InvokeStreamingOptions, type SSELogger, ServerError } from './invoke-types'; +import { ConnectionError, ServerError } from '../../../lib/errors/types'; +import { type InvokeStreamingOptions, type SSELogger } from './invoke-types'; import { isConnectionError, sleep } from './utils'; import { randomUUID } from 'crypto'; @@ -153,7 +154,9 @@ export async function* invokeA2AStreaming(options: InvokeStreamingOptions): Asyn } } - const finalError = new ConnectionError(lastError ?? new Error('Failed to connect to A2A server after retries')); + const finalError = new ConnectionError(lastError?.message ?? 'Failed to connect to A2A server after retries', { + cause: lastError, + }); logger?.log?.('error', `Failed to connect after ${maxRetries} attempts: ${finalError.message}`); throw finalError; } diff --git a/src/cli/operations/dev/invoke-agui.ts b/src/cli/operations/dev/invoke-agui.ts index 0349e5271..eb11a5129 100644 --- a/src/cli/operations/dev/invoke-agui.ts +++ b/src/cli/operations/dev/invoke-agui.ts @@ -1,6 +1,7 @@ +import { ConnectionError, ServerError } from '../../../lib/errors/types'; import { parseAguiSSEStream } from '../../aws/agui-parser'; import { AguiEventType } from '../../aws/agui-types'; -import { ConnectionError, type InvokeStreamingOptions, ServerError } from './invoke-types'; +import { type InvokeStreamingOptions } from './invoke-types'; import { isConnectionError, sleep } from './utils'; import { randomUUID } from 'crypto'; @@ -147,7 +148,9 @@ export async function* invokeAguiStreaming(options: InvokeStreamingOptions): Asy } } - const finalError = new ConnectionError(lastError ?? new Error('Failed to connect to AGUI server after retries')); + const finalError = new ConnectionError(lastError?.message ?? 'Failed to connect to AGUI server after retries', { + cause: lastError, + }); logger?.log?.('error', `Failed to connect after ${maxRetries} attempts: ${finalError.message}`); throw finalError; } diff --git a/src/cli/operations/dev/invoke-mcp.ts b/src/cli/operations/dev/invoke-mcp.ts index f87971348..6e861ef43 100644 --- a/src/cli/operations/dev/invoke-mcp.ts +++ b/src/cli/operations/dev/invoke-mcp.ts @@ -1,5 +1,6 @@ +import { ConnectionError, ServerError } from '../../../lib/errors/types'; import { parseJsonRpcResponse } from '../../../lib/utils/json-rpc'; -import { ConnectionError, type SSELogger, ServerError } from './invoke-types'; +import { type SSELogger } from './invoke-types'; import { isConnectionError, sleep } from './utils'; let requestId = 1; @@ -143,7 +144,9 @@ export async function listMcpTools( } } - const finalError = new ConnectionError(lastError ?? new Error('Failed to connect to MCP server after retries')); + const finalError = new ConnectionError(lastError?.message ?? 'Failed to connect to MCP server after retries', { + cause: lastError, + }); logger?.log?.('error', `Failed to connect after ${maxRetries} attempts: ${finalError.message}`); throw finalError; } diff --git a/src/cli/operations/dev/invoke-types.ts b/src/cli/operations/dev/invoke-types.ts index b876b2b00..4bd2f0a97 100644 --- a/src/cli/operations/dev/invoke-types.ts +++ b/src/cli/operations/dev/invoke-types.ts @@ -1,22 +1,3 @@ -/** Error thrown when the dev server returns a non-OK HTTP response. */ -export class ServerError extends Error { - constructor( - public readonly statusCode: number, - body: string - ) { - super(body || `Server returned ${statusCode}`); - this.name = 'ServerError'; - } -} - -/** Error thrown when the connection to the dev server fails. */ -export class ConnectionError extends Error { - constructor(cause: Error) { - super(cause.message); - this.name = 'ConnectionError'; - } -} - /** Logger interface for SSE events and error logging */ export interface SSELogger { logSSEEvent(rawLine: string): void; diff --git a/src/cli/operations/dev/invoke.ts b/src/cli/operations/dev/invoke.ts index f676c3ca8..9a385e574 100644 --- a/src/cli/operations/dev/invoke.ts +++ b/src/cli/operations/dev/invoke.ts @@ -1,10 +1,10 @@ +import { ConnectionError, ServerError } from '../../../lib/errors/types'; import { invokeA2AStreaming } from './invoke-a2a'; import { invokeAguiStreaming } from './invoke-agui'; -import { ConnectionError, type InvokeStreamingOptions, type SSELogger, ServerError } from './invoke-types'; +import { type InvokeStreamingOptions, type SSELogger } from './invoke-types'; import { isConnectionError, sleep } from './utils'; -// Re-export shared types so existing consumers don't break -export { ConnectionError, ServerError, type InvokeStreamingOptions, type SSELogger } from './invoke-types'; +export { type InvokeStreamingOptions, type SSELogger } from './invoke-types'; /** * Parse a single SSE data line and extract the content. @@ -196,7 +196,9 @@ export async function* invokeAgentStreaming( } // Log final failure after all retries exhausted with full details - const finalError = new ConnectionError(lastError ?? new Error('Failed to connect to dev server after retries')); + const finalError = new ConnectionError(lastError?.message ?? 'Failed to connect to dev server after retries', { + cause: lastError, + }); logger?.log?.('error', `Failed to connect after ${maxRetries} attempts: ${finalError.message}`); throw finalError; } @@ -304,7 +306,9 @@ export async function invokeAgent(portOrOptions: number | InvokeOptions, message } // Log final failure after all retries exhausted with full details - const finalError = new ConnectionError(lastError ?? new Error('Failed to connect to dev server after retries')); + const finalError = new ConnectionError(lastError?.message ?? 'Failed to connect to dev server after retries', { + cause: lastError, + }); logger?.log?.('error', `Failed to connect after ${maxRetries} attempts: ${finalError.message}`); throw finalError; } diff --git a/src/cli/telemetry/__tests__/client.test.ts b/src/cli/telemetry/__tests__/client.test.ts index cfba2a0c2..b66111849 100644 --- a/src/cli/telemetry/__tests__/client.test.ts +++ b/src/cli/telemetry/__tests__/client.test.ts @@ -1,4 +1,5 @@ /* eslint-disable @typescript-eslint/require-await */ +import { AccessDeniedError, DependencyCheckError } from '../../../lib/errors/types'; import { CANCELLED, TelemetryClient } from '../client'; import { InMemorySink } from '../sinks/in-memory-sink'; import { describe, expect, it } from 'vitest'; @@ -48,26 +49,19 @@ describe('TelemetryClient', () => { }); }); - it('classifies PackagingError subclasses', async () => { + it('classifies DependencyCheckError correctly', async () => { const sink = new InMemorySink(); const client = new TelemetryClient(sink); - class MissingDependencyError extends Error { - constructor() { - super('missing dep'); - this.name = 'MissingDependencyError'; - } - } - await expect( client.withCommandRun('deploy', async () => { - throw new MissingDependencyError(); + throw new DependencyCheckError(['missing docker']); }) ).rejects.toThrow(); expect(sink.metrics[0]!.attrs).toMatchObject({ - error_name: 'PackagingError', - is_user_error: 'false', + error_name: 'DependencyCheckError', + error_source: 'user', }); }); @@ -75,22 +69,15 @@ describe('TelemetryClient', () => { const sink = new InMemorySink(); const client = new TelemetryClient(sink); - class AwsCredentialsError extends Error { - constructor() { - super('creds expired'); - this.name = 'AwsCredentialsError'; - } - } - await expect( client.withCommandRun('invoke', async () => { - throw new AwsCredentialsError(); + throw new AccessDeniedError('creds expired'); }) ).rejects.toThrow(); expect(sink.metrics[0]!.attrs).toMatchObject({ - error_name: 'CredentialsError', - is_user_error: 'true', + error_name: 'AccessDeniedError', + error_source: 'user', }); }); diff --git a/src/cli/telemetry/__tests__/error-classification.test.ts b/src/cli/telemetry/__tests__/error-classification.test.ts deleted file mode 100644 index 0640e6ce0..000000000 --- a/src/cli/telemetry/__tests__/error-classification.test.ts +++ /dev/null @@ -1,63 +0,0 @@ -import { classifyError, isUserError } from '../error-classification'; -import { describe, expect, it } from 'vitest'; - -function errorWithName(name: string): Error { - const err = new Error('test'); - err.name = name; - return err; -} - -describe('classifyError', () => { - it.each([ - ['ConfigValidationError', 'ConfigError'], - ['ConfigNotFoundError', 'ConfigError'], - ['ConfigReadError', 'ConfigError'], - ['ConfigWriteError', 'ConfigError'], - ['ConfigParseError', 'ConfigError'], - ['AwsCredentialsError', 'CredentialsError'], - ['AccessDeniedException', 'CredentialsError'], - ['ExpiredToken', 'CredentialsError'], - ['PackagingError', 'PackagingError'], - ['MissingDependencyError', 'PackagingError'], - ['ArtifactSizeError', 'PackagingError'], - ['NoProjectError', 'ProjectError'], - ['AgentAlreadyExistsError', 'ProjectError'], - ['ResourceNotFoundException', 'ServiceError'], - ['ValidationException', 'ServiceError'], - ['ConflictException', 'ServiceError'], - ['ConnectionError', 'ConnectionError'], - ['ServerError', 'ConnectionError'], - ] as const)('%s → %s', (errorName, expected) => { - expect(classifyError(errorWithName(errorName))).toBe(expected); - }); - - it('returns UnknownError for unrecognized errors', () => { - expect(classifyError(new Error('something'))).toBe('UnknownError'); - }); - - it('returns UnknownError for non-Error values', () => { - expect(classifyError('string')).toBe('UnknownError'); - expect(classifyError(null)).toBe('UnknownError'); - expect(classifyError(undefined)).toBe('UnknownError'); - }); - - it('uses err.name when constructor.name is Error (SDK pattern)', () => { - // AWS SDK errors often: new Error(); err.name = 'ValidationException' - expect(classifyError(errorWithName('ValidationException'))).toBe('ServiceError'); - }); -}); - -describe('isUserError', () => { - it('returns true for user-fixable categories', () => { - expect(isUserError(errorWithName('ConfigValidationError'))).toBe(true); - expect(isUserError(errorWithName('AwsCredentialsError'))).toBe(true); - expect(isUserError(errorWithName('NoProjectError'))).toBe(true); - }); - - it('returns false for system categories', () => { - expect(isUserError(errorWithName('PackagingError'))).toBe(false); - expect(isUserError(errorWithName('ResourceNotFoundException'))).toBe(false); - expect(isUserError(errorWithName('ConnectionError'))).toBe(false); - expect(isUserError(new Error('unknown'))).toBe(false); - }); -}); diff --git a/src/cli/telemetry/client.ts b/src/cli/telemetry/client.ts index 2341231ea..14082ac42 100644 --- a/src/cli/telemetry/client.ts +++ b/src/cli/telemetry/client.ts @@ -1,4 +1,4 @@ -import { classifyError, isUserError } from './error-classification.js'; +import { classifyError } from './error.js'; import { COMMAND_SCHEMAS, type Command, type CommandAttrs, deriveCommandGroup } from './schemas/command-run.js'; import { type CommandResult, CommandResultSchema, resilientParse } from './schemas/common-shapes.js'; import type { MetricSink } from './sinks/metric-sink.js'; @@ -39,10 +39,11 @@ export class TelemetryClient { this.recordCommandRun(command, { exit_reason: 'success' }, result, durationMs); } } catch (err) { + const { category, source } = classifyError(err); const failureResult: CommandResult & { exit_reason: 'failure' } = { exit_reason: 'failure', - error_name: classifyError(err), - is_user_error: isUserError(err), + error_name: category, + error_source: source, }; this.recordCommandRun(command, failureResult, fallbackAttrs ?? {}, Math.round(performance.now() - start)); throw err; diff --git a/src/cli/telemetry/error-classification.ts b/src/cli/telemetry/error-classification.ts deleted file mode 100644 index 1f5627699..000000000 --- a/src/cli/telemetry/error-classification.ts +++ /dev/null @@ -1,67 +0,0 @@ -import { type ErrorCategory } from './schemas/common-shapes.js'; -import type { z } from 'zod'; - -type ErrorCategoryValue = z.infer; - -const CONFIG_ERRORS = new Set([ - 'ConfigValidationError', - 'ConfigNotFoundError', - 'ConfigReadError', - 'ConfigWriteError', - 'ConfigParseError', - 'ValidationError', -]); -const PACKAGING_ERRORS = new Set([ - 'PackagingError', - 'MissingDependencyError', - 'MissingProjectFileError', - 'UnsupportedLanguageError', - 'ArtifactSizeError', - 'DependencyCheckError', -]); -const CREDENTIAL_ERRORS = new Set([ - 'AwsCredentialsError', - 'AccessDeniedException', - 'AccessDenied', - 'AccessDeniedError', - 'ExpiredToken', - 'ExpiredTokenException', - 'TokenRefreshRequired', - 'CredentialsExpired', - 'InvalidIdentityToken', - 'UnauthorizedAccess', - 'InvalidClientTokenId', -]); -const PROJECT_ERRORS = new Set(['NoProjectError', 'AgentAlreadyExistsError']); -const CONNECTION_ERRORS = new Set(['ConnectionError', 'ServerError']); -const SERVICE_ERRORS = new Set([ - 'ResourceNotFoundException', - 'ValidationException', - 'ConflictException', - 'ResourceAlreadyExistsException', - 'ResourceNotFoundError', - 'ConflictError', -]); - -const USER_CATEGORIES = new Set(['ConfigError', 'CredentialsError', 'ProjectError']); - -export function classifyError(err: unknown): ErrorCategoryValue { - if (!(err instanceof Error)) return 'UnknownError'; - const name = - err.constructor.name === 'Error' - ? 'name' in err && typeof err.name === 'string' - ? err.name - : 'Error' - : err.constructor.name; - if (CONFIG_ERRORS.has(name)) return 'ConfigError'; - if (CREDENTIAL_ERRORS.has(name)) return 'CredentialsError'; - if (PACKAGING_ERRORS.has(name)) return 'PackagingError'; - if (PROJECT_ERRORS.has(name)) return 'ProjectError'; - if (SERVICE_ERRORS.has(name)) return 'ServiceError'; - if (CONNECTION_ERRORS.has(name)) return 'ConnectionError'; - return 'UnknownError'; -} - -export function isUserError(err: unknown): boolean { - return USER_CATEGORIES.has(classifyError(err)); -} diff --git a/src/cli/telemetry/error.ts b/src/cli/telemetry/error.ts new file mode 100644 index 000000000..91bc83029 --- /dev/null +++ b/src/cli/telemetry/error.ts @@ -0,0 +1,33 @@ +import { BaseError, type ErrorSource } from '../../lib/errors/types.js'; +import { ErrorName } from './schemas/common-shapes.js'; +import type { z } from 'zod'; + +type ErrorNameValue = z.infer; + +// Maps common AWS SDK error names to a category and source. +const SDK_ERROR_MAP: Record = { + AccessDeniedException: { category: 'AccessDeniedError', source: 'user' }, + AccessDenied: { category: 'AccessDeniedError', source: 'user' }, + ExpiredToken: { category: 'AwsCredentialsError', source: 'user' }, + ExpiredTokenException: { category: 'AwsCredentialsError', source: 'user' }, + InvalidClientTokenId: { category: 'AwsCredentialsError', source: 'user' }, + TokenRefreshRequired: { category: 'AwsCredentialsError', source: 'user' }, + CredentialsExpired: { category: 'AwsCredentialsError', source: 'user' }, + InvalidIdentityToken: { category: 'AwsCredentialsError', source: 'user' }, + UnauthorizedAccess: { category: 'AwsCredentialsError', source: 'user' }, + ValidationException: { category: 'ValidationError', source: 'user' }, + ResourceNotFoundException: { category: 'ResourceNotFoundError', source: 'user' }, + ConflictException: { category: 'ConflictError', source: 'user' }, + ResourceAlreadyExistsException: { category: 'ConflictError', source: 'user' }, +}; + +export function classifyError(err: unknown): { category: ErrorNameValue; source: ErrorSource } { + if (err instanceof BaseError) { + const parsed = ErrorName.safeParse(err.name); + return { category: parsed.success ? parsed.data : 'UnknownError', source: err.errorSource }; + } + if (err instanceof Error && err.name in SDK_ERROR_MAP) { + return SDK_ERROR_MAP[err.name]!; + } + return { category: 'UnknownError', source: 'unknown' }; +} diff --git a/src/cli/telemetry/index.ts b/src/cli/telemetry/index.ts index 778376713..911141c6e 100644 --- a/src/cli/telemetry/index.ts +++ b/src/cli/telemetry/index.ts @@ -5,4 +5,3 @@ export { TelemetryClient, CANCELLED } from './client.js'; export { type MetricSink, CompositeSink } from './sinks/metric-sink.js'; export { OtelMetricSink, type OtelMetricSinkConfig } from './sinks/otel-metric-sink.js'; export { FileSystemSink, type FileSystemSinkConfig } from './sinks/filesystem-sink.js'; -export { classifyError, isUserError } from './error-classification.js'; diff --git a/src/cli/telemetry/schemas/__tests__/command-run.test.ts b/src/cli/telemetry/schemas/__tests__/command-run.test.ts index eae73cabc..e0ad17f25 100644 --- a/src/cli/telemetry/schemas/__tests__/command-run.test.ts +++ b/src/cli/telemetry/schemas/__tests__/command-run.test.ts @@ -12,10 +12,10 @@ describe('CommandResultSchema', () => { it('accepts failure with required error fields', () => { const result = CommandResultSchema.parse({ exit_reason: 'failure', - error_name: 'PackagingError', - is_user_error: false, + error_name: 'DependencyCheckError', + error_source: 'user', }); - expect(result).toMatchObject({ exit_reason: 'failure', error_name: 'PackagingError' }); + expect(result).toMatchObject({ exit_reason: 'failure', error_name: 'DependencyCheckError' }); }); it('rejects failure missing error_name', () => { diff --git a/src/cli/telemetry/schemas/common-shapes.ts b/src/cli/telemetry/schemas/common-shapes.ts index 429716096..d9622512a 100644 --- a/src/cli/telemetry/schemas/common-shapes.ts +++ b/src/cli/telemetry/schemas/common-shapes.ts @@ -101,23 +101,43 @@ export const ResourceType = z.enum(['gateway', 'agent']); export const SourceType = z.enum(['file', 'statement', 'generate']); export const ValidationMode = z.enum(['fail_on_any_findings', 'ignore_all_findings']); -export const ErrorCategory = z.enum([ - 'ConfigError', - 'CredentialsError', - 'PackagingError', - 'ProjectError', - 'ServiceError', +export const ErrorName = z.enum([ + 'AccessDeniedError', + 'AgentAlreadyExistsError', + 'ArtifactSizeError', + 'AwsCredentialsError', + 'ConfigNotFoundError', + 'ConfigParseError', + 'ConfigReadError', + 'ConfigValidationError', + 'ConfigWriteError', + 'ConflictError', 'ConnectionError', + 'DependencyCheckError', + 'GitInitError', + 'MissingDependencyError', + 'MissingProjectFileError', + 'NoProjectError', + 'PackagingError', + 'PollExhaustedError', + 'PollTimeoutError', + 'ResourceNotFoundError', + 'ServerError', + 'TimeoutError', + 'UnsupportedLanguageError', + 'ValidationError', 'UnknownError', ]); +export const ErrorSource = z.enum(['user', 'client', 'service', 'unknown']); + // Common result shapes — reusable across metrics export const SuccessResult = z.object({ exit_reason: z.literal('success') }); export const CancelResult = z.object({ exit_reason: z.literal('cancel') }); export const FailureResult = z.object({ exit_reason: z.literal('failure'), - error_name: ErrorCategory, - is_user_error: z.boolean(), + error_name: ErrorName, + error_source: ErrorSource, }); export const CommandResultSchema = z.discriminatedUnion('exit_reason', [SuccessResult, CancelResult, FailureResult]); export type CommandResult = z.infer; diff --git a/src/cli/telemetry/schemas/index.ts b/src/cli/telemetry/schemas/index.ts index 7110f61d9..dd921df7a 100644 --- a/src/cli/telemetry/schemas/index.ts +++ b/src/cli/telemetry/schemas/index.ts @@ -1,7 +1,8 @@ export { CommandResultSchema, Count, - ErrorCategory, + ErrorName, + ErrorSource, ExitReason, FailureResult, Mode, diff --git a/src/cli/tui/hooks/useCdkPreflight.ts b/src/cli/tui/hooks/useCdkPreflight.ts index 062da752f..4602c1c14 100644 --- a/src/cli/tui/hooks/useCdkPreflight.ts +++ b/src/cli/tui/hooks/useCdkPreflight.ts @@ -1,7 +1,8 @@ import { ConfigIO, SecureCredentials } from '../../../lib'; +import { AwsCredentialsError } from '../../../lib/errors/types'; import type { DeployedState } from '../../../schema'; import { applyTargetRegionToEnv } from '../../aws'; -import { AwsCredentialsError, validateAwsCredentials } from '../../aws/account'; +import { validateAwsCredentials } from '../../aws/account'; import { type CdkToolkitWrapper, type SwitchableIoHost, createSwitchableIoHost } from '../../cdk/toolkit-lib'; import { getErrorMessage, isExpiredTokenError, isNoCredentialsError } from '../../errors'; import type { ExecLogger } from '../../logging'; diff --git a/src/cli/tui/hooks/useDevServer.ts b/src/cli/tui/hooks/useDevServer.ts index c964b2419..d5a6f0075 100644 --- a/src/cli/tui/hooks/useDevServer.ts +++ b/src/cli/tui/hooks/useDevServer.ts @@ -1,15 +1,14 @@ import { findConfigRoot } from '../../../lib'; +import { ConnectionError, ServerError } from '../../../lib/errors/types'; import type { AgentCoreProjectSpec, ProtocolMode } from '../../../schema'; import { detectContainerRuntime } from '../../external-requirements'; import { DevLogger } from '../../logging/dev-logger'; import { type A2AAgentCard, - ConnectionError, type DevConfig, DevServer, type LogLevel, type McpTool, - ServerError, callMcpTool, createDevServer, fetchA2AAgentCard, diff --git a/src/lib/errors/__tests__/config.test.ts b/src/lib/errors/__tests__/config.test.ts index 365afc7f5..ffba67445 100644 --- a/src/lib/errors/__tests__/config.test.ts +++ b/src/lib/errors/__tests__/config.test.ts @@ -5,7 +5,7 @@ import { ConfigReadError, ConfigValidationError, ConfigWriteError, -} from '../config.js'; +} from '../types.js'; import { describe, expect, it } from 'vitest'; import { ZodError, ZodIssueCode, z } from 'zod'; diff --git a/src/lib/errors/index.ts b/src/lib/errors/index.ts index 2acd167c8..fcb073fef 100644 --- a/src/lib/errors/index.ts +++ b/src/lib/errors/index.ts @@ -1,2 +1 @@ -export * from './config'; export * from './types'; diff --git a/src/lib/errors/types.ts b/src/lib/errors/types.ts index ca6bfded5..16940a3d2 100644 --- a/src/lib/errors/types.ts +++ b/src/lib/errors/types.ts @@ -1,104 +1,292 @@ +import { formatZodErrors } from './zod.js'; +import { ZodError } from 'zod'; + +export type ErrorSource = 'user' | 'client' | 'service' | 'unknown'; + +export interface BaseErrorOptions extends ErrorOptions { + errorSource?: ErrorSource; +} + +interface InternalErrorOptions extends BaseErrorOptions { + defaultSource: ErrorSource; +} + +export abstract class BaseError extends Error { + readonly errorSource: ErrorSource; + + protected constructor(message: string, options: InternalErrorOptions) { + super(message, options); + this.name = this.constructor.name; + this.errorSource = options.errorSource ?? options.defaultSource; + } +} + +/** + * Converts an unknown thrown value to an Error instance. + * Use in catch blocks to ensure the error field is always an Error object. + */ +export function toError(err: unknown): Error { + return err instanceof Error ? err : new Error(String(err)); +} + +// --- User errors --- + /** * Error indicating no agentcore project was found in the working directory. */ -export class NoProjectError extends Error { - constructor(message?: string) { - super(message ?? 'No agentcore project found. Run "agentcore create" first.'); - this.name = 'NoProjectError'; +export class NoProjectError extends BaseError { + constructor(message?: string, options?: BaseErrorOptions) { + super(message ?? 'No agentcore project found. Run "agentcore create" first.', { + defaultSource: 'user', + ...options, + }); } } /** * Error thrown when an agent with the same name already exists. */ -export class AgentAlreadyExistsError extends Error { - constructor(agentName: string) { - super(`An agent named "${agentName}" already exists in the schema.`); - this.name = 'AgentAlreadyExistsError'; +export class AgentAlreadyExistsError extends BaseError { + constructor(agentName: string, options?: BaseErrorOptions) { + super(`An agent named "${agentName}" already exists in the schema.`, { defaultSource: 'user', ...options }); } } /** * Error indicating an AWS permissions failure (AccessDenied / AccessDeniedException). */ -export class AccessDeniedError extends Error { - constructor(message: string, options?: ErrorOptions) { - super(message, options); - this.name = 'AccessDeniedError'; +export class AccessDeniedError extends BaseError { + constructor(message: string, options?: BaseErrorOptions) { + super(message, { defaultSource: 'user', ...options }); } } /** * Error indicating missing system dependencies required for an operation. */ -export class DependencyCheckError extends Error { - constructor(errors: string[]) { - super(errors.join('\n')); - this.name = 'DependencyCheckError'; +export class DependencyCheckError extends BaseError { + constructor(errors: string[], options?: BaseErrorOptions) { + super(errors.join('\n'), { defaultSource: 'user', ...options }); } } /** - * Error indicating git repository initialization failed. + * Error indicating a referenced resource could not be found. */ -export class GitInitError extends Error { - constructor(message: string) { - super(message); - this.name = 'GitInitError'; +export class ResourceNotFoundError extends BaseError { + constructor(message: string, options?: BaseErrorOptions) { + super(message, { defaultSource: 'user', ...options }); } } /** - * Error indicating a referenced resource could not be found. + * Error indicating invalid input or configuration values. */ -export class ResourceNotFoundError extends Error { - constructor(message: string) { - super(message); - this.name = 'ResourceNotFoundError'; +export class ValidationError extends BaseError { + constructor(message: string, options?: BaseErrorOptions) { + super(message, { defaultSource: 'user', ...options }); } } -export class ValidationError extends Error { - constructor(message: string, options?: ErrorOptions) { - super(message, options); - this.name = 'ValidationError'; +/** + * Error thrown when AWS credentials are not configured or invalid. + * Supports both a short message (for interactive mode) and detailed message (for CLI mode). + */ +export class AwsCredentialsError extends BaseError { + readonly shortMessage: string; + constructor(shortMessage: string, detailedMessage?: string, options?: BaseErrorOptions) { + super(detailedMessage ?? shortMessage, { defaultSource: 'user', ...options }); + this.shortMessage = shortMessage; } } /** - * Error indicating a network connection failure (e.g., server not reachable). + * Error indicating a packaging operation failed. */ -export class ConnectionError extends Error { - constructor(message: string, options?: ErrorOptions) { - super(message, options); - this.name = 'ConnectionError'; +export class PackagingError extends BaseError { + constructor(message: string, options?: BaseErrorOptions) { + super(message, { defaultSource: 'user', ...options }); } } /** - * Converts an unknown thrown value to an Error instance. - * Use in catch blocks to ensure the error field is always an Error object. + * Error indicating the packaged artifact exceeds the size limit. */ -export function toError(err: unknown): Error { - return err instanceof Error ? err : new Error(String(err)); +export class ArtifactSizeError extends PackagingError { + constructor(limitBytes: number, actualBytes: number, options?: BaseErrorOptions) { + super(`Packaged artifact exceeds ${limitBytes} bytes (actual: ${actualBytes}).`, options); + } +} + +/** + * Error indicating a required binary or tool is not installed. + */ +export class MissingDependencyError extends PackagingError { + constructor(binary: string, installHint?: string, options?: BaseErrorOptions) { + super(installHint ? `${binary} is required. ${installHint}` : `${binary} is required.`, options); + } +} + +/** + * Error indicating a required project file is missing. + */ +export class MissingProjectFileError extends PackagingError { + constructor(filePath: string, options?: BaseErrorOptions) { + super(`Required project file not found: ${filePath}`, options); + } +} + +/** + * Error indicating the project language is not supported for packaging. + */ +export class UnsupportedLanguageError extends PackagingError { + constructor(language: string, options?: BaseErrorOptions) { + super(`${language} packaging is not supported yet.`, options); + } +} + +// --- Config errors (user) --- + +/** + * Base class for all config-related errors. + */ +export abstract class ConfigError extends BaseError { + protected constructor(message: string, options?: BaseErrorOptions) { + super(message, { defaultSource: 'user', ...options }); + } } +/** + * Thrown when a config file doesn't exist. + */ +export class ConfigNotFoundError extends ConfigError { + constructor( + public readonly filePath: string, + public readonly fileType: string + ) { + super(`${fileType} config file not found at: ${filePath}`); + } +} + +/** + * Thrown when a config file can't be read. + */ +export class ConfigReadError extends ConfigError { + constructor( + public readonly filePath: string, + public override readonly cause: unknown + ) { + const message = cause instanceof Error ? cause.message : String(cause); + super(`Failed to read config file at ${filePath}: ${message}`); + } +} + +/** + * Thrown when a config file can't be written. + */ +export class ConfigWriteError extends ConfigError { + constructor( + public readonly filePath: string, + public override readonly cause: unknown + ) { + const message = cause instanceof Error ? cause.message : String(cause); + super(`Failed to write config file at ${filePath}: ${message}`); + } +} + +/** + * Thrown when config validation fails. + */ +export class ConfigValidationError extends ConfigError { + constructor( + public readonly filePath: string, + public readonly fileType: string, + public readonly zodError: ZodError + ) { + super(`${filePath}:\n${formatZodErrors(zodError.issues)}`); + } +} + +/** + * Thrown when JSON parsing fails. + */ +export class ConfigParseError extends ConfigError { + constructor( + public readonly filePath: string, + public override readonly cause: unknown + ) { + const message = cause instanceof Error ? cause.message : String(cause); + super(`Failed to parse JSON in config file at ${filePath}: ${message}`); + } +} + +// --- Client errors --- + +/** + * Error indicating git repository initialization failed. + */ +export class GitInitError extends BaseError { + constructor(message: string, options?: BaseErrorOptions) { + super(message, { defaultSource: 'client', ...options }); + } +} + +// --- Service errors --- + /** * Error indicating a resource conflict (e.g., already exists). */ -export class ConflictError extends Error { - constructor(message: string) { - super(message); - this.name = 'ConflictError'; +export class ConflictError extends BaseError { + constructor(message: string, options?: BaseErrorOptions) { + super(message, { defaultSource: 'user', ...options }); } } /** * Error indicating an operation timed out or exceeded retry limits. */ -export class TimeoutError extends Error { - constructor(message: string) { - super(message); - this.name = 'TimeoutError'; +export class TimeoutError extends BaseError { + constructor(message: string, options?: BaseErrorOptions) { + super(message, { defaultSource: 'service', ...options }); + } +} + +/** + * Error thrown when the dev server returns a non-OK HTTP response. + */ +export class ServerError extends BaseError { + constructor( + public readonly statusCode: number, + body: string, + options?: BaseErrorOptions + ) { + super(body || `Server returned ${statusCode}`, { defaultSource: 'client', ...options }); + } +} + +/** + * Error thrown when the connection to the dev server fails. + */ +export class ConnectionError extends BaseError { + constructor(message: string, options?: BaseErrorOptions) { + super(message, { defaultSource: 'client', ...options }); + } +} + +/** + * Error indicating polling timed out. + */ +export class PollTimeoutError extends BaseError { + constructor(timeoutMs: number, options?: BaseErrorOptions) { + super(`Polling timed out after ${timeoutMs}ms`, { defaultSource: 'service', ...options }); + } +} + +/** + * Error indicating polling exhausted all retry attempts. + */ +export class PollExhaustedError extends BaseError { + constructor(maxAttempts: number, options?: BaseErrorOptions) { + super(`Polling exhausted after ${maxAttempts} attempts`, { defaultSource: 'service', ...options }); } } diff --git a/src/lib/errors/config.ts b/src/lib/errors/zod.ts similarity index 64% rename from src/lib/errors/config.ts rename to src/lib/errors/zod.ts index 4d7ccebe0..e940c308e 100644 --- a/src/lib/errors/config.ts +++ b/src/lib/errors/zod.ts @@ -1,65 +1,6 @@ -import { ZodError } from 'zod'; - -/** - * Base class for all config-related errors - */ -export abstract class ConfigError extends Error { - protected constructor(message: string) { - super(message); - this.name = this.constructor.name; - Error.captureStackTrace(this, this.constructor); - } -} - -/** - * Thrown when a config file doesn't exist - */ -export class ConfigNotFoundError extends ConfigError { - constructor( - public readonly filePath: string, - public readonly fileType: string - ) { - super(`${fileType} config file not found at: ${filePath}`); - } -} - -/** - * Thrown when a config file can't be read - */ -export class ConfigReadError extends ConfigError { - constructor( - public readonly filePath: string, - public override readonly cause: unknown - ) { - const message = cause instanceof Error ? cause.message : String(cause); - super(`Failed to read config file at ${filePath}: ${message}`); - } -} - -/** - * Thrown when a config file can't be written - */ -export class ConfigWriteError extends ConfigError { - constructor( - public readonly filePath: string, - public override readonly cause: unknown - ) { - const message = cause instanceof Error ? cause.message : String(cause); - super(`Failed to write config file at ${filePath}: ${message}`); - } -} - /** - * Format a Zod path array to bracket notation: agents[0].name + * Zod error formatting utilities for human-readable validation messages. */ -function formatPath(path: PropertyKey[]): string { - if (path.length === 0) return 'root'; - return path - .map((segment, i) => - typeof segment === 'number' ? `[${segment}]` : i === 0 ? String(segment) : `.${String(segment)}` - ) - .join(''); -} // Zod issue with extended properties for type-safe access (Zod 4 compatible) interface ZodIssueExt { @@ -76,6 +17,18 @@ interface ZodIssueExt { discriminator?: string; // Zod 4 discriminated union } +/** + * Format a Zod path array to bracket notation: agents[0].name + */ +function formatPath(path: PropertyKey[]): string { + if (path.length === 0) return 'root'; + return path + .map((segment, i) => + typeof segment === 'number' ? `[${segment}]` : i === 0 ? String(segment) : `.${String(segment)}` + ) + .join(''); +} + /** * Format a single Zod issue into an actionable message. * Augments common cases with "got X, expected Y" format. @@ -158,28 +111,8 @@ function formatZodIssue(issue: ZodIssueExt): string { } /** - * Thrown when config validation fails + * Format all issues from a ZodError into a newline-separated list. */ -export class ConfigValidationError extends ConfigError { - constructor( - public readonly filePath: string, - public readonly fileType: string, - public readonly zodError: ZodError - ) { - const formattedErrors = (zodError.issues as ZodIssueExt[]).map(issue => ` - ${formatZodIssue(issue)}`).join('\n'); - super(`${filePath}:\n${formattedErrors}`); - } -} - -/** - * Thrown when JSON parsing fails - */ -export class ConfigParseError extends ConfigError { - constructor( - public readonly filePath: string, - public override readonly cause: unknown - ) { - const message = cause instanceof Error ? cause.message : String(cause); - super(`Failed to parse JSON in config file at ${filePath}: ${message}`); - } +export function formatZodErrors(issues: unknown[]): string { + return (issues as ZodIssueExt[]).map(issue => ` - ${formatZodIssue(issue)}`).join('\n'); } diff --git a/src/lib/packaging/__tests__/errors.test.ts b/src/lib/packaging/__tests__/errors.test.ts index e6d07805b..2394126cf 100644 --- a/src/lib/packaging/__tests__/errors.test.ts +++ b/src/lib/packaging/__tests__/errors.test.ts @@ -4,7 +4,7 @@ import { MissingProjectFileError, PackagingError, UnsupportedLanguageError, -} from '../errors.js'; +} from '../../errors/types.js'; import { describe, expect, it } from 'vitest'; describe('PackagingError', () => { diff --git a/src/lib/packaging/container.ts b/src/lib/packaging/container.ts index a166a08f3..fc9400a86 100644 --- a/src/lib/packaging/container.ts +++ b/src/lib/packaging/container.ts @@ -1,7 +1,7 @@ import type { AgentEnvSpec } from '../../schema'; import { CONTAINER_RUNTIMES, DOCKERFILE_NAME, ONE_GB, getDockerfilePath } from '../constants'; +import { PackagingError } from '../errors/types'; import { getUvBuildArgs } from './build-args'; -import { PackagingError } from './errors'; import { resolveCodeLocation } from './helpers'; import type { ArtifactResult, PackageOptions, RuntimePackager } from './types/packaging'; import { spawnSync } from 'child_process'; diff --git a/src/lib/packaging/errors.ts b/src/lib/packaging/errors.ts deleted file mode 100644 index e88701ce2..000000000 --- a/src/lib/packaging/errors.ts +++ /dev/null @@ -1,31 +0,0 @@ -export class PackagingError extends Error { - constructor(message: string) { - super(message); - this.name = this.constructor.name; - Error.captureStackTrace(this, this.constructor); - } -} - -export class MissingDependencyError extends PackagingError { - constructor(binary: string, installHint?: string) { - super(installHint ? `${binary} is required. ${installHint}` : `${binary} is required.`); - } -} - -export class MissingProjectFileError extends PackagingError { - constructor(filePath: string) { - super(`Required project file not found: ${filePath}`); - } -} - -export class UnsupportedLanguageError extends PackagingError { - constructor(language: string) { - super(`${language} packaging is not supported yet.`); - } -} - -export class ArtifactSizeError extends PackagingError { - constructor(limitBytes: number, actualBytes: number) { - super(`Packaged artifact exceeds ${limitBytes} bytes (actual: ${actualBytes}).`); - } -} diff --git a/src/lib/packaging/helpers.ts b/src/lib/packaging/helpers.ts index 6f871c410..caa3d3792 100644 --- a/src/lib/packaging/helpers.ts +++ b/src/lib/packaging/helpers.ts @@ -1,8 +1,8 @@ import type { RuntimeVersion } from '../../schema'; import { CONFIG_DIR } from '../constants'; +import { ArtifactSizeError, MissingDependencyError, MissingProjectFileError } from '../errors/types'; import { isWindows } from '../utils/platform'; import { checkSubprocess, checkSubprocessSync, runSubprocess } from '../utils/subprocess'; -import { ArtifactSizeError, MissingDependencyError, MissingProjectFileError } from './errors'; import type { PackageOptions } from './types/packaging'; import type { Zippable } from 'fflate'; import { zipSync } from 'fflate'; diff --git a/src/lib/packaging/index.ts b/src/lib/packaging/index.ts index a281bbdbb..8c3067ca3 100644 --- a/src/lib/packaging/index.ts +++ b/src/lib/packaging/index.ts @@ -1,7 +1,7 @@ import type { AgentCoreProjectSpec, AgentEnvSpec, RuntimeVersion } from '../../schema'; import { DEFAULT_PYTHON_VERSION } from '../../schema'; +import { PackagingError } from '../errors/types'; import { ContainerPackager } from './container'; -import { PackagingError } from './errors'; import { isNodeRuntime, isPythonRuntime } from './helpers'; import { NodeCodeZipPackager, NodeCodeZipPackagerSync } from './node'; import { PythonCodeZipPackager, PythonCodeZipPackagerSync } from './python'; @@ -91,5 +91,5 @@ export type { PackageOptions, RuntimePackager, } from './types/packaging'; -export * from './errors'; + export { resolveCodeLocation } from './helpers'; diff --git a/src/lib/packaging/node.ts b/src/lib/packaging/node.ts index 2d12e5cd7..38f6efec2 100644 --- a/src/lib/packaging/node.ts +++ b/src/lib/packaging/node.ts @@ -1,6 +1,6 @@ import type { AgentEnvSpec, NodeRuntime, RuntimeVersion } from '../../schema'; import { getArtifactZipName } from '../constants'; -import { PackagingError } from './errors'; +import { PackagingError } from '../errors/types'; import { createZipFromDir, createZipFromDirSync, diff --git a/src/lib/packaging/python.ts b/src/lib/packaging/python.ts index eac12567f..0e4a7b2c3 100644 --- a/src/lib/packaging/python.ts +++ b/src/lib/packaging/python.ts @@ -1,8 +1,8 @@ import type { AgentEnvSpec, PythonRuntime, RuntimeVersion } from '../../schema'; import { DEFAULT_PYTHON_VERSION } from '../../schema'; import { UV_INSTALL_HINT, getArtifactZipName } from '../constants'; +import { PackagingError } from '../errors/types'; import { runSubprocessCapture, runSubprocessCaptureSync } from '../utils/subprocess'; -import { PackagingError } from './errors'; import { convertWindowsScriptsToLinux, convertWindowsScriptsToLinuxSync, diff --git a/src/lib/schemas/io/__tests__/config-io.test.ts b/src/lib/schemas/io/__tests__/config-io.test.ts index 99e885e28..a41edb28d 100644 --- a/src/lib/schemas/io/__tests__/config-io.test.ts +++ b/src/lib/schemas/io/__tests__/config-io.test.ts @@ -1,5 +1,5 @@ import { NoProjectError } from '../../../errors'; -import { ConfigNotFoundError, ConfigParseError, ConfigValidationError } from '../../../errors/config.js'; +import { ConfigNotFoundError, ConfigParseError, ConfigValidationError } from '../../../errors/types.js'; import { ConfigIO } from '../config-io.js'; import { randomUUID } from 'node:crypto'; import { existsSync, mkdirSync, writeFileSync } from 'node:fs'; diff --git a/src/lib/schemas/io/config-io.ts b/src/lib/schemas/io/config-io.ts index 37cc728c8..25841f4d8 100644 --- a/src/lib/schemas/io/config-io.ts +++ b/src/lib/schemas/io/config-io.ts @@ -12,8 +12,8 @@ import { ConfigReadError, ConfigValidationError, ConfigWriteError, + NoProjectError, } from '../../errors'; -import { NoProjectError } from '../../errors'; import { detectAwsAccount } from '../../utils'; import { type PathConfig, PathResolver, findConfigRoot } from './path-resolver'; import { loadSharedConfigFiles } from '@smithy/shared-ini-file-loader'; diff --git a/src/lib/utils/__tests__/polling.test.ts b/src/lib/utils/__tests__/polling.test.ts index c3440b669..581f6bc23 100644 --- a/src/lib/utils/__tests__/polling.test.ts +++ b/src/lib/utils/__tests__/polling.test.ts @@ -1,4 +1,5 @@ -import { PollExhaustedError, PollTimeoutError, isThrottlingError, poll } from '../polling.js'; +import { PollExhaustedError, PollTimeoutError } from '../../errors/types.js'; +import { isThrottlingError, poll } from '../polling.js'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; /* eslint-disable @typescript-eslint/require-await */ diff --git a/src/lib/utils/index.ts b/src/lib/utils/index.ts index 8902fca44..1ab339321 100644 --- a/src/lib/utils/index.ts +++ b/src/lib/utils/index.ts @@ -11,5 +11,5 @@ export { } from './subprocess'; export { parseTimeString } from './time-parser'; export { parseJsonRpcResponse } from './json-rpc'; -export { poll, isThrottlingError, PollTimeoutError, PollExhaustedError } from './polling'; +export { poll, isThrottlingError } from './polling'; export { validateAgentSchema, validateProjectSchema } from './zod'; diff --git a/src/lib/utils/polling.ts b/src/lib/utils/polling.ts index 3486e634f..012e02140 100644 --- a/src/lib/utils/polling.ts +++ b/src/lib/utils/polling.ts @@ -1,3 +1,5 @@ +import { PollExhaustedError, PollTimeoutError } from '../errors/types.js'; + /** * Shared polling/retry utility for async operations. */ @@ -23,20 +25,6 @@ export interface PollOptions { onError?: (err: unknown) => 'retry' | 'abort'; } -export class PollTimeoutError extends Error { - constructor(timeoutMs: number, options?: { cause?: unknown }) { - super(`Polling timed out after ${timeoutMs}ms`, options); - this.name = 'PollTimeoutError'; - } -} - -export class PollExhaustedError extends Error { - constructor(maxAttempts: number, options?: { cause?: unknown }) { - super(`Polling exhausted after ${maxAttempts} attempts`, options); - this.name = 'PollExhaustedError'; - } -} - export async function poll(options: PollOptions): Promise { const { fn,