diff --git a/core/src/common.ts b/core/src/common.ts index a05b87cd1..b05a34be1 100644 --- a/core/src/common.ts +++ b/core/src/common.ts @@ -146,6 +146,14 @@ export {TruncatingContextCompactor} from './context/truncating_context_compactor export type {TruncatingContextCompactorOptions} from './context/truncating_context_compactor.js'; export {BaseEnvironment} from './environment/base_environment.js'; export type {ExecutionResult} from './environment/base_environment.js'; +export {AlreadyExistsError} from './errors/already_exists_error.js'; +export {InputValidationError} from './errors/input_validation_error.js'; +export {NotFoundError} from './errors/not_found_error.js'; +export {SessionNotFoundError} from './errors/session_not_found_error.js'; +export { + ToolErrorType, + ToolExecutionError, +} from './errors/tool_execution_error.js'; export {isCompactedEvent, isScratchpadEvent} from './events/compacted_event.js'; export type {CompactedEvent} from './events/compacted_event.js'; export { diff --git a/core/src/errors/already_exists_error.ts b/core/src/errors/already_exists_error.ts new file mode 100644 index 000000000..59adb0081 --- /dev/null +++ b/core/src/errors/already_exists_error.ts @@ -0,0 +1,18 @@ +/** + * @license + * Copyright 2026 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * Represents an error that occurs when an entity already exists. + */ +export class AlreadyExistsError extends Error { + /** + * @param message An optional custom message to describe the error. + */ + constructor(message = 'The resource already exists.') { + super(message); + this.name = 'AlreadyExistsError'; + } +} diff --git a/core/src/errors/input_validation_error.ts b/core/src/errors/input_validation_error.ts new file mode 100644 index 000000000..4d2de2dd4 --- /dev/null +++ b/core/src/errors/input_validation_error.ts @@ -0,0 +1,18 @@ +/** + * @license + * Copyright 2026 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * Represents an error raised when user input fails validation. + */ +export class InputValidationError extends Error { + /** + * @param message A message describing why the input is invalid. + */ + constructor(message = 'Invalid input.') { + super(message); + this.name = 'InputValidationError'; + } +} diff --git a/core/src/errors/not_found_error.ts b/core/src/errors/not_found_error.ts new file mode 100644 index 000000000..0e755b470 --- /dev/null +++ b/core/src/errors/not_found_error.ts @@ -0,0 +1,18 @@ +/** + * @license + * Copyright 2026 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * Represents an error that occurs when an entity is not found. + */ +export class NotFoundError extends Error { + /** + * @param message An optional custom message to describe the error. + */ + constructor(message = 'The requested item was not found.') { + super(message); + this.name = 'NotFoundError'; + } +} diff --git a/core/src/errors/session_not_found_error.ts b/core/src/errors/session_not_found_error.ts new file mode 100644 index 000000000..2e5e15e75 --- /dev/null +++ b/core/src/errors/session_not_found_error.ts @@ -0,0 +1,18 @@ +/** + * @license + * Copyright 2026 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * Raised when a session cannot be found. + */ +export class SessionNotFoundError extends Error { + /** + * @param message An optional custom message to describe the error. + */ + constructor(message = 'Session not found.') { + super(message); + this.name = 'SessionNotFoundError'; + } +} diff --git a/core/src/errors/tool_execution_error.ts b/core/src/errors/tool_execution_error.ts new file mode 100644 index 000000000..0169073be --- /dev/null +++ b/core/src/errors/tool_execution_error.ts @@ -0,0 +1,43 @@ +/** + * @license + * Copyright 2026 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * HTTP error types conforming to OpenTelemetry semantics. + * + * The string values populate the `error.type` span attribute, so they are + * observable outside the process and must stay identical to the adk-python + * `ToolErrorType` members. + */ +export enum ToolErrorType { + BAD_REQUEST = 'BAD_REQUEST', + UNAUTHORIZED = 'UNAUTHORIZED', + FORBIDDEN = 'FORBIDDEN', + NOT_FOUND = 'NOT_FOUND', + REQUEST_TIMEOUT = 'REQUEST_TIMEOUT', + INTERNAL_SERVER_ERROR = 'INTERNAL_SERVER_ERROR', + BAD_GATEWAY = 'BAD_GATEWAY', + SERVICE_UNAVAILABLE = 'SERVICE_UNAVAILABLE', + GATEWAY_TIMEOUT = 'GATEWAY_TIMEOUT', +} + +/** + * Represents an error that occurs during the execution of a tool. + */ +export class ToolExecutionError extends Error { + /** + * @param message A message describing the error. + * @param errorType The semantic error type (e.g. + * {@link ToolErrorType.REQUEST_TIMEOUT} or `'500'`). Used to populate the + * `error.type` span attribute in OpenTelemetry traces. + */ + constructor( + message: string, + readonly errorType?: ToolErrorType | string, + ) { + super(message); + this.name = 'ToolExecutionError'; + } +} diff --git a/core/test/errors/already_exists_error_test.ts b/core/test/errors/already_exists_error_test.ts new file mode 100644 index 000000000..42cadff9b --- /dev/null +++ b/core/test/errors/already_exists_error_test.ts @@ -0,0 +1,46 @@ +/** + * @license + * Copyright 2026 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import {AlreadyExistsError, NotFoundError} from '@google/adk'; +import {describe, expect, it} from 'vitest'; + +describe('AlreadyExistsError', () => { + it('defaults the message when none is supplied', () => { + expect(new AlreadyExistsError().message).toBe( + 'The resource already exists.', + ); + expect(new AlreadyExistsError(undefined).message).toBe( + 'The resource already exists.', + ); + }); + + it('stores a supplied message verbatim', () => { + expect(new AlreadyExistsError('Session 42 already exists.').message).toBe( + 'Session 42 already exists.', + ); + expect(new AlreadyExistsError('').message).toBe(''); + }); + + it('sets name', () => { + expect(new AlreadyExistsError().name).toBe('AlreadyExistsError'); + }); + + it('is an instance of itself and of Error', () => { + const error = new AlreadyExistsError(); + expect(error).toBeInstanceOf(AlreadyExistsError); + expect(error).toBeInstanceOf(Error); + }); + + it('is not an instance of a sibling error class', () => { + expect(new AlreadyExistsError()).not.toBeInstanceOf(NotFoundError); + }); + + it('can be thrown and caught by type', () => { + expect(() => { + throw new AlreadyExistsError('boom'); + }).toThrow(AlreadyExistsError); + }); +}); diff --git a/core/test/errors/input_validation_error_test.ts b/core/test/errors/input_validation_error_test.ts new file mode 100644 index 000000000..a640453da --- /dev/null +++ b/core/test/errors/input_validation_error_test.ts @@ -0,0 +1,42 @@ +/** + * @license + * Copyright 2026 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import {InputValidationError, NotFoundError} from '@google/adk'; +import {describe, expect, it} from 'vitest'; + +describe('InputValidationError', () => { + it('defaults the message when none is supplied', () => { + expect(new InputValidationError().message).toBe('Invalid input.'); + expect(new InputValidationError(undefined).message).toBe('Invalid input.'); + }); + + it('stores a supplied message verbatim', () => { + expect(new InputValidationError('appName is required.').message).toBe( + 'appName is required.', + ); + expect(new InputValidationError('').message).toBe(''); + }); + + it('sets name', () => { + expect(new InputValidationError().name).toBe('InputValidationError'); + }); + + it('is an instance of itself and of Error', () => { + const error = new InputValidationError(); + expect(error).toBeInstanceOf(InputValidationError); + expect(error).toBeInstanceOf(Error); + }); + + it('is not an instance of a sibling error class', () => { + expect(new InputValidationError()).not.toBeInstanceOf(NotFoundError); + }); + + it('can be thrown and caught by type', () => { + expect(() => { + throw new InputValidationError('boom'); + }).toThrow(InputValidationError); + }); +}); diff --git a/core/test/errors/not_found_error_test.ts b/core/test/errors/not_found_error_test.ts new file mode 100644 index 000000000..e20150d65 --- /dev/null +++ b/core/test/errors/not_found_error_test.ts @@ -0,0 +1,50 @@ +/** + * @license + * Copyright 2026 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import {NotFoundError, SessionNotFoundError} from '@google/adk'; +import {describe, expect, it} from 'vitest'; + +describe('NotFoundError', () => { + it('defaults the message when none is supplied', () => { + expect(new NotFoundError().message).toBe( + 'The requested item was not found.', + ); + expect(new NotFoundError(undefined).message).toBe( + 'The requested item was not found.', + ); + }); + + it('stores a supplied message verbatim', () => { + expect(new NotFoundError('No eval set foo.').message).toBe( + 'No eval set foo.', + ); + // An empty string is a supplied argument, so it must not fall back to the + // default: only `undefined` triggers a default parameter. + expect(new NotFoundError('').message).toBe(''); + // No sanitisation: `$` replacement patterns are stored as written. + expect(new NotFoundError("a $& b $' c").message).toBe("a $& b $' c"); + }); + + it('sets name', () => { + expect(new NotFoundError().name).toBe('NotFoundError'); + }); + + it('is an instance of itself and of Error', () => { + const error = new NotFoundError(); + expect(error).toBeInstanceOf(NotFoundError); + expect(error).toBeInstanceOf(Error); + }); + + it('is not an instance of a sibling error class', () => { + expect(new NotFoundError()).not.toBeInstanceOf(SessionNotFoundError); + }); + + it('can be thrown and caught by type', () => { + expect(() => { + throw new NotFoundError('boom'); + }).toThrow(NotFoundError); + }); +}); diff --git a/core/test/errors/session_not_found_error_test.ts b/core/test/errors/session_not_found_error_test.ts new file mode 100644 index 000000000..f649c41dc --- /dev/null +++ b/core/test/errors/session_not_found_error_test.ts @@ -0,0 +1,47 @@ +/** + * @license + * Copyright 2026 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import {NotFoundError, SessionNotFoundError} from '@google/adk'; +import {describe, expect, it} from 'vitest'; + +describe('SessionNotFoundError', () => { + it('defaults the message when none is supplied', () => { + expect(new SessionNotFoundError().message).toBe('Session not found.'); + expect(new SessionNotFoundError(undefined).message).toBe( + 'Session not found.', + ); + }); + + it('stores a supplied message verbatim', () => { + expect(new SessionNotFoundError('No session 42.').message).toBe( + 'No session 42.', + ); + expect(new SessionNotFoundError('').message).toBe(''); + }); + + it('sets name', () => { + expect(new SessionNotFoundError().name).toBe('SessionNotFoundError'); + }); + + it('is an instance of itself and of Error', () => { + const error = new SessionNotFoundError(); + expect(error).toBeInstanceOf(SessionNotFoundError); + expect(error).toBeInstanceOf(Error); + }); + + it('is not an instance of a sibling error class', () => { + // The hierarchy is flat: SessionNotFoundError must not extend + // NotFoundError, or `catch (e) { if (e instanceof NotFoundError) }` would + // start swallowing session lookups it does not swallow in adk-python. + expect(new SessionNotFoundError()).not.toBeInstanceOf(NotFoundError); + }); + + it('can be thrown and caught by type', () => { + expect(() => { + throw new SessionNotFoundError('boom'); + }).toThrow(SessionNotFoundError); + }); +}); diff --git a/core/test/errors/tool_execution_error_test.ts b/core/test/errors/tool_execution_error_test.ts new file mode 100644 index 000000000..ecf21a7db --- /dev/null +++ b/core/test/errors/tool_execution_error_test.ts @@ -0,0 +1,79 @@ +/** + * @license + * Copyright 2026 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import {NotFoundError, ToolErrorType, ToolExecutionError} from '@google/adk'; +import {describe, expect, it} from 'vitest'; + +describe('ToolErrorType', () => { + it('matches the adk-python members, in declaration order', () => { + expect(Object.values(ToolErrorType)).toEqual([ + 'BAD_REQUEST', + 'UNAUTHORIZED', + 'FORBIDDEN', + 'NOT_FOUND', + 'REQUEST_TIMEOUT', + 'INTERNAL_SERVER_ERROR', + 'BAD_GATEWAY', + 'SERVICE_UNAVAILABLE', + 'GATEWAY_TIMEOUT', + ]); + // Pins the count independently, so an added member fails even if the + // expected list above is ever loosened. + expect(Object.values(ToolErrorType)).toHaveLength(9); + }); + + it('gives every member a value identical to its name', () => { + for (const [name, value] of Object.entries(ToolErrorType)) { + expect(value).toBe(name); + } + }); +}); + +describe('ToolExecutionError', () => { + it('stores the message and leaves errorType undefined when not supplied', () => { + const error = new ToolExecutionError('Tool blew up.'); + expect(error.message).toBe('Tool blew up.'); + expect(error.errorType).toBeUndefined(); + }); + + it('stores a ToolErrorType member', () => { + expect( + new ToolExecutionError('boom', ToolErrorType.BAD_REQUEST).errorType, + ).toBe(ToolErrorType.BAD_REQUEST); + }); + + it('stores a member as its own string value, needing no normalisation', () => { + // adk-python unwraps `error_type.value`; a TypeScript string-enum member + // already *is* that string, so direct assignment is equivalent. + expect( + new ToolExecutionError('boom', ToolErrorType.NOT_FOUND).errorType, + ).toBe('NOT_FOUND'); + }); + + it('stores a raw string errorType verbatim', () => { + expect(new ToolExecutionError('boom', '500').errorType).toBe('500'); + }); + + it('sets name', () => { + expect(new ToolExecutionError('boom').name).toBe('ToolExecutionError'); + }); + + it('is an instance of itself and of Error', () => { + const error = new ToolExecutionError('boom'); + expect(error).toBeInstanceOf(ToolExecutionError); + expect(error).toBeInstanceOf(Error); + }); + + it('is not an instance of a sibling error class', () => { + expect(new ToolExecutionError('boom')).not.toBeInstanceOf(NotFoundError); + }); + + it('can be thrown and caught by type', () => { + expect(() => { + throw new ToolExecutionError('boom', ToolErrorType.GATEWAY_TIMEOUT); + }).toThrow(ToolExecutionError); + }); +});