From f1a255093ed2d7c7cebe784716d81920be2fd29f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 14 Nov 2025 19:52:36 +0000 Subject: [PATCH 01/18] Initial plan From 5639b46c8c9d482e5418dadaf79ce5e6cd78cb2e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 14 Nov 2025 20:01:25 +0000 Subject: [PATCH 02/18] Extract error codes from CosmosDB to errorHelper.ts Co-authored-by: cleemullins <1165321+cleemullins@users.noreply.github.com> --- .../src/cosmosDbKeyEscape.ts | 6 +- .../src/cosmosDbPartitionedStorage.ts | 111 +++++-- .../src/errorHelper.ts | 274 ++++++++++++++++++ .../src/index.ts | 1 + 4 files changed, 362 insertions(+), 30 deletions(-) create mode 100644 packages/agents-hosting-storage-cosmos/src/errorHelper.ts diff --git a/packages/agents-hosting-storage-cosmos/src/cosmosDbKeyEscape.ts b/packages/agents-hosting-storage-cosmos/src/cosmosDbKeyEscape.ts index ce28e4e4..932aedac 100644 --- a/packages/agents-hosting-storage-cosmos/src/cosmosDbKeyEscape.ts +++ b/packages/agents-hosting-storage-cosmos/src/cosmosDbKeyEscape.ts @@ -1,6 +1,7 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. import { createHash } from 'crypto' +import { ErrorHelper, ExceptionHelper } from './errorHelper' /** * Provides methods for escaping keys for Cosmos DB. @@ -25,7 +26,10 @@ export namespace CosmosDbKeyEscape { */ export function escapeKey (key: string, keySuffix?: string, compatibilityMode?: boolean): string { if (!key) { - throw new Error("The 'key' parameter is required.") + throw ExceptionHelper.generateException( + Error, + ErrorHelper.MissingKeyParameter + ) } const keySplitted: string[] = key.split('') diff --git a/packages/agents-hosting-storage-cosmos/src/cosmosDbPartitionedStorage.ts b/packages/agents-hosting-storage-cosmos/src/cosmosDbPartitionedStorage.ts index 4d71c300..747759d3 100644 --- a/packages/agents-hosting-storage-cosmos/src/cosmosDbPartitionedStorage.ts +++ b/packages/agents-hosting-storage-cosmos/src/cosmosDbPartitionedStorage.ts @@ -6,6 +6,7 @@ import { CosmosDbKeyEscape } from './cosmosDbKeyEscape' import { DocumentStoreItem } from './documentStoreItem' import { CosmosDbPartitionedStorageOptions } from './cosmosDbPartitionedStorageOptions' import { Storage, StoreItems } from '@microsoft/agents-hosting' +import { ErrorHelper, ExceptionHelper, AgentErrorDefinition } from './errorHelper' /** * A utility class to ensure that a specific asynchronous task is executed only once for a given key. @@ -55,30 +56,51 @@ export class CosmosDbPartitionedStorage implements Storage { */ constructor (private readonly cosmosDbStorageOptions: CosmosDbPartitionedStorageOptions) { if (!cosmosDbStorageOptions) { - throw new ReferenceError('CosmosDbPartitionedStorageOptions is required.') + throw ExceptionHelper.generateException( + ReferenceError, + ErrorHelper.MissingCosmosDbStorageOptions + ) } const { cosmosClientOptions } = cosmosDbStorageOptions if (!cosmosClientOptions?.endpoint) { - throw new ReferenceError('endpoint in cosmosClientOptions is required.') + throw ExceptionHelper.generateException( + ReferenceError, + ErrorHelper.MissingCosmosEndpoint + ) } if (!cosmosClientOptions?.key && !cosmosClientOptions?.tokenProvider) { - throw new ReferenceError('key or tokenProvider in cosmosClientOptions is required.') + throw ExceptionHelper.generateException( + ReferenceError, + ErrorHelper.MissingCosmosCredentials + ) } if (!cosmosDbStorageOptions.databaseId) { - throw new ReferenceError('databaseId is for CosmosDB required.') + throw ExceptionHelper.generateException( + ReferenceError, + ErrorHelper.MissingDatabaseId + ) } if (!cosmosDbStorageOptions.containerId) { - throw new ReferenceError('containerId for CosmosDB is required.') + throw ExceptionHelper.generateException( + ReferenceError, + ErrorHelper.MissingContainerId + ) } cosmosDbStorageOptions.compatibilityMode ??= true if (cosmosDbStorageOptions.keySuffix) { if (cosmosDbStorageOptions.compatibilityMode) { - throw new ReferenceError('compatibilityMode cannot be true while using a keySuffix.') + throw ExceptionHelper.generateException( + ReferenceError, + ErrorHelper.InvalidCompatibilityModeWithKeySuffix + ) } const suffixEscaped = CosmosDbKeyEscape.escapeKey(cosmosDbStorageOptions.keySuffix) if (cosmosDbStorageOptions.keySuffix !== suffixEscaped) { - throw new ReferenceError( - `Cannot use invalid Row Key characters: ${cosmosDbStorageOptions.keySuffix} in keySuffix` + throw ExceptionHelper.generateException( + ReferenceError, + ErrorHelper.InvalidKeySuffixCharacters, + undefined, + cosmosDbStorageOptions.keySuffix ) } } @@ -91,7 +113,10 @@ export class CosmosDbPartitionedStorage implements Storage { */ async read (keys: string[]): Promise { if (!keys) { - throw new ReferenceError('Keys are required when reading.') + throw ExceptionHelper.generateException( + ReferenceError, + ErrorHelper.MissingReadKeys + ) } else if (keys.length === 0) { return {} } @@ -119,16 +144,15 @@ export class CosmosDbPartitionedStorage implements Storage { } } catch (err: any) { if (err.code === 404) { - this.throwInformativeError('Not Found', - err) + this.throwInformativeError(ErrorHelper.ContainerReadNotFound.description, + err, ErrorHelper.ContainerReadNotFound.code) } else if (err.code === 400) { this.throwInformativeError( - `Error reading from container. You might be attempting to read from a non-partitioned - container or a container that does not use '/id' as the partitionKeyPath`, - err + ErrorHelper.ContainerReadBadRequest.description, + err, ErrorHelper.ContainerReadBadRequest.code ) } else { - this.throwInformativeError('Error reading from container', err) + this.throwInformativeError(ErrorHelper.ContainerReadError.description, err, ErrorHelper.ContainerReadError.code) } } }) @@ -143,7 +167,10 @@ export class CosmosDbPartitionedStorage implements Storage { */ async write (changes: StoreItems): Promise { if (!changes) { - throw new ReferenceError('Changes are required when writing.') + throw ExceptionHelper.generateException( + ReferenceError, + ErrorHelper.MissingWriteChanges + ) } else if (changes.length === 0) { return } @@ -171,7 +198,7 @@ export class CosmosDbPartitionedStorage implements Storage { await this.container.items.upsert(document, accessCondition) } catch (err: any) { this.checkForNestingError(change, err) - this.throwInformativeError('Error upserting document', err) + this.throwInformativeError(ErrorHelper.DocumentUpsertError.description, err, ErrorHelper.DocumentUpsertError.code) } }) ) @@ -195,9 +222,9 @@ export class CosmosDbPartitionedStorage implements Storage { await this.container.item(escapedKey, this.getPartitionKey(escapedKey)).delete() } catch (err: any) { if (err.code === 404) { - this.throwInformativeError('Not Found', err) + this.throwInformativeError(ErrorHelper.DocumentDeleteNotFound.description, err, ErrorHelper.DocumentDeleteNotFound.code) } else { - this.throwInformativeError('Unable to delete document', err) + this.throwInformativeError(ErrorHelper.DocumentDeleteError.description, err, ErrorHelper.DocumentDeleteError.code) } } }) @@ -239,8 +266,12 @@ export class CosmosDbPartitionedStorage implements Storage { if (paths.includes('/_partitionKey')) { this.compatibilityModePartitionKey = true } else if (paths.indexOf(DocumentStoreItem.partitionKeyPath) === -1) { - throw new Error( - `Custom Partition Key Paths are not supported. ${this.cosmosDbStorageOptions.containerId} has a custom Partition Key Path of ${paths[0]}.` + throw ExceptionHelper.generateException( + Error, + ErrorHelper.UnsupportedCustomPartitionKeyPath, + undefined, + this.cosmosDbStorageOptions.containerId, + paths[0] ) } } else { @@ -264,13 +295,19 @@ export class CosmosDbPartitionedStorage implements Storage { } if (!container) { - throw new Error(`Container ${this.cosmosDbStorageOptions.containerId} not found.`) + throw ExceptionHelper.generateException( + Error, + ErrorHelper.ContainerNotFound, + undefined, + this.cosmosDbStorageOptions.containerId + ) } return container } catch (err: any) { this.throwInformativeError( - `Failed to initialize Cosmos DB database/container: ${this.cosmosDbStorageOptions.databaseId}/${this.cosmosDbStorageOptions.containerId}`, - err + ErrorHelper.InitializationError.description.replace('{0}', this.cosmosDbStorageOptions.databaseId).replace('{1}', this.cosmosDbStorageOptions.containerId), + err, + ErrorHelper.InitializationError.code ) throw err } @@ -283,18 +320,24 @@ export class CosmosDbPartitionedStorage implements Storage { private checkForNestingError (json: object, err: Error | Record<'message', string> | string): void { const checkDepth = (obj: unknown, depth: number, isInDialogState: boolean): void => { if (depth > maxDepthAllowed) { - let message = `Maximum nesting depth of ${maxDepthAllowed} exceeded.` + let additionalMessage = '' if (isInDialogState) { - message += + additionalMessage = ' This is most likely caused by recursive component dialogs. ' + 'Try reworking your dialog code to make sure it does not keep dialogs on the stack ' + "that it's not using. For example, consider using replaceDialog instead of beginDialog." } else { - message += ' Please check your data for signs of unintended recursion.' + additionalMessage = ' Please check your data for signs of unintended recursion.' } - this.throwInformativeError(message, err) + this.throwInformativeError( + ErrorHelper.MaxNestingDepthExceeded.description + .replace('{0}', maxDepthAllowed.toString()) + .replace('{1}', additionalMessage), + err, + ErrorHelper.MaxNestingDepthExceeded.code + ) } else if (obj && typeof obj === 'object') { for (const [key, value] of Object.entries(obj)) { checkDepth(value, depth + 1, key === 'dialogStack' || isInDialogState) @@ -305,13 +348,23 @@ export class CosmosDbPartitionedStorage implements Storage { checkDepth(json, 0, false) } - private throwInformativeError (prependedMessage: string, err: Error | Record<'message', string> | string): void { + private throwInformativeError (prependedMessage: string, err: Error | Record<'message', string> | string, errorCode?: number): void { if (typeof err === 'string') { err = new Error(err) } err.message = `[${prependedMessage}] ${err.message}` + if (errorCode !== undefined) { + (err as any).code = errorCode + // Find the corresponding error definition to get the help link + const errorDefinitions = Object.values(ErrorHelper).filter(val => val instanceof AgentErrorDefinition) as AgentErrorDefinition[] + const errorDef = errorDefinitions.find(def => def.code === errorCode) + if (errorDef) { + (err as any).helpLink = errorDef.helplink + } + } + throw err } } diff --git a/packages/agents-hosting-storage-cosmos/src/errorHelper.ts b/packages/agents-hosting-storage-cosmos/src/errorHelper.ts new file mode 100644 index 00000000..1e3174ed --- /dev/null +++ b/packages/agents-hosting-storage-cosmos/src/errorHelper.ts @@ -0,0 +1,274 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +/** + * Represents an error definition for the Agents SDK. + * Each error definition includes an error code, description, and help link. + */ +export class AgentErrorDefinition { + /** + * Error code for the exception + */ + readonly code: number + + /** + * Displayed error message + */ + readonly description: string + + /** + * Help URL link for the error + */ + readonly helplink: string + + /** + * Creates a new AgentErrorDefinition instance. + * @param code Error code for the exception + * @param description Displayed error message + * @param helplink Help URL link for the error + */ + constructor (code: number, description: string, helplink: string) { + this.code = code + this.description = description + this.helplink = helplink + } +} + +/** + * Error helper for the CosmosDB storage system. + * This is used to define localized error codes for the CosmosDB storage subsystem of the AgentSDK. + * + * Each error should be created as an AgentErrorDefinition and added to the ErrorHelper class. + * Each definition should include an error code (starting from -100000), a description, and a help link + * pointing to an AKA link to get help for the given error. + * + * Usage example: + * ``` + * throw new ReferenceError(ErrorHelper.MissingCosmosDbStorageOptions.description) { + * code: ErrorHelper.MissingCosmosDbStorageOptions.code, + * // Note: In JavaScript, we use a custom property 'code' instead of HResult + * }; + * ``` + */ +export class ErrorHelper { + // Base error code for CosmosDB storage: -100000 + + /** + * Error thrown when CosmosDbPartitionedStorageOptions is not provided. + */ + static readonly MissingCosmosDbStorageOptions = new AgentErrorDefinition( + -100000, + 'CosmosDbPartitionedStorageOptions is required.', + 'https://aka.ms/M365AgentsErrorCodes/#-100000' + ) + + /** + * Error thrown when endpoint in cosmosClientOptions is not provided. + */ + static readonly MissingCosmosEndpoint = new AgentErrorDefinition( + -100001, + 'endpoint in cosmosClientOptions is required.', + 'https://aka.ms/M365AgentsErrorCodes/#-100001' + ) + + /** + * Error thrown when neither key nor tokenProvider is provided in cosmosClientOptions. + */ + static readonly MissingCosmosCredentials = new AgentErrorDefinition( + -100002, + 'key or tokenProvider in cosmosClientOptions is required.', + 'https://aka.ms/M365AgentsErrorCodes/#-100002' + ) + + /** + * Error thrown when databaseId is not provided. + */ + static readonly MissingDatabaseId = new AgentErrorDefinition( + -100003, + 'databaseId is for CosmosDB required.', + 'https://aka.ms/M365AgentsErrorCodes/#-100003' + ) + + /** + * Error thrown when containerId is not provided. + */ + static readonly MissingContainerId = new AgentErrorDefinition( + -100004, + 'containerId for CosmosDB is required.', + 'https://aka.ms/M365AgentsErrorCodes/#-100004' + ) + + /** + * Error thrown when compatibilityMode is enabled with a keySuffix. + */ + static readonly InvalidCompatibilityModeWithKeySuffix = new AgentErrorDefinition( + -100005, + 'compatibilityMode cannot be true while using a keySuffix.', + 'https://aka.ms/M365AgentsErrorCodes/#-100005' + ) + + /** + * Error thrown when keySuffix contains invalid Row Key characters. + */ + static readonly InvalidKeySuffixCharacters = new AgentErrorDefinition( + -100006, + 'Cannot use invalid Row Key characters: {0} in keySuffix', + 'https://aka.ms/M365AgentsErrorCodes/#-100006' + ) + + /** + * Error thrown when keys are not provided for reading. + */ + static readonly MissingReadKeys = new AgentErrorDefinition( + -100007, + 'Keys are required when reading.', + 'https://aka.ms/M365AgentsErrorCodes/#-100007' + ) + + /** + * Error thrown when changes are not provided for writing. + */ + static readonly MissingWriteChanges = new AgentErrorDefinition( + -100008, + 'Changes are required when writing.', + 'https://aka.ms/M365AgentsErrorCodes/#-100008' + ) + + /** + * Error thrown when attempting to use a custom partition key path. + */ + static readonly UnsupportedCustomPartitionKeyPath = new AgentErrorDefinition( + -100009, + 'Custom Partition Key Paths are not supported. {0} has a custom Partition Key Path of {1}.', + 'https://aka.ms/M365AgentsErrorCodes/#-100009' + ) + + /** + * Error thrown when the specified container is not found. + */ + static readonly ContainerNotFound = new AgentErrorDefinition( + -100010, + 'Container {0} not found.', + 'https://aka.ms/M365AgentsErrorCodes/#-100010' + ) + + /** + * Error thrown when the key parameter is missing in CosmosDbKeyEscape. + */ + static readonly MissingKeyParameter = new AgentErrorDefinition( + -100011, + "The 'key' parameter is required.", + 'https://aka.ms/M365AgentsErrorCodes/#-100011' + ) + + /** + * Error thrown when there is an error reading from the container (404 Not Found). + */ + static readonly ContainerReadNotFound = new AgentErrorDefinition( + -100012, + 'Not Found', + 'https://aka.ms/M365AgentsErrorCodes/#-100012' + ) + + /** + * Error thrown when there is an error reading from container (400 Bad Request). + */ + static readonly ContainerReadBadRequest = new AgentErrorDefinition( + -100013, + 'Error reading from container. You might be attempting to read from a non-partitioned container or a container that does not use \'/id\' as the partitionKeyPath', + 'https://aka.ms/M365AgentsErrorCodes/#-100013' + ) + + /** + * Error thrown when there is a general error reading from the container. + */ + static readonly ContainerReadError = new AgentErrorDefinition( + -100014, + 'Error reading from container', + 'https://aka.ms/M365AgentsErrorCodes/#-100014' + ) + + /** + * Error thrown when there is an error upserting a document. + */ + static readonly DocumentUpsertError = new AgentErrorDefinition( + -100015, + 'Error upserting document', + 'https://aka.ms/M365AgentsErrorCodes/#-100015' + ) + + /** + * Error thrown when there is an error deleting a document (404 Not Found). + */ + static readonly DocumentDeleteNotFound = new AgentErrorDefinition( + -100016, + 'Not Found', + 'https://aka.ms/M365AgentsErrorCodes/#-100016' + ) + + /** + * Error thrown when unable to delete a document. + */ + static readonly DocumentDeleteError = new AgentErrorDefinition( + -100017, + 'Unable to delete document', + 'https://aka.ms/M365AgentsErrorCodes/#-100017' + ) + + /** + * Error thrown when failing to initialize CosmosDB database/container. + */ + static readonly InitializationError = new AgentErrorDefinition( + -100018, + 'Failed to initialize Cosmos DB database/container: {0}/{1}', + 'https://aka.ms/M365AgentsErrorCodes/#-100018' + ) + + /** + * Error thrown when maximum nesting depth is exceeded. + */ + static readonly MaxNestingDepthExceeded = new AgentErrorDefinition( + -100019, + 'Maximum nesting depth of {0} exceeded. {1}', + 'https://aka.ms/M365AgentsErrorCodes/#-100019' + ) +} + +/** + * Helper class for generating exceptions with error codes. + */ +export class ExceptionHelper { + /** + * Generates a typed exception with error code and help link. + * @param ErrorType The constructor of the error type to create + * @param errorDefinition The error definition containing code, description, and help link + * @param innerException Optional inner exception + * @param messageFormat Optional format parameters for the error message + * @returns A new exception instance with error code and help link + */ + static generateException( + ErrorType: new (message: string, innerException?: Error) => T, + errorDefinition: AgentErrorDefinition, + innerException?: Error, + ...messageFormat: string[] + ): T { + // Format the message with parameters if provided + let message = errorDefinition.description + if (messageFormat && messageFormat.length > 0) { + messageFormat.forEach((param, index) => { + message = message.replace(`{${index}}`, param) + }) + } + + // Create the exception + const exception = innerException + ? new ErrorType(message, innerException) + : new ErrorType(message); + + // Set error code and help link as custom properties + (exception as any).code = errorDefinition.code; + (exception as any).helpLink = errorDefinition.helplink + + return exception + } +} diff --git a/packages/agents-hosting-storage-cosmos/src/index.ts b/packages/agents-hosting-storage-cosmos/src/index.ts index 525f0720..aef6895e 100644 --- a/packages/agents-hosting-storage-cosmos/src/index.ts +++ b/packages/agents-hosting-storage-cosmos/src/index.ts @@ -3,3 +3,4 @@ export { CosmosDbPartitionedStorage } from './cosmosDbPartitionedStorage' export * from './cosmosDbPartitionedStorageOptions' +export * from './errorHelper' From 1f89bf7f67b05fb88e5b205c1e8b3ac5e2476376 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 14 Nov 2025 20:51:33 +0000 Subject: [PATCH 03/18] Add unit tests for errorHelper Co-authored-by: cleemullins <1165321+cleemullins@users.noreply.github.com> --- .../test/errorHelper.test.ts | 275 ++++++++++++++++++ .../tsconfig.json | 5 +- 2 files changed, 278 insertions(+), 2 deletions(-) create mode 100644 packages/agents-hosting-storage-cosmos/test/errorHelper.test.ts diff --git a/packages/agents-hosting-storage-cosmos/test/errorHelper.test.ts b/packages/agents-hosting-storage-cosmos/test/errorHelper.test.ts new file mode 100644 index 00000000..8dad900d --- /dev/null +++ b/packages/agents-hosting-storage-cosmos/test/errorHelper.test.ts @@ -0,0 +1,275 @@ +import assert from 'assert' +import { describe, it } from 'node:test' +import { AgentErrorDefinition, ErrorHelper, ExceptionHelper } from '../src/errorHelper' + +describe('AgentErrorDefinition tests', () => { + it('should create an error definition with all properties', () => { + const errorDef = new AgentErrorDefinition(-100000, 'Test error message', 'https://aka.ms/test') + + assert.strictEqual(errorDef.code, -100000) + assert.strictEqual(errorDef.description, 'Test error message') + assert.strictEqual(errorDef.helplink, 'https://aka.ms/test') + }) + + it('should have readonly properties', () => { + const errorDef = new AgentErrorDefinition(-100001, 'Test error', 'https://aka.ms/test') + + // TypeScript enforces readonly at compile time, but we can verify the properties exist + assert.ok(Object.prototype.hasOwnProperty.call(errorDef, 'code')) + assert.ok(Object.prototype.hasOwnProperty.call(errorDef, 'description')) + assert.ok(Object.prototype.hasOwnProperty.call(errorDef, 'helplink')) + }) +}) + +describe('ErrorHelper tests', () => { + it('should have MissingCosmosDbStorageOptions error definition', () => { + const error = ErrorHelper.MissingCosmosDbStorageOptions + + assert.strictEqual(error.code, -100000) + assert.strictEqual(error.description, 'CosmosDbPartitionedStorageOptions is required.') + assert.strictEqual(error.helplink, 'https://aka.ms/M365AgentsErrorCodes/#-100000') + }) + + it('should have MissingCosmosEndpoint error definition', () => { + const error = ErrorHelper.MissingCosmosEndpoint + + assert.strictEqual(error.code, -100001) + assert.strictEqual(error.description, 'endpoint in cosmosClientOptions is required.') + assert.strictEqual(error.helplink, 'https://aka.ms/M365AgentsErrorCodes/#-100001') + }) + + it('should have MissingCosmosCredentials error definition', () => { + const error = ErrorHelper.MissingCosmosCredentials + + assert.strictEqual(error.code, -100002) + assert.strictEqual(error.description, 'key or tokenProvider in cosmosClientOptions is required.') + assert.strictEqual(error.helplink, 'https://aka.ms/M365AgentsErrorCodes/#-100002') + }) + + it('should have all error codes in the correct range', () => { + const errorDefinitions = Object.values(ErrorHelper).filter( + val => val instanceof AgentErrorDefinition + ) as AgentErrorDefinition[] + + // All error codes should be negative and in the range -100000 to -100019 + errorDefinitions.forEach(errorDef => { + assert.ok(errorDef.code < 0, `Error code ${errorDef.code} should be negative`) + assert.ok(errorDef.code >= -100019, `Error code ${errorDef.code} should be >= -100019`) + assert.ok(errorDef.code <= -100000, `Error code ${errorDef.code} should be <= -100000`) + }) + }) + + it('should have unique error codes', () => { + const errorDefinitions = Object.values(ErrorHelper).filter( + val => val instanceof AgentErrorDefinition + ) as AgentErrorDefinition[] + + const codes = errorDefinitions.map(e => e.code) + const uniqueCodes = new Set(codes) + + assert.strictEqual(codes.length, uniqueCodes.size, 'All error codes should be unique') + }) + + it('should have help links with correct format', () => { + const errorDefinitions = Object.values(ErrorHelper).filter( + val => val instanceof AgentErrorDefinition + ) as AgentErrorDefinition[] + + errorDefinitions.forEach(errorDef => { + assert.ok( + errorDef.helplink.startsWith('https://aka.ms/M365AgentsErrorCodes/#'), + `Help link should start with correct URL: ${errorDef.helplink}` + ) + assert.ok( + errorDef.helplink.endsWith(errorDef.code.toString()), + `Help link should end with error code: ${errorDef.helplink}` + ) + }) + }) + + it('should have non-empty descriptions', () => { + const errorDefinitions = Object.values(ErrorHelper).filter( + val => val instanceof AgentErrorDefinition + ) as AgentErrorDefinition[] + + errorDefinitions.forEach(errorDef => { + assert.ok(errorDef.description.length > 0, 'Description should not be empty') + }) + }) +}) + +describe('ExceptionHelper tests', () => { + it('should generate exception with error code and help link', () => { + const errorDef = new AgentErrorDefinition(-100000, 'Test error', 'https://aka.ms/test') + + const exception = ExceptionHelper.generateException(Error, errorDef) + + assert.strictEqual(exception.message, 'Test error') + assert.strictEqual((exception as any).code, -100000) + assert.strictEqual((exception as any).helpLink, 'https://aka.ms/test') + }) + + it('should generate ReferenceError exception', () => { + const exception = ExceptionHelper.generateException( + ReferenceError, + ErrorHelper.MissingCosmosDbStorageOptions + ) + + assert.ok(exception instanceof ReferenceError) + assert.strictEqual(exception.message, 'CosmosDbPartitionedStorageOptions is required.') + assert.strictEqual((exception as any).code, -100000) + assert.strictEqual((exception as any).helpLink, 'https://aka.ms/M365AgentsErrorCodes/#-100000') + }) + + it('should generate exception with inner exception', () => { + const innerError = new Error('Inner error') + const errorDef = new AgentErrorDefinition(-100000, 'Test error', 'https://aka.ms/test') + + const exception = ExceptionHelper.generateException(Error, errorDef, innerError) + + assert.strictEqual(exception.message, 'Test error') + assert.strictEqual((exception as any).code, -100000) + }) + + it('should format message with single parameter', () => { + const errorDef = new AgentErrorDefinition( + -100006, + 'Cannot use invalid Row Key characters: {0} in keySuffix', + 'https://aka.ms/test' + ) + + const exception = ExceptionHelper.generateException(Error, errorDef, undefined, 'test*suffix') + + assert.strictEqual( + exception.message, + 'Cannot use invalid Row Key characters: test*suffix in keySuffix' + ) + }) + + it('should format message with multiple parameters', () => { + const errorDef = new AgentErrorDefinition( + -100009, + 'Custom Partition Key Paths are not supported. {0} has a custom Partition Key Path of {1}.', + 'https://aka.ms/test' + ) + + const exception = ExceptionHelper.generateException( + Error, + errorDef, + undefined, + 'myContainer', + '/customPath' + ) + + assert.strictEqual( + exception.message, + 'Custom Partition Key Paths are not supported. myContainer has a custom Partition Key Path of /customPath.' + ) + }) + + it('should handle message with no parameters', () => { + const errorDef = new AgentErrorDefinition(-100000, 'Simple error message', 'https://aka.ms/test') + + const exception = ExceptionHelper.generateException(Error, errorDef) + + assert.strictEqual(exception.message, 'Simple error message') + }) + + it('should throw and catch exception correctly', () => { + assert.throws( + () => { + throw ExceptionHelper.generateException( + ReferenceError, + ErrorHelper.MissingCosmosDbStorageOptions + ) + }, + (err: any) => { + return ( + err instanceof ReferenceError && + err.message === 'CosmosDbPartitionedStorageOptions is required.' && + err.code === -100000 && + err.helpLink === 'https://aka.ms/M365AgentsErrorCodes/#-100000' + ) + } + ) + }) +}) + +describe('ErrorHelper integration tests', () => { + it('should work with InvalidKeySuffixCharacters error', () => { + const exception = ExceptionHelper.generateException( + ReferenceError, + ErrorHelper.InvalidKeySuffixCharacters, + undefined, + 'invalid*suffix' + ) + + assert.ok(exception instanceof ReferenceError) + assert.strictEqual( + exception.message, + 'Cannot use invalid Row Key characters: invalid*suffix in keySuffix' + ) + assert.strictEqual((exception as any).code, -100006) + }) + + it('should work with UnsupportedCustomPartitionKeyPath error', () => { + const exception = ExceptionHelper.generateException( + Error, + ErrorHelper.UnsupportedCustomPartitionKeyPath, + undefined, + 'myContainer', + '/custom' + ) + + assert.ok(exception instanceof Error) + assert.strictEqual( + exception.message, + 'Custom Partition Key Paths are not supported. myContainer has a custom Partition Key Path of /custom.' + ) + assert.strictEqual((exception as any).code, -100009) + }) + + it('should work with ContainerNotFound error', () => { + const exception = ExceptionHelper.generateException( + Error, + ErrorHelper.ContainerNotFound, + undefined, + 'myContainer' + ) + + assert.strictEqual(exception.message, 'Container myContainer not found.') + assert.strictEqual((exception as any).code, -100010) + }) + + it('should work with InitializationError', () => { + const exception = ExceptionHelper.generateException( + Error, + ErrorHelper.InitializationError, + undefined, + 'myDatabase', + 'myContainer' + ) + + assert.strictEqual( + exception.message, + 'Failed to initialize Cosmos DB database/container: myDatabase/myContainer' + ) + assert.strictEqual((exception as any).code, -100018) + }) + + it('should work with MaxNestingDepthExceeded error', () => { + const exception = ExceptionHelper.generateException( + Error, + ErrorHelper.MaxNestingDepthExceeded, + undefined, + '127', + 'Additional context message' + ) + + assert.strictEqual( + exception.message, + 'Maximum nesting depth of 127 exceeded. Additional context message' + ) + assert.strictEqual((exception as any).code, -100019) + }) +}) diff --git a/packages/agents-hosting-storage-cosmos/tsconfig.json b/packages/agents-hosting-storage-cosmos/tsconfig.json index 8208479a..1b4ef51e 100644 --- a/packages/agents-hosting-storage-cosmos/tsconfig.json +++ b/packages/agents-hosting-storage-cosmos/tsconfig.json @@ -1,10 +1,11 @@ { "extends": "../../tsconfig.json", "compilerOptions": { - "rootDirs": [ "src/", "test/"], + "rootDirs": [ "src/"], "outDir": "dist", "paths": { "@microsoft/*": ["./packages/*"] } - } + }, + "exclude": ["test/**/*"] } From 081fcf0f04f06c6e90ae59d68f4f1071bf849a3b Mon Sep 17 00:00:00 2001 From: Chris Mullins Date: Fri, 14 Nov 2025 14:48:57 -0800 Subject: [PATCH 04/18] Update packages/agents-hosting-storage-cosmos/src/errorHelper.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- packages/agents-hosting-storage-cosmos/src/errorHelper.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/agents-hosting-storage-cosmos/src/errorHelper.ts b/packages/agents-hosting-storage-cosmos/src/errorHelper.ts index 1e3174ed..e78b0b2a 100644 --- a/packages/agents-hosting-storage-cosmos/src/errorHelper.ts +++ b/packages/agents-hosting-storage-cosmos/src/errorHelper.ts @@ -85,7 +85,7 @@ export class ErrorHelper { */ static readonly MissingDatabaseId = new AgentErrorDefinition( -100003, - 'databaseId is for CosmosDB required.', + 'databaseId for CosmosDB is required.', 'https://aka.ms/M365AgentsErrorCodes/#-100003' ) From ff451e4775044677d3d42860bf69eb11ef4aea5f Mon Sep 17 00:00:00 2001 From: Chris Mullins Date: Fri, 14 Nov 2025 14:50:01 -0800 Subject: [PATCH 05/18] Update packages/agents-hosting-storage-cosmos/src/cosmosDbPartitionedStorage.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../src/cosmosDbPartitionedStorage.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/agents-hosting-storage-cosmos/src/cosmosDbPartitionedStorage.ts b/packages/agents-hosting-storage-cosmos/src/cosmosDbPartitionedStorage.ts index 747759d3..6722ee3c 100644 --- a/packages/agents-hosting-storage-cosmos/src/cosmosDbPartitionedStorage.ts +++ b/packages/agents-hosting-storage-cosmos/src/cosmosDbPartitionedStorage.ts @@ -331,12 +331,12 @@ export class CosmosDbPartitionedStorage implements Storage { additionalMessage = ' Please check your data for signs of unintended recursion.' } - this.throwInformativeError( - ErrorHelper.MaxNestingDepthExceeded.description - .replace('{0}', maxDepthAllowed.toString()) - .replace('{1}', additionalMessage), + throw ExceptionHelper.generateException( + Error, + ErrorHelper.MaxNestingDepthExceeded, err, - ErrorHelper.MaxNestingDepthExceeded.code + maxDepthAllowed.toString(), + additionalMessage ) } else if (obj && typeof obj === 'object') { for (const [key, value] of Object.entries(obj)) { From 21ce4f60557ef95abed60fe02c82eaabddf4db1a Mon Sep 17 00:00:00 2001 From: Chris Mullins Date: Fri, 14 Nov 2025 14:50:59 -0800 Subject: [PATCH 06/18] Update packages/agents-hosting-storage-cosmos/src/cosmosDbPartitionedStorage.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../src/cosmosDbPartitionedStorage.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/agents-hosting-storage-cosmos/src/cosmosDbPartitionedStorage.ts b/packages/agents-hosting-storage-cosmos/src/cosmosDbPartitionedStorage.ts index 6722ee3c..9f983c48 100644 --- a/packages/agents-hosting-storage-cosmos/src/cosmosDbPartitionedStorage.ts +++ b/packages/agents-hosting-storage-cosmos/src/cosmosDbPartitionedStorage.ts @@ -144,8 +144,11 @@ export class CosmosDbPartitionedStorage implements Storage { } } catch (err: any) { if (err.code === 404) { - this.throwInformativeError(ErrorHelper.ContainerReadNotFound.description, - err, ErrorHelper.ContainerReadNotFound.code) + throw ExceptionHelper.generateException( + Error, + ErrorHelper.ContainerReadNotFound, + err + ) } else if (err.code === 400) { this.throwInformativeError( ErrorHelper.ContainerReadBadRequest.description, From 6b70c5babab3b119100fae2b63609b2d80b7db5c Mon Sep 17 00:00:00 2001 From: Chris Mullins Date: Fri, 14 Nov 2025 14:51:27 -0800 Subject: [PATCH 07/18] Update packages/agents-hosting-storage-cosmos/src/errorHelper.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- packages/agents-hosting-storage-cosmos/src/errorHelper.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/agents-hosting-storage-cosmos/src/errorHelper.ts b/packages/agents-hosting-storage-cosmos/src/errorHelper.ts index e78b0b2a..6bc37431 100644 --- a/packages/agents-hosting-storage-cosmos/src/errorHelper.ts +++ b/packages/agents-hosting-storage-cosmos/src/errorHelper.ts @@ -254,7 +254,7 @@ export class ExceptionHelper { ): T { // Format the message with parameters if provided let message = errorDefinition.description - if (messageFormat && messageFormat.length > 0) { + if (messageFormat.length > 0) { messageFormat.forEach((param, index) => { message = message.replace(`{${index}}`, param) }) From dece2bb0351f808e6b14d0c70297d57651c95f6b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 14 Nov 2025 23:00:38 +0000 Subject: [PATCH 08/18] Refactor error handling to use ExceptionHelper consistently and fix inner exception handling Co-authored-by: cleemullins <1165321+cleemullins@users.noreply.github.com> --- .../src/cosmosDbPartitionedStorage.ts | 69 +++++++++---------- .../src/errorHelper.ts | 14 ++-- 2 files changed, 42 insertions(+), 41 deletions(-) diff --git a/packages/agents-hosting-storage-cosmos/src/cosmosDbPartitionedStorage.ts b/packages/agents-hosting-storage-cosmos/src/cosmosDbPartitionedStorage.ts index 9f983c48..0f65f807 100644 --- a/packages/agents-hosting-storage-cosmos/src/cosmosDbPartitionedStorage.ts +++ b/packages/agents-hosting-storage-cosmos/src/cosmosDbPartitionedStorage.ts @@ -6,7 +6,7 @@ import { CosmosDbKeyEscape } from './cosmosDbKeyEscape' import { DocumentStoreItem } from './documentStoreItem' import { CosmosDbPartitionedStorageOptions } from './cosmosDbPartitionedStorageOptions' import { Storage, StoreItems } from '@microsoft/agents-hosting' -import { ErrorHelper, ExceptionHelper, AgentErrorDefinition } from './errorHelper' +import { ErrorHelper, ExceptionHelper } from './errorHelper' /** * A utility class to ensure that a specific asynchronous task is executed only once for a given key. @@ -144,18 +144,19 @@ export class CosmosDbPartitionedStorage implements Storage { } } catch (err: any) { if (err.code === 404) { + // Not Found is not an error during read operations, just skip + } else if (err.code === 400) { throw ExceptionHelper.generateException( Error, - ErrorHelper.ContainerReadNotFound, + ErrorHelper.ContainerReadBadRequest, err ) - } else if (err.code === 400) { - this.throwInformativeError( - ErrorHelper.ContainerReadBadRequest.description, - err, ErrorHelper.ContainerReadBadRequest.code - ) } else { - this.throwInformativeError(ErrorHelper.ContainerReadError.description, err, ErrorHelper.ContainerReadError.code) + throw ExceptionHelper.generateException( + Error, + ErrorHelper.ContainerReadError, + err + ) } } }) @@ -201,7 +202,11 @@ export class CosmosDbPartitionedStorage implements Storage { await this.container.items.upsert(document, accessCondition) } catch (err: any) { this.checkForNestingError(change, err) - this.throwInformativeError(ErrorHelper.DocumentUpsertError.description, err, ErrorHelper.DocumentUpsertError.code) + throw ExceptionHelper.generateException( + Error, + ErrorHelper.DocumentUpsertError, + err + ) } }) ) @@ -225,9 +230,13 @@ export class CosmosDbPartitionedStorage implements Storage { await this.container.item(escapedKey, this.getPartitionKey(escapedKey)).delete() } catch (err: any) { if (err.code === 404) { - this.throwInformativeError(ErrorHelper.DocumentDeleteNotFound.description, err, ErrorHelper.DocumentDeleteNotFound.code) + // Not Found is not an error during delete operations, just skip } else { - this.throwInformativeError(ErrorHelper.DocumentDeleteError.description, err, ErrorHelper.DocumentDeleteError.code) + throw ExceptionHelper.generateException( + Error, + ErrorHelper.DocumentDeleteError, + err + ) } } }) @@ -307,12 +316,13 @@ export class CosmosDbPartitionedStorage implements Storage { } return container } catch (err: any) { - this.throwInformativeError( - ErrorHelper.InitializationError.description.replace('{0}', this.cosmosDbStorageOptions.databaseId).replace('{1}', this.cosmosDbStorageOptions.containerId), + throw ExceptionHelper.generateException( + Error, + ErrorHelper.InitializationError, err, - ErrorHelper.InitializationError.code + this.cosmosDbStorageOptions.databaseId, + this.cosmosDbStorageOptions.containerId ) - throw err } } @@ -334,10 +344,17 @@ export class CosmosDbPartitionedStorage implements Storage { additionalMessage = ' Please check your data for signs of unintended recursion.' } + // Convert err to Error if needed + const errorObj = typeof err === 'string' + ? new Error(err) + : err instanceof Error + ? err + : new Error(err.message) + throw ExceptionHelper.generateException( Error, ErrorHelper.MaxNestingDepthExceeded, - err, + errorObj, maxDepthAllowed.toString(), additionalMessage ) @@ -350,24 +367,4 @@ export class CosmosDbPartitionedStorage implements Storage { checkDepth(json, 0, false) } - - private throwInformativeError (prependedMessage: string, err: Error | Record<'message', string> | string, errorCode?: number): void { - if (typeof err === 'string') { - err = new Error(err) - } - - err.message = `[${prependedMessage}] ${err.message}` - - if (errorCode !== undefined) { - (err as any).code = errorCode - // Find the corresponding error definition to get the help link - const errorDefinitions = Object.values(ErrorHelper).filter(val => val instanceof AgentErrorDefinition) as AgentErrorDefinition[] - const errorDef = errorDefinitions.find(def => def.code === errorCode) - if (errorDef) { - (err as any).helpLink = errorDef.helplink - } - } - - throw err - } } diff --git a/packages/agents-hosting-storage-cosmos/src/errorHelper.ts b/packages/agents-hosting-storage-cosmos/src/errorHelper.ts index 6bc37431..a0eb74ab 100644 --- a/packages/agents-hosting-storage-cosmos/src/errorHelper.ts +++ b/packages/agents-hosting-storage-cosmos/src/errorHelper.ts @@ -261,13 +261,17 @@ export class ExceptionHelper { } // Create the exception - const exception = innerException - ? new ErrorType(message, innerException) - : new ErrorType(message); + const exception = new ErrorType(message) // Set error code and help link as custom properties - (exception as any).code = errorDefinition.code; - (exception as any).helpLink = errorDefinition.helplink + const exceptionWithProps = exception as any + exceptionWithProps.code = errorDefinition.code + exceptionWithProps.helpLink = errorDefinition.helplink + + // Store inner exception as a custom property if provided + if (innerException) { + exceptionWithProps.innerException = innerException + } return exception } From f02fde13efd8bc914e8415715f1db612059a5570 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 18 Nov 2025 22:10:13 +0000 Subject: [PATCH 09/18] Move ExceptionHelper to agents-activity package and refactor AgentErrorDefinition to interface Co-authored-by: cleemullins <1165321+cleemullins@users.noreply.github.com> --- .../agents-activity/src/exceptionHelper.ts | 66 +++++ packages/agents-activity/src/index.ts | 2 + .../test/exceptionHelper.test.ts | 145 +++++++++ packages/agents-activity/tsconfig.json | 5 +- .../src/cosmosDbKeyEscape.ts | 3 +- .../src/cosmosDbPartitionedStorage.ts | 3 +- .../src/errorHelper.ts | 274 +++++++----------- .../src/index.ts | 3 +- .../test/errorHelper.test.ts | 206 +------------ 9 files changed, 328 insertions(+), 379 deletions(-) create mode 100644 packages/agents-activity/src/exceptionHelper.ts create mode 100644 packages/agents-activity/test/exceptionHelper.test.ts diff --git a/packages/agents-activity/src/exceptionHelper.ts b/packages/agents-activity/src/exceptionHelper.ts new file mode 100644 index 00000000..8593d008 --- /dev/null +++ b/packages/agents-activity/src/exceptionHelper.ts @@ -0,0 +1,66 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +/** + * Represents an error definition for the Agents SDK. + * Each error definition includes an error code, description, and help link. + */ +export interface AgentErrorDefinition { + /** + * Error code for the exception + */ + code: number + + /** + * Displayed error message + */ + description: string + + /** + * Help URL link for the error + */ + helplink: string +} + +/** + * Helper class for generating exceptions with error codes. + */ +export class ExceptionHelper { + /** + * Generates a typed exception with error code and help link. + * @param ErrorType The constructor of the error type to create + * @param errorDefinition The error definition containing code, description, and help link + * @param innerException Optional inner exception + * @param messageFormat Optional format parameters for the error message + * @returns A new exception instance with error code and help link + */ + static generateException ( + ErrorType: new (message: string, innerException?: Error) => T, + errorDefinition: AgentErrorDefinition, + innerException?: Error, + ...messageFormat: string[] + ): T { + // Format the message with parameters if provided + let message = errorDefinition.description + if (messageFormat.length > 0) { + messageFormat.forEach((param, index) => { + message = message.replace(`{${index}}`, param) + }) + } + + // Create the exception + const exception = new ErrorType(message) + + // Set error code and help link as custom properties + const exceptionWithProps = exception as any + exceptionWithProps.code = errorDefinition.code + exceptionWithProps.helpLink = errorDefinition.helplink + + // Store inner exception as a custom property if provided + if (innerException) { + exceptionWithProps.innerException = innerException + } + + return exception + } +} diff --git a/packages/agents-activity/src/index.ts b/packages/agents-activity/src/index.ts index 8ccf5dbe..a6cead00 100644 --- a/packages/agents-activity/src/index.ts +++ b/packages/agents-activity/src/index.ts @@ -48,3 +48,5 @@ export { TextFormatTypes } from './textFormatTypes' export { TextHighlight } from './textHighlight' export { ActivityTreatments } from './activityTreatments' export { debug, Logger } from './logger' + +export { AgentErrorDefinition, ExceptionHelper } from './exceptionHelper' diff --git a/packages/agents-activity/test/exceptionHelper.test.ts b/packages/agents-activity/test/exceptionHelper.test.ts new file mode 100644 index 00000000..533c473d --- /dev/null +++ b/packages/agents-activity/test/exceptionHelper.test.ts @@ -0,0 +1,145 @@ +import assert from 'assert' +import { describe, it } from 'node:test' +import { AgentErrorDefinition, ExceptionHelper } from '../src/exceptionHelper' + +describe('AgentErrorDefinition tests', () => { + it('should create an error definition with all properties', () => { + const errorDef: AgentErrorDefinition = { + code: -100000, + description: 'Test error message', + helplink: 'https://aka.ms/test' + } + + assert.strictEqual(errorDef.code, -100000) + assert.strictEqual(errorDef.description, 'Test error message') + assert.strictEqual(errorDef.helplink, 'https://aka.ms/test') + }) + + it('should have all required properties', () => { + const errorDef: AgentErrorDefinition = { + code: -100001, + description: 'Test error', + helplink: 'https://aka.ms/test' + } + + assert.ok(Object.prototype.hasOwnProperty.call(errorDef, 'code')) + assert.ok(Object.prototype.hasOwnProperty.call(errorDef, 'description')) + assert.ok(Object.prototype.hasOwnProperty.call(errorDef, 'helplink')) + }) +}) + +describe('ExceptionHelper tests', () => { + it('should generate exception with error code and help link', () => { + const errorDef: AgentErrorDefinition = { + code: -100000, + description: 'Test error', + helplink: 'https://aka.ms/test' + } + + const exception = ExceptionHelper.generateException(Error, errorDef) + + assert.strictEqual(exception.message, 'Test error') + assert.strictEqual((exception as any).code, -100000) + assert.strictEqual((exception as any).helpLink, 'https://aka.ms/test') + }) + + it('should generate ReferenceError exception', () => { + const errorDef: AgentErrorDefinition = { + code: -100000, + description: 'Reference error test', + helplink: 'https://aka.ms/test' + } + + const exception = ExceptionHelper.generateException(ReferenceError, errorDef) + + assert.ok(exception instanceof ReferenceError) + assert.strictEqual(exception.message, 'Reference error test') + assert.strictEqual((exception as any).code, -100000) + assert.strictEqual((exception as any).helpLink, 'https://aka.ms/test') + }) + + it('should generate exception with inner exception', () => { + const innerError = new Error('Inner error') + const errorDef: AgentErrorDefinition = { + code: -100000, + description: 'Test error', + helplink: 'https://aka.ms/test' + } + + const exception = ExceptionHelper.generateException(Error, errorDef, innerError) + + assert.strictEqual(exception.message, 'Test error') + assert.strictEqual((exception as any).code, -100000) + assert.strictEqual((exception as any).innerException, innerError) + }) + + it('should format message with single parameter', () => { + const errorDef: AgentErrorDefinition = { + code: -100006, + description: 'Cannot use invalid Row Key characters: {0} in keySuffix', + helplink: 'https://aka.ms/test' + } + + const exception = ExceptionHelper.generateException(Error, errorDef, undefined, 'test*suffix') + + assert.strictEqual( + exception.message, + 'Cannot use invalid Row Key characters: test*suffix in keySuffix' + ) + }) + + it('should format message with multiple parameters', () => { + const errorDef: AgentErrorDefinition = { + code: -100009, + description: 'Custom Partition Key Paths are not supported. {0} has a custom Partition Key Path of {1}.', + helplink: 'https://aka.ms/test' + } + + const exception = ExceptionHelper.generateException( + Error, + errorDef, + undefined, + 'myContainer', + '/customPath' + ) + + assert.strictEqual( + exception.message, + 'Custom Partition Key Paths are not supported. myContainer has a custom Partition Key Path of /customPath.' + ) + }) + + it('should handle message with no parameters', () => { + const errorDef: AgentErrorDefinition = { + code: -100000, + description: 'Simple error message', + helplink: 'https://aka.ms/test' + } + + const exception = ExceptionHelper.generateException(Error, errorDef) + + assert.strictEqual(exception.message, 'Simple error message') + }) + + it('should throw and catch exception correctly', () => { + const errorDef: AgentErrorDefinition = { + code: -100000, + description: 'Test error', + helplink: 'https://aka.ms/test' + } + + assert.throws( + () => { + throw ExceptionHelper.generateException(ReferenceError, errorDef) + }, + (err: any) => { + return ( + err instanceof ReferenceError && + err.message === 'Test error' && + err.code === -100000 && + err.helpLink === 'https://aka.ms/test' + ) + } + ) + }) +}) diff --git a/packages/agents-activity/tsconfig.json b/packages/agents-activity/tsconfig.json index 8208479a..1b4ef51e 100644 --- a/packages/agents-activity/tsconfig.json +++ b/packages/agents-activity/tsconfig.json @@ -1,10 +1,11 @@ { "extends": "../../tsconfig.json", "compilerOptions": { - "rootDirs": [ "src/", "test/"], + "rootDirs": [ "src/"], "outDir": "dist", "paths": { "@microsoft/*": ["./packages/*"] } - } + }, + "exclude": ["test/**/*"] } diff --git a/packages/agents-hosting-storage-cosmos/src/cosmosDbKeyEscape.ts b/packages/agents-hosting-storage-cosmos/src/cosmosDbKeyEscape.ts index 932aedac..e5a1bad3 100644 --- a/packages/agents-hosting-storage-cosmos/src/cosmosDbKeyEscape.ts +++ b/packages/agents-hosting-storage-cosmos/src/cosmosDbKeyEscape.ts @@ -1,7 +1,8 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. import { createHash } from 'crypto' -import { ErrorHelper, ExceptionHelper } from './errorHelper' +import { ExceptionHelper } from '@microsoft/agents-activity' +import { ErrorHelper } from './errorHelper' /** * Provides methods for escaping keys for Cosmos DB. diff --git a/packages/agents-hosting-storage-cosmos/src/cosmosDbPartitionedStorage.ts b/packages/agents-hosting-storage-cosmos/src/cosmosDbPartitionedStorage.ts index 0f65f807..bc8e5f94 100644 --- a/packages/agents-hosting-storage-cosmos/src/cosmosDbPartitionedStorage.ts +++ b/packages/agents-hosting-storage-cosmos/src/cosmosDbPartitionedStorage.ts @@ -6,7 +6,8 @@ import { CosmosDbKeyEscape } from './cosmosDbKeyEscape' import { DocumentStoreItem } from './documentStoreItem' import { CosmosDbPartitionedStorageOptions } from './cosmosDbPartitionedStorageOptions' import { Storage, StoreItems } from '@microsoft/agents-hosting' -import { ErrorHelper, ExceptionHelper } from './errorHelper' +import { ExceptionHelper } from '@microsoft/agents-activity' +import { ErrorHelper } from './errorHelper' /** * A utility class to ensure that a specific asynchronous task is executed only once for a given key. diff --git a/packages/agents-hosting-storage-cosmos/src/errorHelper.ts b/packages/agents-hosting-storage-cosmos/src/errorHelper.ts index a0eb74ab..8766560a 100644 --- a/packages/agents-hosting-storage-cosmos/src/errorHelper.ts +++ b/packages/agents-hosting-storage-cosmos/src/errorHelper.ts @@ -1,38 +1,7 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -/** - * Represents an error definition for the Agents SDK. - * Each error definition includes an error code, description, and help link. - */ -export class AgentErrorDefinition { - /** - * Error code for the exception - */ - readonly code: number - - /** - * Displayed error message - */ - readonly description: string - - /** - * Help URL link for the error - */ - readonly helplink: string - - /** - * Creates a new AgentErrorDefinition instance. - * @param code Error code for the exception - * @param description Displayed error message - * @param helplink Help URL link for the error - */ - constructor (code: number, description: string, helplink: string) { - this.code = code - this.description = description - this.helplink = helplink - } -} +import { AgentErrorDefinition } from '@microsoft/agents-activity' /** * Error helper for the CosmosDB storage system. @@ -56,223 +25,180 @@ export class ErrorHelper { /** * Error thrown when CosmosDbPartitionedStorageOptions is not provided. */ - static readonly MissingCosmosDbStorageOptions = new AgentErrorDefinition( - -100000, - 'CosmosDbPartitionedStorageOptions is required.', - 'https://aka.ms/M365AgentsErrorCodes/#-100000' - ) + static readonly MissingCosmosDbStorageOptions: AgentErrorDefinition = { + code: -100000, + description: 'CosmosDbPartitionedStorageOptions is required.', + helplink: 'https://aka.ms/M365AgentsErrorCodes/#-100000' + } /** * Error thrown when endpoint in cosmosClientOptions is not provided. */ - static readonly MissingCosmosEndpoint = new AgentErrorDefinition( - -100001, - 'endpoint in cosmosClientOptions is required.', - 'https://aka.ms/M365AgentsErrorCodes/#-100001' - ) + static readonly MissingCosmosEndpoint: AgentErrorDefinition = { + code: -100001, + description: 'endpoint in cosmosClientOptions is required.', + helplink: 'https://aka.ms/M365AgentsErrorCodes/#-100001' + } /** * Error thrown when neither key nor tokenProvider is provided in cosmosClientOptions. */ - static readonly MissingCosmosCredentials = new AgentErrorDefinition( - -100002, - 'key or tokenProvider in cosmosClientOptions is required.', - 'https://aka.ms/M365AgentsErrorCodes/#-100002' - ) + static readonly MissingCosmosCredentials: AgentErrorDefinition = { + code: -100002, + description: 'key or tokenProvider in cosmosClientOptions is required.', + helplink: 'https://aka.ms/M365AgentsErrorCodes/#-100002' + } /** * Error thrown when databaseId is not provided. */ - static readonly MissingDatabaseId = new AgentErrorDefinition( - -100003, - 'databaseId for CosmosDB is required.', - 'https://aka.ms/M365AgentsErrorCodes/#-100003' - ) + static readonly MissingDatabaseId: AgentErrorDefinition = { + code: -100003, + description: 'databaseId for CosmosDB is required.', + helplink: 'https://aka.ms/M365AgentsErrorCodes/#-100003' + } /** * Error thrown when containerId is not provided. */ - static readonly MissingContainerId = new AgentErrorDefinition( - -100004, - 'containerId for CosmosDB is required.', - 'https://aka.ms/M365AgentsErrorCodes/#-100004' - ) + static readonly MissingContainerId: AgentErrorDefinition = { + code: -100004, + description: 'containerId for CosmosDB is required.', + helplink: 'https://aka.ms/M365AgentsErrorCodes/#-100004' + } /** * Error thrown when compatibilityMode is enabled with a keySuffix. */ - static readonly InvalidCompatibilityModeWithKeySuffix = new AgentErrorDefinition( - -100005, - 'compatibilityMode cannot be true while using a keySuffix.', - 'https://aka.ms/M365AgentsErrorCodes/#-100005' - ) + static readonly InvalidCompatibilityModeWithKeySuffix: AgentErrorDefinition = { + code: -100005, + description: 'compatibilityMode cannot be true while using a keySuffix.', + helplink: 'https://aka.ms/M365AgentsErrorCodes/#-100005' + } /** * Error thrown when keySuffix contains invalid Row Key characters. */ - static readonly InvalidKeySuffixCharacters = new AgentErrorDefinition( - -100006, - 'Cannot use invalid Row Key characters: {0} in keySuffix', - 'https://aka.ms/M365AgentsErrorCodes/#-100006' - ) + static readonly InvalidKeySuffixCharacters: AgentErrorDefinition = { + code: -100006, + description: 'Cannot use invalid Row Key characters: {0} in keySuffix', + helplink: 'https://aka.ms/M365AgentsErrorCodes/#-100006' + } /** * Error thrown when keys are not provided for reading. */ - static readonly MissingReadKeys = new AgentErrorDefinition( - -100007, - 'Keys are required when reading.', - 'https://aka.ms/M365AgentsErrorCodes/#-100007' - ) + static readonly MissingReadKeys: AgentErrorDefinition = { + code: -100007, + description: 'Keys are required when reading.', + helplink: 'https://aka.ms/M365AgentsErrorCodes/#-100007' + } /** * Error thrown when changes are not provided for writing. */ - static readonly MissingWriteChanges = new AgentErrorDefinition( - -100008, - 'Changes are required when writing.', - 'https://aka.ms/M365AgentsErrorCodes/#-100008' - ) + static readonly MissingWriteChanges: AgentErrorDefinition = { + code: -100008, + description: 'Changes are required when writing.', + helplink: 'https://aka.ms/M365AgentsErrorCodes/#-100008' + } /** * Error thrown when attempting to use a custom partition key path. */ - static readonly UnsupportedCustomPartitionKeyPath = new AgentErrorDefinition( - -100009, - 'Custom Partition Key Paths are not supported. {0} has a custom Partition Key Path of {1}.', - 'https://aka.ms/M365AgentsErrorCodes/#-100009' - ) + static readonly UnsupportedCustomPartitionKeyPath: AgentErrorDefinition = { + code: -100009, + description: 'Custom Partition Key Paths are not supported. {0} has a custom Partition Key Path of {1}.', + helplink: 'https://aka.ms/M365AgentsErrorCodes/#-100009' + } /** * Error thrown when the specified container is not found. */ - static readonly ContainerNotFound = new AgentErrorDefinition( - -100010, - 'Container {0} not found.', - 'https://aka.ms/M365AgentsErrorCodes/#-100010' - ) + static readonly ContainerNotFound: AgentErrorDefinition = { + code: -100010, + description: 'Container {0} not found.', + helplink: 'https://aka.ms/M365AgentsErrorCodes/#-100010' + } /** * Error thrown when the key parameter is missing in CosmosDbKeyEscape. */ - static readonly MissingKeyParameter = new AgentErrorDefinition( - -100011, - "The 'key' parameter is required.", - 'https://aka.ms/M365AgentsErrorCodes/#-100011' - ) + static readonly MissingKeyParameter: AgentErrorDefinition = { + code: -100011, + description: "The 'key' parameter is required.", + helplink: 'https://aka.ms/M365AgentsErrorCodes/#-100011' + } /** * Error thrown when there is an error reading from the container (404 Not Found). */ - static readonly ContainerReadNotFound = new AgentErrorDefinition( - -100012, - 'Not Found', - 'https://aka.ms/M365AgentsErrorCodes/#-100012' - ) + static readonly ContainerReadNotFound: AgentErrorDefinition = { + code: -100012, + description: 'Not Found', + helplink: 'https://aka.ms/M365AgentsErrorCodes/#-100012' + } /** * Error thrown when there is an error reading from container (400 Bad Request). */ - static readonly ContainerReadBadRequest = new AgentErrorDefinition( - -100013, - 'Error reading from container. You might be attempting to read from a non-partitioned container or a container that does not use \'/id\' as the partitionKeyPath', - 'https://aka.ms/M365AgentsErrorCodes/#-100013' - ) + static readonly ContainerReadBadRequest: AgentErrorDefinition = { + code: -100013, + description: 'Error reading from container. You might be attempting to read from a non-partitioned container or a container that does not use \'/id\' as the partitionKeyPath', + helplink: 'https://aka.ms/M365AgentsErrorCodes/#-100013' + } /** * Error thrown when there is a general error reading from the container. */ - static readonly ContainerReadError = new AgentErrorDefinition( - -100014, - 'Error reading from container', - 'https://aka.ms/M365AgentsErrorCodes/#-100014' - ) + static readonly ContainerReadError: AgentErrorDefinition = { + code: -100014, + description: 'Error reading from container', + helplink: 'https://aka.ms/M365AgentsErrorCodes/#-100014' + } /** * Error thrown when there is an error upserting a document. */ - static readonly DocumentUpsertError = new AgentErrorDefinition( - -100015, - 'Error upserting document', - 'https://aka.ms/M365AgentsErrorCodes/#-100015' - ) + static readonly DocumentUpsertError: AgentErrorDefinition = { + code: -100015, + description: 'Error upserting document', + helplink: 'https://aka.ms/M365AgentsErrorCodes/#-100015' + } /** * Error thrown when there is an error deleting a document (404 Not Found). */ - static readonly DocumentDeleteNotFound = new AgentErrorDefinition( - -100016, - 'Not Found', - 'https://aka.ms/M365AgentsErrorCodes/#-100016' - ) + static readonly DocumentDeleteNotFound: AgentErrorDefinition = { + code: -100016, + description: 'Not Found', + helplink: 'https://aka.ms/M365AgentsErrorCodes/#-100016' + } /** * Error thrown when unable to delete a document. */ - static readonly DocumentDeleteError = new AgentErrorDefinition( - -100017, - 'Unable to delete document', - 'https://aka.ms/M365AgentsErrorCodes/#-100017' - ) + static readonly DocumentDeleteError: AgentErrorDefinition = { + code: -100017, + description: 'Unable to delete document', + helplink: 'https://aka.ms/M365AgentsErrorCodes/#-100017' + } /** * Error thrown when failing to initialize CosmosDB database/container. */ - static readonly InitializationError = new AgentErrorDefinition( - -100018, - 'Failed to initialize Cosmos DB database/container: {0}/{1}', - 'https://aka.ms/M365AgentsErrorCodes/#-100018' - ) + static readonly InitializationError: AgentErrorDefinition = { + code: -100018, + description: 'Failed to initialize Cosmos DB database/container: {0}/{1}', + helplink: 'https://aka.ms/M365AgentsErrorCodes/#-100018' + } /** * Error thrown when maximum nesting depth is exceeded. */ - static readonly MaxNestingDepthExceeded = new AgentErrorDefinition( - -100019, - 'Maximum nesting depth of {0} exceeded. {1}', - 'https://aka.ms/M365AgentsErrorCodes/#-100019' - ) -} - -/** - * Helper class for generating exceptions with error codes. - */ -export class ExceptionHelper { - /** - * Generates a typed exception with error code and help link. - * @param ErrorType The constructor of the error type to create - * @param errorDefinition The error definition containing code, description, and help link - * @param innerException Optional inner exception - * @param messageFormat Optional format parameters for the error message - * @returns A new exception instance with error code and help link - */ - static generateException( - ErrorType: new (message: string, innerException?: Error) => T, - errorDefinition: AgentErrorDefinition, - innerException?: Error, - ...messageFormat: string[] - ): T { - // Format the message with parameters if provided - let message = errorDefinition.description - if (messageFormat.length > 0) { - messageFormat.forEach((param, index) => { - message = message.replace(`{${index}}`, param) - }) - } - - // Create the exception - const exception = new ErrorType(message) - - // Set error code and help link as custom properties - const exceptionWithProps = exception as any - exceptionWithProps.code = errorDefinition.code - exceptionWithProps.helpLink = errorDefinition.helplink - - // Store inner exception as a custom property if provided - if (innerException) { - exceptionWithProps.innerException = innerException - } - - return exception + static readonly MaxNestingDepthExceeded: AgentErrorDefinition = { + code: -100019, + description: 'Maximum nesting depth of {0} exceeded. {1}', + helplink: 'https://aka.ms/M365AgentsErrorCodes/#-100019' } } diff --git a/packages/agents-hosting-storage-cosmos/src/index.ts b/packages/agents-hosting-storage-cosmos/src/index.ts index aef6895e..a79a1f16 100644 --- a/packages/agents-hosting-storage-cosmos/src/index.ts +++ b/packages/agents-hosting-storage-cosmos/src/index.ts @@ -3,4 +3,5 @@ export { CosmosDbPartitionedStorage } from './cosmosDbPartitionedStorage' export * from './cosmosDbPartitionedStorageOptions' -export * from './errorHelper' +export { ErrorHelper } from './errorHelper' +export { AgentErrorDefinition, ExceptionHelper } from '@microsoft/agents-activity' diff --git a/packages/agents-hosting-storage-cosmos/test/errorHelper.test.ts b/packages/agents-hosting-storage-cosmos/test/errorHelper.test.ts index 8dad900d..65bd927a 100644 --- a/packages/agents-hosting-storage-cosmos/test/errorHelper.test.ts +++ b/packages/agents-hosting-storage-cosmos/test/errorHelper.test.ts @@ -1,25 +1,7 @@ import assert from 'assert' import { describe, it } from 'node:test' -import { AgentErrorDefinition, ErrorHelper, ExceptionHelper } from '../src/errorHelper' - -describe('AgentErrorDefinition tests', () => { - it('should create an error definition with all properties', () => { - const errorDef = new AgentErrorDefinition(-100000, 'Test error message', 'https://aka.ms/test') - - assert.strictEqual(errorDef.code, -100000) - assert.strictEqual(errorDef.description, 'Test error message') - assert.strictEqual(errorDef.helplink, 'https://aka.ms/test') - }) - - it('should have readonly properties', () => { - const errorDef = new AgentErrorDefinition(-100001, 'Test error', 'https://aka.ms/test') - - // TypeScript enforces readonly at compile time, but we can verify the properties exist - assert.ok(Object.prototype.hasOwnProperty.call(errorDef, 'code')) - assert.ok(Object.prototype.hasOwnProperty.call(errorDef, 'description')) - assert.ok(Object.prototype.hasOwnProperty.call(errorDef, 'helplink')) - }) -}) +import { AgentErrorDefinition } from '@microsoft/agents-activity' +import { ErrorHelper } from '../src/errorHelper' describe('ErrorHelper tests', () => { it('should have MissingCosmosDbStorageOptions error definition', () => { @@ -48,7 +30,7 @@ describe('ErrorHelper tests', () => { it('should have all error codes in the correct range', () => { const errorDefinitions = Object.values(ErrorHelper).filter( - val => val instanceof AgentErrorDefinition + val => val && typeof val === 'object' && 'code' in val && 'description' in val && 'helplink' in val ) as AgentErrorDefinition[] // All error codes should be negative and in the range -100000 to -100019 @@ -61,7 +43,7 @@ describe('ErrorHelper tests', () => { it('should have unique error codes', () => { const errorDefinitions = Object.values(ErrorHelper).filter( - val => val instanceof AgentErrorDefinition + val => val && typeof val === 'object' && 'code' in val && 'description' in val && 'helplink' in val ) as AgentErrorDefinition[] const codes = errorDefinitions.map(e => e.code) @@ -72,7 +54,7 @@ describe('ErrorHelper tests', () => { it('should have help links with correct format', () => { const errorDefinitions = Object.values(ErrorHelper).filter( - val => val instanceof AgentErrorDefinition + val => val && typeof val === 'object' && 'code' in val && 'description' in val && 'helplink' in val ) as AgentErrorDefinition[] errorDefinitions.forEach(errorDef => { @@ -89,7 +71,7 @@ describe('ErrorHelper tests', () => { it('should have non-empty descriptions', () => { const errorDefinitions = Object.values(ErrorHelper).filter( - val => val instanceof AgentErrorDefinition + val => val && typeof val === 'object' && 'code' in val && 'description' in val && 'helplink' in val ) as AgentErrorDefinition[] errorDefinitions.forEach(errorDef => { @@ -97,179 +79,3 @@ describe('ErrorHelper tests', () => { }) }) }) - -describe('ExceptionHelper tests', () => { - it('should generate exception with error code and help link', () => { - const errorDef = new AgentErrorDefinition(-100000, 'Test error', 'https://aka.ms/test') - - const exception = ExceptionHelper.generateException(Error, errorDef) - - assert.strictEqual(exception.message, 'Test error') - assert.strictEqual((exception as any).code, -100000) - assert.strictEqual((exception as any).helpLink, 'https://aka.ms/test') - }) - - it('should generate ReferenceError exception', () => { - const exception = ExceptionHelper.generateException( - ReferenceError, - ErrorHelper.MissingCosmosDbStorageOptions - ) - - assert.ok(exception instanceof ReferenceError) - assert.strictEqual(exception.message, 'CosmosDbPartitionedStorageOptions is required.') - assert.strictEqual((exception as any).code, -100000) - assert.strictEqual((exception as any).helpLink, 'https://aka.ms/M365AgentsErrorCodes/#-100000') - }) - - it('should generate exception with inner exception', () => { - const innerError = new Error('Inner error') - const errorDef = new AgentErrorDefinition(-100000, 'Test error', 'https://aka.ms/test') - - const exception = ExceptionHelper.generateException(Error, errorDef, innerError) - - assert.strictEqual(exception.message, 'Test error') - assert.strictEqual((exception as any).code, -100000) - }) - - it('should format message with single parameter', () => { - const errorDef = new AgentErrorDefinition( - -100006, - 'Cannot use invalid Row Key characters: {0} in keySuffix', - 'https://aka.ms/test' - ) - - const exception = ExceptionHelper.generateException(Error, errorDef, undefined, 'test*suffix') - - assert.strictEqual( - exception.message, - 'Cannot use invalid Row Key characters: test*suffix in keySuffix' - ) - }) - - it('should format message with multiple parameters', () => { - const errorDef = new AgentErrorDefinition( - -100009, - 'Custom Partition Key Paths are not supported. {0} has a custom Partition Key Path of {1}.', - 'https://aka.ms/test' - ) - - const exception = ExceptionHelper.generateException( - Error, - errorDef, - undefined, - 'myContainer', - '/customPath' - ) - - assert.strictEqual( - exception.message, - 'Custom Partition Key Paths are not supported. myContainer has a custom Partition Key Path of /customPath.' - ) - }) - - it('should handle message with no parameters', () => { - const errorDef = new AgentErrorDefinition(-100000, 'Simple error message', 'https://aka.ms/test') - - const exception = ExceptionHelper.generateException(Error, errorDef) - - assert.strictEqual(exception.message, 'Simple error message') - }) - - it('should throw and catch exception correctly', () => { - assert.throws( - () => { - throw ExceptionHelper.generateException( - ReferenceError, - ErrorHelper.MissingCosmosDbStorageOptions - ) - }, - (err: any) => { - return ( - err instanceof ReferenceError && - err.message === 'CosmosDbPartitionedStorageOptions is required.' && - err.code === -100000 && - err.helpLink === 'https://aka.ms/M365AgentsErrorCodes/#-100000' - ) - } - ) - }) -}) - -describe('ErrorHelper integration tests', () => { - it('should work with InvalidKeySuffixCharacters error', () => { - const exception = ExceptionHelper.generateException( - ReferenceError, - ErrorHelper.InvalidKeySuffixCharacters, - undefined, - 'invalid*suffix' - ) - - assert.ok(exception instanceof ReferenceError) - assert.strictEqual( - exception.message, - 'Cannot use invalid Row Key characters: invalid*suffix in keySuffix' - ) - assert.strictEqual((exception as any).code, -100006) - }) - - it('should work with UnsupportedCustomPartitionKeyPath error', () => { - const exception = ExceptionHelper.generateException( - Error, - ErrorHelper.UnsupportedCustomPartitionKeyPath, - undefined, - 'myContainer', - '/custom' - ) - - assert.ok(exception instanceof Error) - assert.strictEqual( - exception.message, - 'Custom Partition Key Paths are not supported. myContainer has a custom Partition Key Path of /custom.' - ) - assert.strictEqual((exception as any).code, -100009) - }) - - it('should work with ContainerNotFound error', () => { - const exception = ExceptionHelper.generateException( - Error, - ErrorHelper.ContainerNotFound, - undefined, - 'myContainer' - ) - - assert.strictEqual(exception.message, 'Container myContainer not found.') - assert.strictEqual((exception as any).code, -100010) - }) - - it('should work with InitializationError', () => { - const exception = ExceptionHelper.generateException( - Error, - ErrorHelper.InitializationError, - undefined, - 'myDatabase', - 'myContainer' - ) - - assert.strictEqual( - exception.message, - 'Failed to initialize Cosmos DB database/container: myDatabase/myContainer' - ) - assert.strictEqual((exception as any).code, -100018) - }) - - it('should work with MaxNestingDepthExceeded error', () => { - const exception = ExceptionHelper.generateException( - Error, - ErrorHelper.MaxNestingDepthExceeded, - undefined, - '127', - 'Additional context message' - ) - - assert.strictEqual( - exception.message, - 'Maximum nesting depth of 127 exceeded. Additional context message' - ) - assert.strictEqual((exception as any).code, -100019) - }) -}) From e0406d6d6d8bfe9a25e376970ad212329420382f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 18 Nov 2025 22:24:44 +0000 Subject: [PATCH 10/18] Refactor ErrorHelper class to Errors const object Co-authored-by: cleemullins <1165321+cleemullins@users.noreply.github.com> --- .../src/cosmosDbKeyEscape.ts | 4 +- .../src/cosmosDbPartitionedStorage.ts | 36 +++---- .../src/errorHelper.ts | 97 +++++++++---------- .../src/index.ts | 2 +- .../test/errorHelper.test.ts | 18 ++-- 5 files changed, 77 insertions(+), 80 deletions(-) diff --git a/packages/agents-hosting-storage-cosmos/src/cosmosDbKeyEscape.ts b/packages/agents-hosting-storage-cosmos/src/cosmosDbKeyEscape.ts index e5a1bad3..98d93c8f 100644 --- a/packages/agents-hosting-storage-cosmos/src/cosmosDbKeyEscape.ts +++ b/packages/agents-hosting-storage-cosmos/src/cosmosDbKeyEscape.ts @@ -2,7 +2,7 @@ // Licensed under the MIT License. import { createHash } from 'crypto' import { ExceptionHelper } from '@microsoft/agents-activity' -import { ErrorHelper } from './errorHelper' +import { Errors } from './errorHelper' /** * Provides methods for escaping keys for Cosmos DB. @@ -29,7 +29,7 @@ export namespace CosmosDbKeyEscape { if (!key) { throw ExceptionHelper.generateException( Error, - ErrorHelper.MissingKeyParameter + Errors.MissingKeyParameter ) } diff --git a/packages/agents-hosting-storage-cosmos/src/cosmosDbPartitionedStorage.ts b/packages/agents-hosting-storage-cosmos/src/cosmosDbPartitionedStorage.ts index bc8e5f94..7ae46305 100644 --- a/packages/agents-hosting-storage-cosmos/src/cosmosDbPartitionedStorage.ts +++ b/packages/agents-hosting-storage-cosmos/src/cosmosDbPartitionedStorage.ts @@ -7,7 +7,7 @@ import { DocumentStoreItem } from './documentStoreItem' import { CosmosDbPartitionedStorageOptions } from './cosmosDbPartitionedStorageOptions' import { Storage, StoreItems } from '@microsoft/agents-hosting' import { ExceptionHelper } from '@microsoft/agents-activity' -import { ErrorHelper } from './errorHelper' +import { Errors } from './errorHelper' /** * A utility class to ensure that a specific asynchronous task is executed only once for a given key. @@ -59,32 +59,32 @@ export class CosmosDbPartitionedStorage implements Storage { if (!cosmosDbStorageOptions) { throw ExceptionHelper.generateException( ReferenceError, - ErrorHelper.MissingCosmosDbStorageOptions + Errors.MissingCosmosDbStorageOptions ) } const { cosmosClientOptions } = cosmosDbStorageOptions if (!cosmosClientOptions?.endpoint) { throw ExceptionHelper.generateException( ReferenceError, - ErrorHelper.MissingCosmosEndpoint + Errors.MissingCosmosEndpoint ) } if (!cosmosClientOptions?.key && !cosmosClientOptions?.tokenProvider) { throw ExceptionHelper.generateException( ReferenceError, - ErrorHelper.MissingCosmosCredentials + Errors.MissingCosmosCredentials ) } if (!cosmosDbStorageOptions.databaseId) { throw ExceptionHelper.generateException( ReferenceError, - ErrorHelper.MissingDatabaseId + Errors.MissingDatabaseId ) } if (!cosmosDbStorageOptions.containerId) { throw ExceptionHelper.generateException( ReferenceError, - ErrorHelper.MissingContainerId + Errors.MissingContainerId ) } cosmosDbStorageOptions.compatibilityMode ??= true @@ -92,14 +92,14 @@ export class CosmosDbPartitionedStorage implements Storage { if (cosmosDbStorageOptions.compatibilityMode) { throw ExceptionHelper.generateException( ReferenceError, - ErrorHelper.InvalidCompatibilityModeWithKeySuffix + Errors.InvalidCompatibilityModeWithKeySuffix ) } const suffixEscaped = CosmosDbKeyEscape.escapeKey(cosmosDbStorageOptions.keySuffix) if (cosmosDbStorageOptions.keySuffix !== suffixEscaped) { throw ExceptionHelper.generateException( ReferenceError, - ErrorHelper.InvalidKeySuffixCharacters, + Errors.InvalidKeySuffixCharacters, undefined, cosmosDbStorageOptions.keySuffix ) @@ -116,7 +116,7 @@ export class CosmosDbPartitionedStorage implements Storage { if (!keys) { throw ExceptionHelper.generateException( ReferenceError, - ErrorHelper.MissingReadKeys + Errors.MissingReadKeys ) } else if (keys.length === 0) { return {} @@ -149,13 +149,13 @@ export class CosmosDbPartitionedStorage implements Storage { } else if (err.code === 400) { throw ExceptionHelper.generateException( Error, - ErrorHelper.ContainerReadBadRequest, + Errors.ContainerReadBadRequest, err ) } else { throw ExceptionHelper.generateException( Error, - ErrorHelper.ContainerReadError, + Errors.ContainerReadError, err ) } @@ -174,7 +174,7 @@ export class CosmosDbPartitionedStorage implements Storage { if (!changes) { throw ExceptionHelper.generateException( ReferenceError, - ErrorHelper.MissingWriteChanges + Errors.MissingWriteChanges ) } else if (changes.length === 0) { return @@ -205,7 +205,7 @@ export class CosmosDbPartitionedStorage implements Storage { this.checkForNestingError(change, err) throw ExceptionHelper.generateException( Error, - ErrorHelper.DocumentUpsertError, + Errors.DocumentUpsertError, err ) } @@ -235,7 +235,7 @@ export class CosmosDbPartitionedStorage implements Storage { } else { throw ExceptionHelper.generateException( Error, - ErrorHelper.DocumentDeleteError, + Errors.DocumentDeleteError, err ) } @@ -281,7 +281,7 @@ export class CosmosDbPartitionedStorage implements Storage { } else if (paths.indexOf(DocumentStoreItem.partitionKeyPath) === -1) { throw ExceptionHelper.generateException( Error, - ErrorHelper.UnsupportedCustomPartitionKeyPath, + Errors.UnsupportedCustomPartitionKeyPath, undefined, this.cosmosDbStorageOptions.containerId, paths[0] @@ -310,7 +310,7 @@ export class CosmosDbPartitionedStorage implements Storage { if (!container) { throw ExceptionHelper.generateException( Error, - ErrorHelper.ContainerNotFound, + Errors.ContainerNotFound, undefined, this.cosmosDbStorageOptions.containerId ) @@ -319,7 +319,7 @@ export class CosmosDbPartitionedStorage implements Storage { } catch (err: any) { throw ExceptionHelper.generateException( Error, - ErrorHelper.InitializationError, + Errors.InitializationError, err, this.cosmosDbStorageOptions.databaseId, this.cosmosDbStorageOptions.containerId @@ -354,7 +354,7 @@ export class CosmosDbPartitionedStorage implements Storage { throw ExceptionHelper.generateException( Error, - ErrorHelper.MaxNestingDepthExceeded, + Errors.MaxNestingDepthExceeded, errorObj, maxDepthAllowed.toString(), additionalMessage diff --git a/packages/agents-hosting-storage-cosmos/src/errorHelper.ts b/packages/agents-hosting-storage-cosmos/src/errorHelper.ts index 8766560a..9bd53c99 100644 --- a/packages/agents-hosting-storage-cosmos/src/errorHelper.ts +++ b/packages/agents-hosting-storage-cosmos/src/errorHelper.ts @@ -4,199 +4,196 @@ import { AgentErrorDefinition } from '@microsoft/agents-activity' /** - * Error helper for the CosmosDB storage system. - * This is used to define localized error codes for the CosmosDB storage subsystem of the AgentSDK. + * Error definitions for the CosmosDB storage system. + * This contains localized error codes for the CosmosDB storage subsystem of the AgentSDK. * - * Each error should be created as an AgentErrorDefinition and added to the ErrorHelper class. - * Each definition should include an error code (starting from -100000), a description, and a help link + * Each error definition includes an error code (starting from -100000), a description, and a help link * pointing to an AKA link to get help for the given error. * * Usage example: * ``` - * throw new ReferenceError(ErrorHelper.MissingCosmosDbStorageOptions.description) { - * code: ErrorHelper.MissingCosmosDbStorageOptions.code, - * // Note: In JavaScript, we use a custom property 'code' instead of HResult - * }; + * throw ExceptionHelper.generateException( + * ReferenceError, + * Errors.MissingCosmosDbStorageOptions + * ); * ``` */ -export class ErrorHelper { - // Base error code for CosmosDB storage: -100000 - +export const Errors: { [key: string]: AgentErrorDefinition } = { /** * Error thrown when CosmosDbPartitionedStorageOptions is not provided. */ - static readonly MissingCosmosDbStorageOptions: AgentErrorDefinition = { + MissingCosmosDbStorageOptions: { code: -100000, description: 'CosmosDbPartitionedStorageOptions is required.', helplink: 'https://aka.ms/M365AgentsErrorCodes/#-100000' - } + }, /** * Error thrown when endpoint in cosmosClientOptions is not provided. */ - static readonly MissingCosmosEndpoint: AgentErrorDefinition = { + MissingCosmosEndpoint: { code: -100001, description: 'endpoint in cosmosClientOptions is required.', helplink: 'https://aka.ms/M365AgentsErrorCodes/#-100001' - } + }, /** * Error thrown when neither key nor tokenProvider is provided in cosmosClientOptions. */ - static readonly MissingCosmosCredentials: AgentErrorDefinition = { + MissingCosmosCredentials: { code: -100002, description: 'key or tokenProvider in cosmosClientOptions is required.', helplink: 'https://aka.ms/M365AgentsErrorCodes/#-100002' - } + }, /** * Error thrown when databaseId is not provided. */ - static readonly MissingDatabaseId: AgentErrorDefinition = { + MissingDatabaseId: { code: -100003, description: 'databaseId for CosmosDB is required.', helplink: 'https://aka.ms/M365AgentsErrorCodes/#-100003' - } + }, /** * Error thrown when containerId is not provided. */ - static readonly MissingContainerId: AgentErrorDefinition = { + MissingContainerId: { code: -100004, description: 'containerId for CosmosDB is required.', helplink: 'https://aka.ms/M365AgentsErrorCodes/#-100004' - } + }, /** * Error thrown when compatibilityMode is enabled with a keySuffix. */ - static readonly InvalidCompatibilityModeWithKeySuffix: AgentErrorDefinition = { + InvalidCompatibilityModeWithKeySuffix: { code: -100005, description: 'compatibilityMode cannot be true while using a keySuffix.', helplink: 'https://aka.ms/M365AgentsErrorCodes/#-100005' - } + }, /** * Error thrown when keySuffix contains invalid Row Key characters. */ - static readonly InvalidKeySuffixCharacters: AgentErrorDefinition = { + InvalidKeySuffixCharacters: { code: -100006, description: 'Cannot use invalid Row Key characters: {0} in keySuffix', helplink: 'https://aka.ms/M365AgentsErrorCodes/#-100006' - } + }, /** * Error thrown when keys are not provided for reading. */ - static readonly MissingReadKeys: AgentErrorDefinition = { + MissingReadKeys: { code: -100007, description: 'Keys are required when reading.', helplink: 'https://aka.ms/M365AgentsErrorCodes/#-100007' - } + }, /** * Error thrown when changes are not provided for writing. */ - static readonly MissingWriteChanges: AgentErrorDefinition = { + MissingWriteChanges: { code: -100008, description: 'Changes are required when writing.', helplink: 'https://aka.ms/M365AgentsErrorCodes/#-100008' - } + }, /** * Error thrown when attempting to use a custom partition key path. */ - static readonly UnsupportedCustomPartitionKeyPath: AgentErrorDefinition = { + UnsupportedCustomPartitionKeyPath: { code: -100009, description: 'Custom Partition Key Paths are not supported. {0} has a custom Partition Key Path of {1}.', helplink: 'https://aka.ms/M365AgentsErrorCodes/#-100009' - } + }, /** * Error thrown when the specified container is not found. */ - static readonly ContainerNotFound: AgentErrorDefinition = { + ContainerNotFound: { code: -100010, description: 'Container {0} not found.', helplink: 'https://aka.ms/M365AgentsErrorCodes/#-100010' - } + }, /** * Error thrown when the key parameter is missing in CosmosDbKeyEscape. */ - static readonly MissingKeyParameter: AgentErrorDefinition = { + MissingKeyParameter: { code: -100011, description: "The 'key' parameter is required.", helplink: 'https://aka.ms/M365AgentsErrorCodes/#-100011' - } + }, /** * Error thrown when there is an error reading from the container (404 Not Found). */ - static readonly ContainerReadNotFound: AgentErrorDefinition = { + ContainerReadNotFound: { code: -100012, description: 'Not Found', helplink: 'https://aka.ms/M365AgentsErrorCodes/#-100012' - } + }, /** * Error thrown when there is an error reading from container (400 Bad Request). */ - static readonly ContainerReadBadRequest: AgentErrorDefinition = { + ContainerReadBadRequest: { code: -100013, description: 'Error reading from container. You might be attempting to read from a non-partitioned container or a container that does not use \'/id\' as the partitionKeyPath', helplink: 'https://aka.ms/M365AgentsErrorCodes/#-100013' - } + }, /** * Error thrown when there is a general error reading from the container. */ - static readonly ContainerReadError: AgentErrorDefinition = { + ContainerReadError: { code: -100014, description: 'Error reading from container', helplink: 'https://aka.ms/M365AgentsErrorCodes/#-100014' - } + }, /** * Error thrown when there is an error upserting a document. */ - static readonly DocumentUpsertError: AgentErrorDefinition = { + DocumentUpsertError: { code: -100015, description: 'Error upserting document', helplink: 'https://aka.ms/M365AgentsErrorCodes/#-100015' - } + }, /** * Error thrown when there is an error deleting a document (404 Not Found). */ - static readonly DocumentDeleteNotFound: AgentErrorDefinition = { + DocumentDeleteNotFound: { code: -100016, description: 'Not Found', helplink: 'https://aka.ms/M365AgentsErrorCodes/#-100016' - } + }, /** * Error thrown when unable to delete a document. */ - static readonly DocumentDeleteError: AgentErrorDefinition = { + DocumentDeleteError: { code: -100017, description: 'Unable to delete document', helplink: 'https://aka.ms/M365AgentsErrorCodes/#-100017' - } + }, /** * Error thrown when failing to initialize CosmosDB database/container. */ - static readonly InitializationError: AgentErrorDefinition = { + InitializationError: { code: -100018, description: 'Failed to initialize Cosmos DB database/container: {0}/{1}', helplink: 'https://aka.ms/M365AgentsErrorCodes/#-100018' - } + }, /** * Error thrown when maximum nesting depth is exceeded. */ - static readonly MaxNestingDepthExceeded: AgentErrorDefinition = { + MaxNestingDepthExceeded: { code: -100019, description: 'Maximum nesting depth of {0} exceeded. {1}', helplink: 'https://aka.ms/M365AgentsErrorCodes/#-100019' diff --git a/packages/agents-hosting-storage-cosmos/src/index.ts b/packages/agents-hosting-storage-cosmos/src/index.ts index a79a1f16..3a19b772 100644 --- a/packages/agents-hosting-storage-cosmos/src/index.ts +++ b/packages/agents-hosting-storage-cosmos/src/index.ts @@ -3,5 +3,5 @@ export { CosmosDbPartitionedStorage } from './cosmosDbPartitionedStorage' export * from './cosmosDbPartitionedStorageOptions' -export { ErrorHelper } from './errorHelper' +export { Errors } from './errorHelper' export { AgentErrorDefinition, ExceptionHelper } from '@microsoft/agents-activity' diff --git a/packages/agents-hosting-storage-cosmos/test/errorHelper.test.ts b/packages/agents-hosting-storage-cosmos/test/errorHelper.test.ts index 65bd927a..7c93312a 100644 --- a/packages/agents-hosting-storage-cosmos/test/errorHelper.test.ts +++ b/packages/agents-hosting-storage-cosmos/test/errorHelper.test.ts @@ -1,11 +1,11 @@ import assert from 'assert' import { describe, it } from 'node:test' import { AgentErrorDefinition } from '@microsoft/agents-activity' -import { ErrorHelper } from '../src/errorHelper' +import { Errors } from '../src/errorHelper' -describe('ErrorHelper tests', () => { +describe('Errors tests', () => { it('should have MissingCosmosDbStorageOptions error definition', () => { - const error = ErrorHelper.MissingCosmosDbStorageOptions + const error = Errors.MissingCosmosDbStorageOptions assert.strictEqual(error.code, -100000) assert.strictEqual(error.description, 'CosmosDbPartitionedStorageOptions is required.') @@ -13,7 +13,7 @@ describe('ErrorHelper tests', () => { }) it('should have MissingCosmosEndpoint error definition', () => { - const error = ErrorHelper.MissingCosmosEndpoint + const error = Errors.MissingCosmosEndpoint assert.strictEqual(error.code, -100001) assert.strictEqual(error.description, 'endpoint in cosmosClientOptions is required.') @@ -21,7 +21,7 @@ describe('ErrorHelper tests', () => { }) it('should have MissingCosmosCredentials error definition', () => { - const error = ErrorHelper.MissingCosmosCredentials + const error = Errors.MissingCosmosCredentials assert.strictEqual(error.code, -100002) assert.strictEqual(error.description, 'key or tokenProvider in cosmosClientOptions is required.') @@ -29,7 +29,7 @@ describe('ErrorHelper tests', () => { }) it('should have all error codes in the correct range', () => { - const errorDefinitions = Object.values(ErrorHelper).filter( + const errorDefinitions = Object.values(Errors).filter( val => val && typeof val === 'object' && 'code' in val && 'description' in val && 'helplink' in val ) as AgentErrorDefinition[] @@ -42,7 +42,7 @@ describe('ErrorHelper tests', () => { }) it('should have unique error codes', () => { - const errorDefinitions = Object.values(ErrorHelper).filter( + const errorDefinitions = Object.values(Errors).filter( val => val && typeof val === 'object' && 'code' in val && 'description' in val && 'helplink' in val ) as AgentErrorDefinition[] @@ -53,7 +53,7 @@ describe('ErrorHelper tests', () => { }) it('should have help links with correct format', () => { - const errorDefinitions = Object.values(ErrorHelper).filter( + const errorDefinitions = Object.values(Errors).filter( val => val && typeof val === 'object' && 'code' in val && 'description' in val && 'helplink' in val ) as AgentErrorDefinition[] @@ -70,7 +70,7 @@ describe('ErrorHelper tests', () => { }) it('should have non-empty descriptions', () => { - const errorDefinitions = Object.values(ErrorHelper).filter( + const errorDefinitions = Object.values(Errors).filter( val => val && typeof val === 'object' && 'code' in val && 'description' in val && 'helplink' in val ) as AgentErrorDefinition[] From ab57c72511484769caf1f6bad4f631f3b10d39d5 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 18 Nov 2025 22:38:43 +0000 Subject: [PATCH 11/18] Change ExceptionHelper to use named parameters object instead of array indices Co-authored-by: cleemullins <1165321+cleemullins@users.noreply.github.com> --- .../agents-activity/src/exceptionHelper.ts | 10 ++++----- .../test/exceptionHelper.test.ts | 12 +++++----- .../src/cosmosDbPartitionedStorage.ts | 22 ++++++++++++------- .../src/errorHelper.ts | 10 ++++----- 4 files changed, 31 insertions(+), 23 deletions(-) diff --git a/packages/agents-activity/src/exceptionHelper.ts b/packages/agents-activity/src/exceptionHelper.ts index 8593d008..3fbf6486 100644 --- a/packages/agents-activity/src/exceptionHelper.ts +++ b/packages/agents-activity/src/exceptionHelper.ts @@ -31,20 +31,20 @@ export class ExceptionHelper { * @param ErrorType The constructor of the error type to create * @param errorDefinition The error definition containing code, description, and help link * @param innerException Optional inner exception - * @param messageFormat Optional format parameters for the error message + * @param params Optional parameters object for message formatting with key-value pairs * @returns A new exception instance with error code and help link */ static generateException ( ErrorType: new (message: string, innerException?: Error) => T, errorDefinition: AgentErrorDefinition, innerException?: Error, - ...messageFormat: string[] + params?: { [key: string]: string } ): T { // Format the message with parameters if provided let message = errorDefinition.description - if (messageFormat.length > 0) { - messageFormat.forEach((param, index) => { - message = message.replace(`{${index}}`, param) + if (params) { + Object.keys(params).forEach((key) => { + message = message.replace(`{${key}}`, params[key]) }) } diff --git a/packages/agents-activity/test/exceptionHelper.test.ts b/packages/agents-activity/test/exceptionHelper.test.ts index 533c473d..8b3e8d00 100644 --- a/packages/agents-activity/test/exceptionHelper.test.ts +++ b/packages/agents-activity/test/exceptionHelper.test.ts @@ -76,11 +76,11 @@ describe('ExceptionHelper tests', () => { it('should format message with single parameter', () => { const errorDef: AgentErrorDefinition = { code: -100006, - description: 'Cannot use invalid Row Key characters: {0} in keySuffix', + description: 'Cannot use invalid Row Key characters: {keySuffix} in keySuffix', helplink: 'https://aka.ms/test' } - const exception = ExceptionHelper.generateException(Error, errorDef, undefined, 'test*suffix') + const exception = ExceptionHelper.generateException(Error, errorDef, undefined, { keySuffix: 'test*suffix' }) assert.strictEqual( exception.message, @@ -91,7 +91,7 @@ describe('ExceptionHelper tests', () => { it('should format message with multiple parameters', () => { const errorDef: AgentErrorDefinition = { code: -100009, - description: 'Custom Partition Key Paths are not supported. {0} has a custom Partition Key Path of {1}.', + description: 'Custom Partition Key Paths are not supported. {containerId} has a custom Partition Key Path of {partitionKeyPath}.', helplink: 'https://aka.ms/test' } @@ -99,8 +99,10 @@ describe('ExceptionHelper tests', () => { Error, errorDef, undefined, - 'myContainer', - '/customPath' + { + containerId: 'myContainer', + partitionKeyPath: '/customPath' + } ) assert.strictEqual( diff --git a/packages/agents-hosting-storage-cosmos/src/cosmosDbPartitionedStorage.ts b/packages/agents-hosting-storage-cosmos/src/cosmosDbPartitionedStorage.ts index 7ae46305..40641095 100644 --- a/packages/agents-hosting-storage-cosmos/src/cosmosDbPartitionedStorage.ts +++ b/packages/agents-hosting-storage-cosmos/src/cosmosDbPartitionedStorage.ts @@ -101,7 +101,7 @@ export class CosmosDbPartitionedStorage implements Storage { ReferenceError, Errors.InvalidKeySuffixCharacters, undefined, - cosmosDbStorageOptions.keySuffix + { keySuffix: cosmosDbStorageOptions.keySuffix } ) } } @@ -283,8 +283,10 @@ export class CosmosDbPartitionedStorage implements Storage { Error, Errors.UnsupportedCustomPartitionKeyPath, undefined, - this.cosmosDbStorageOptions.containerId, - paths[0] + { + containerId: this.cosmosDbStorageOptions.containerId, + partitionKeyPath: paths[0] + } ) } } else { @@ -312,7 +314,7 @@ export class CosmosDbPartitionedStorage implements Storage { Error, Errors.ContainerNotFound, undefined, - this.cosmosDbStorageOptions.containerId + { containerId: this.cosmosDbStorageOptions.containerId } ) } return container @@ -321,8 +323,10 @@ export class CosmosDbPartitionedStorage implements Storage { Error, Errors.InitializationError, err, - this.cosmosDbStorageOptions.databaseId, - this.cosmosDbStorageOptions.containerId + { + databaseId: this.cosmosDbStorageOptions.databaseId, + containerId: this.cosmosDbStorageOptions.containerId + } ) } } @@ -356,8 +360,10 @@ export class CosmosDbPartitionedStorage implements Storage { Error, Errors.MaxNestingDepthExceeded, errorObj, - maxDepthAllowed.toString(), - additionalMessage + { + maxDepth: maxDepthAllowed.toString(), + additionalMessage: additionalMessage + } ) } else if (obj && typeof obj === 'object') { for (const [key, value] of Object.entries(obj)) { diff --git a/packages/agents-hosting-storage-cosmos/src/errorHelper.ts b/packages/agents-hosting-storage-cosmos/src/errorHelper.ts index 9bd53c99..de914b0e 100644 --- a/packages/agents-hosting-storage-cosmos/src/errorHelper.ts +++ b/packages/agents-hosting-storage-cosmos/src/errorHelper.ts @@ -78,7 +78,7 @@ export const Errors: { [key: string]: AgentErrorDefinition } = { */ InvalidKeySuffixCharacters: { code: -100006, - description: 'Cannot use invalid Row Key characters: {0} in keySuffix', + description: 'Cannot use invalid Row Key characters: {keySuffix} in keySuffix', helplink: 'https://aka.ms/M365AgentsErrorCodes/#-100006' }, @@ -105,7 +105,7 @@ export const Errors: { [key: string]: AgentErrorDefinition } = { */ UnsupportedCustomPartitionKeyPath: { code: -100009, - description: 'Custom Partition Key Paths are not supported. {0} has a custom Partition Key Path of {1}.', + description: 'Custom Partition Key Paths are not supported. {containerId} has a custom Partition Key Path of {partitionKeyPath}.', helplink: 'https://aka.ms/M365AgentsErrorCodes/#-100009' }, @@ -114,7 +114,7 @@ export const Errors: { [key: string]: AgentErrorDefinition } = { */ ContainerNotFound: { code: -100010, - description: 'Container {0} not found.', + description: 'Container {containerId} not found.', helplink: 'https://aka.ms/M365AgentsErrorCodes/#-100010' }, @@ -186,7 +186,7 @@ export const Errors: { [key: string]: AgentErrorDefinition } = { */ InitializationError: { code: -100018, - description: 'Failed to initialize Cosmos DB database/container: {0}/{1}', + description: 'Failed to initialize Cosmos DB database/container: {databaseId}/{containerId}', helplink: 'https://aka.ms/M365AgentsErrorCodes/#-100018' }, @@ -195,7 +195,7 @@ export const Errors: { [key: string]: AgentErrorDefinition } = { */ MaxNestingDepthExceeded: { code: -100019, - description: 'Maximum nesting depth of {0} exceeded. {1}', + description: 'Maximum nesting depth of {maxDepth} exceeded. {additionalMessage}', helplink: 'https://aka.ms/M365AgentsErrorCodes/#-100019' } } From e173c1ee1d5b0ed06ab4e3a28946bc9484519192 Mon Sep 17 00:00:00 2001 From: Ben Brown Date: Wed, 19 Nov 2025 11:00:37 -0600 Subject: [PATCH 12/18] revert changes to tsconfig. add ts-expect-error in test for undefined fields --- packages/agents-activity/test/exceptionHelper.test.ts | 2 ++ packages/agents-activity/tsconfig.json | 5 ++--- packages/agents-hosting-storage-cosmos/tsconfig.json | 5 ++--- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/agents-activity/test/exceptionHelper.test.ts b/packages/agents-activity/test/exceptionHelper.test.ts index 8b3e8d00..b3582820 100644 --- a/packages/agents-activity/test/exceptionHelper.test.ts +++ b/packages/agents-activity/test/exceptionHelper.test.ts @@ -138,7 +138,9 @@ describe('ExceptionHelper tests', () => { return ( err instanceof ReferenceError && err.message === 'Test error' && + // @ts-expect-error err.code === -100000 && + // @ts-expect-error err.helpLink === 'https://aka.ms/test' ) } diff --git a/packages/agents-activity/tsconfig.json b/packages/agents-activity/tsconfig.json index 1b4ef51e..8208479a 100644 --- a/packages/agents-activity/tsconfig.json +++ b/packages/agents-activity/tsconfig.json @@ -1,11 +1,10 @@ { "extends": "../../tsconfig.json", "compilerOptions": { - "rootDirs": [ "src/"], + "rootDirs": [ "src/", "test/"], "outDir": "dist", "paths": { "@microsoft/*": ["./packages/*"] } - }, - "exclude": ["test/**/*"] + } } diff --git a/packages/agents-hosting-storage-cosmos/tsconfig.json b/packages/agents-hosting-storage-cosmos/tsconfig.json index 1b4ef51e..8208479a 100644 --- a/packages/agents-hosting-storage-cosmos/tsconfig.json +++ b/packages/agents-hosting-storage-cosmos/tsconfig.json @@ -1,11 +1,10 @@ { "extends": "../../tsconfig.json", "compilerOptions": { - "rootDirs": [ "src/"], + "rootDirs": [ "src/", "test/"], "outDir": "dist", "paths": { "@microsoft/*": ["./packages/*"] } - }, - "exclude": ["test/**/*"] + } } From 0b83fad41721d3ee6855bb4e7512def3874b6a03 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 19 Nov 2025 17:37:24 +0000 Subject: [PATCH 13/18] Add AgentError interface to eliminate TypeScript warnings on custom error properties Co-authored-by: cleemullins <1165321+cleemullins@users.noreply.github.com> --- .../agents-activity/src/exceptionHelper.ts | 34 +++++++++++++++---- packages/agents-activity/src/index.ts | 2 +- .../test/exceptionHelper.test.ts | 18 +++++----- .../src/index.ts | 2 +- 4 files changed, 37 insertions(+), 19 deletions(-) diff --git a/packages/agents-activity/src/exceptionHelper.ts b/packages/agents-activity/src/exceptionHelper.ts index 3fbf6486..7410d030 100644 --- a/packages/agents-activity/src/exceptionHelper.ts +++ b/packages/agents-activity/src/exceptionHelper.ts @@ -22,6 +22,27 @@ export interface AgentErrorDefinition { helplink: string } +/** + * Enhanced error type with additional properties for error code, help link, and inner exception. + * This interface extends the standard Error type with custom properties added by ExceptionHelper. + */ +export interface AgentError extends Error { + /** + * Error code for the exception + */ + code: number + + /** + * Help URL link for the error + */ + helpLink: string + + /** + * Optional inner exception + */ + innerException?: Error +} + /** * Helper class for generating exceptions with error codes. */ @@ -32,14 +53,14 @@ export class ExceptionHelper { * @param errorDefinition The error definition containing code, description, and help link * @param innerException Optional inner exception * @param params Optional parameters object for message formatting with key-value pairs - * @returns A new exception instance with error code and help link + * @returns A new exception instance with error code and help link, typed as AgentError */ static generateException ( ErrorType: new (message: string, innerException?: Error) => T, errorDefinition: AgentErrorDefinition, innerException?: Error, params?: { [key: string]: string } - ): T { + ): T & AgentError { // Format the message with parameters if provided let message = errorDefinition.description if (params) { @@ -49,16 +70,15 @@ export class ExceptionHelper { } // Create the exception - const exception = new ErrorType(message) + const exception = new ErrorType(message) as T & AgentError // Set error code and help link as custom properties - const exceptionWithProps = exception as any - exceptionWithProps.code = errorDefinition.code - exceptionWithProps.helpLink = errorDefinition.helplink + exception.code = errorDefinition.code + exception.helpLink = errorDefinition.helplink // Store inner exception as a custom property if provided if (innerException) { - exceptionWithProps.innerException = innerException + exception.innerException = innerException } return exception diff --git a/packages/agents-activity/src/index.ts b/packages/agents-activity/src/index.ts index a6cead00..0126453f 100644 --- a/packages/agents-activity/src/index.ts +++ b/packages/agents-activity/src/index.ts @@ -49,4 +49,4 @@ export { TextHighlight } from './textHighlight' export { ActivityTreatments } from './activityTreatments' export { debug, Logger } from './logger' -export { AgentErrorDefinition, ExceptionHelper } from './exceptionHelper' +export { AgentErrorDefinition, AgentError, ExceptionHelper } from './exceptionHelper' diff --git a/packages/agents-activity/test/exceptionHelper.test.ts b/packages/agents-activity/test/exceptionHelper.test.ts index b3582820..cc7ffdd3 100644 --- a/packages/agents-activity/test/exceptionHelper.test.ts +++ b/packages/agents-activity/test/exceptionHelper.test.ts @@ -1,6 +1,6 @@ import assert from 'assert' import { describe, it } from 'node:test' -import { AgentErrorDefinition, ExceptionHelper } from '../src/exceptionHelper' +import { AgentErrorDefinition, AgentError, ExceptionHelper } from '../src/exceptionHelper' describe('AgentErrorDefinition tests', () => { it('should create an error definition with all properties', () => { @@ -39,8 +39,8 @@ describe('ExceptionHelper tests', () => { const exception = ExceptionHelper.generateException(Error, errorDef) assert.strictEqual(exception.message, 'Test error') - assert.strictEqual((exception as any).code, -100000) - assert.strictEqual((exception as any).helpLink, 'https://aka.ms/test') + assert.strictEqual(exception.code, -100000) + assert.strictEqual(exception.helpLink, 'https://aka.ms/test') }) it('should generate ReferenceError exception', () => { @@ -54,8 +54,8 @@ describe('ExceptionHelper tests', () => { assert.ok(exception instanceof ReferenceError) assert.strictEqual(exception.message, 'Reference error test') - assert.strictEqual((exception as any).code, -100000) - assert.strictEqual((exception as any).helpLink, 'https://aka.ms/test') + assert.strictEqual(exception.code, -100000) + assert.strictEqual(exception.helpLink, 'https://aka.ms/test') }) it('should generate exception with inner exception', () => { @@ -69,8 +69,8 @@ describe('ExceptionHelper tests', () => { const exception = ExceptionHelper.generateException(Error, errorDef, innerError) assert.strictEqual(exception.message, 'Test error') - assert.strictEqual((exception as any).code, -100000) - assert.strictEqual((exception as any).innerException, innerError) + assert.strictEqual(exception.code, -100000) + assert.strictEqual(exception.innerException, innerError) }) it('should format message with single parameter', () => { @@ -134,13 +134,11 @@ describe('ExceptionHelper tests', () => { () => { throw ExceptionHelper.generateException(ReferenceError, errorDef) }, - (err: any) => { + (err: ReferenceError & AgentError) => { return ( err instanceof ReferenceError && err.message === 'Test error' && - // @ts-expect-error err.code === -100000 && - // @ts-expect-error err.helpLink === 'https://aka.ms/test' ) } diff --git a/packages/agents-hosting-storage-cosmos/src/index.ts b/packages/agents-hosting-storage-cosmos/src/index.ts index 3a19b772..fe374287 100644 --- a/packages/agents-hosting-storage-cosmos/src/index.ts +++ b/packages/agents-hosting-storage-cosmos/src/index.ts @@ -4,4 +4,4 @@ export { CosmosDbPartitionedStorage } from './cosmosDbPartitionedStorage' export * from './cosmosDbPartitionedStorageOptions' export { Errors } from './errorHelper' -export { AgentErrorDefinition, ExceptionHelper } from '@microsoft/agents-activity' +export { AgentErrorDefinition, AgentError, ExceptionHelper } from '@microsoft/agents-activity' From 97bfdee4d2368c37c0cb3427db27772d325ffc11 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 19 Nov 2025 17:49:43 +0000 Subject: [PATCH 14/18] Format exception message as [CODE] - [message] - [helplink] Co-authored-by: cleemullins <1165321+cleemullins@users.noreply.github.com> --- packages/agents-activity/src/exceptionHelper.ts | 8 ++++++-- .../agents-activity/test/exceptionHelper.test.ts | 14 +++++++------- 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/packages/agents-activity/src/exceptionHelper.ts b/packages/agents-activity/src/exceptionHelper.ts index 7410d030..ea7b09ca 100644 --- a/packages/agents-activity/src/exceptionHelper.ts +++ b/packages/agents-activity/src/exceptionHelper.ts @@ -49,6 +49,7 @@ export interface AgentError extends Error { export class ExceptionHelper { /** * Generates a typed exception with error code and help link. + * The message format is: [CODE] - [message] - [helplink] * @param ErrorType The constructor of the error type to create * @param errorDefinition The error definition containing code, description, and help link * @param innerException Optional inner exception @@ -62,13 +63,16 @@ export class ExceptionHelper { params?: { [key: string]: string } ): T & AgentError { // Format the message with parameters if provided - let message = errorDefinition.description + let description = errorDefinition.description if (params) { Object.keys(params).forEach((key) => { - message = message.replace(`{${key}}`, params[key]) + description = description.replace(`{${key}}`, params[key]) }) } + // Format the full message as: [CODE] - [message] - [helplink] + const message = `[${errorDefinition.code}] - ${description} - ${errorDefinition.helplink}` + // Create the exception const exception = new ErrorType(message) as T & AgentError diff --git a/packages/agents-activity/test/exceptionHelper.test.ts b/packages/agents-activity/test/exceptionHelper.test.ts index cc7ffdd3..80564c94 100644 --- a/packages/agents-activity/test/exceptionHelper.test.ts +++ b/packages/agents-activity/test/exceptionHelper.test.ts @@ -38,7 +38,7 @@ describe('ExceptionHelper tests', () => { const exception = ExceptionHelper.generateException(Error, errorDef) - assert.strictEqual(exception.message, 'Test error') + assert.strictEqual(exception.message, '[-100000] - Test error - https://aka.ms/test') assert.strictEqual(exception.code, -100000) assert.strictEqual(exception.helpLink, 'https://aka.ms/test') }) @@ -53,7 +53,7 @@ describe('ExceptionHelper tests', () => { const exception = ExceptionHelper.generateException(ReferenceError, errorDef) assert.ok(exception instanceof ReferenceError) - assert.strictEqual(exception.message, 'Reference error test') + assert.strictEqual(exception.message, '[-100000] - Reference error test - https://aka.ms/test') assert.strictEqual(exception.code, -100000) assert.strictEqual(exception.helpLink, 'https://aka.ms/test') }) @@ -68,7 +68,7 @@ describe('ExceptionHelper tests', () => { const exception = ExceptionHelper.generateException(Error, errorDef, innerError) - assert.strictEqual(exception.message, 'Test error') + assert.strictEqual(exception.message, '[-100000] - Test error - https://aka.ms/test') assert.strictEqual(exception.code, -100000) assert.strictEqual(exception.innerException, innerError) }) @@ -84,7 +84,7 @@ describe('ExceptionHelper tests', () => { assert.strictEqual( exception.message, - 'Cannot use invalid Row Key characters: test*suffix in keySuffix' + '[-100006] - Cannot use invalid Row Key characters: test*suffix in keySuffix - https://aka.ms/test' ) }) @@ -107,7 +107,7 @@ describe('ExceptionHelper tests', () => { assert.strictEqual( exception.message, - 'Custom Partition Key Paths are not supported. myContainer has a custom Partition Key Path of /customPath.' + '[-100009] - Custom Partition Key Paths are not supported. myContainer has a custom Partition Key Path of /customPath. - https://aka.ms/test' ) }) @@ -120,7 +120,7 @@ describe('ExceptionHelper tests', () => { const exception = ExceptionHelper.generateException(Error, errorDef) - assert.strictEqual(exception.message, 'Simple error message') + assert.strictEqual(exception.message, '[-100000] - Simple error message - https://aka.ms/test') }) it('should throw and catch exception correctly', () => { @@ -137,7 +137,7 @@ describe('ExceptionHelper tests', () => { (err: ReferenceError & AgentError) => { return ( err instanceof ReferenceError && - err.message === 'Test error' && + err.message === '[-100000] - Test error - https://aka.ms/test' && err.code === -100000 && err.helpLink === 'https://aka.ms/test' ) From df76e610150eccf6c47b7b2d142043250dd74b82 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 19 Nov 2025 18:36:42 +0000 Subject: [PATCH 15/18] Refactor to use tokenized {errorCode} in helplinks to avoid duplication Co-authored-by: cleemullins <1165321+cleemullins@users.noreply.github.com> --- .../agents-activity/src/exceptionHelper.ts | 7 +++- .../test/exceptionHelper.test.ts | 28 +++++++++++++ .../src/errorHelper.ts | 40 +++++++++---------- .../test/errorHelper.test.ts | 16 ++++---- 4 files changed, 61 insertions(+), 30 deletions(-) diff --git a/packages/agents-activity/src/exceptionHelper.ts b/packages/agents-activity/src/exceptionHelper.ts index ea7b09ca..dfe974b4 100644 --- a/packages/agents-activity/src/exceptionHelper.ts +++ b/packages/agents-activity/src/exceptionHelper.ts @@ -70,15 +70,18 @@ export class ExceptionHelper { }) } + // Replace {errorCode} token in helplink with the actual error code + const helplink = errorDefinition.helplink.replace('{errorCode}', errorDefinition.code.toString()) + // Format the full message as: [CODE] - [message] - [helplink] - const message = `[${errorDefinition.code}] - ${description} - ${errorDefinition.helplink}` + const message = `[${errorDefinition.code}] - ${description} - ${helplink}` // Create the exception const exception = new ErrorType(message) as T & AgentError // Set error code and help link as custom properties exception.code = errorDefinition.code - exception.helpLink = errorDefinition.helplink + exception.helpLink = helplink // Store inner exception as a custom property if provided if (innerException) { diff --git a/packages/agents-activity/test/exceptionHelper.test.ts b/packages/agents-activity/test/exceptionHelper.test.ts index 80564c94..b0c2b474 100644 --- a/packages/agents-activity/test/exceptionHelper.test.ts +++ b/packages/agents-activity/test/exceptionHelper.test.ts @@ -144,4 +144,32 @@ describe('ExceptionHelper tests', () => { } ) }) + + it('should replace {errorCode} token in helplink', () => { + const errorDef: AgentErrorDefinition = { + code: -100000, + description: 'Test error with tokenized helplink', + helplink: 'https://aka.ms/M365AgentsErrorCodes/#{errorCode}' + } + + const exception = ExceptionHelper.generateException(Error, errorDef) + + assert.strictEqual(exception.message, '[-100000] - Test error with tokenized helplink - https://aka.ms/M365AgentsErrorCodes/#-100000') + assert.strictEqual(exception.code, -100000) + assert.strictEqual(exception.helpLink, 'https://aka.ms/M365AgentsErrorCodes/#-100000') + }) + + it('should not modify helplink if {errorCode} token is not present', () => { + const errorDef: AgentErrorDefinition = { + code: -100000, + description: 'Test error without token', + helplink: 'https://aka.ms/custom-link' + } + + const exception = ExceptionHelper.generateException(Error, errorDef) + + assert.strictEqual(exception.message, '[-100000] - Test error without token - https://aka.ms/custom-link') + assert.strictEqual(exception.code, -100000) + assert.strictEqual(exception.helpLink, 'https://aka.ms/custom-link') + }) }) diff --git a/packages/agents-hosting-storage-cosmos/src/errorHelper.ts b/packages/agents-hosting-storage-cosmos/src/errorHelper.ts index de914b0e..ed496df2 100644 --- a/packages/agents-hosting-storage-cosmos/src/errorHelper.ts +++ b/packages/agents-hosting-storage-cosmos/src/errorHelper.ts @@ -25,7 +25,7 @@ export const Errors: { [key: string]: AgentErrorDefinition } = { MissingCosmosDbStorageOptions: { code: -100000, description: 'CosmosDbPartitionedStorageOptions is required.', - helplink: 'https://aka.ms/M365AgentsErrorCodes/#-100000' + helplink: 'https://aka.ms/M365AgentsErrorCodes/#{errorCode}' }, /** @@ -34,7 +34,7 @@ export const Errors: { [key: string]: AgentErrorDefinition } = { MissingCosmosEndpoint: { code: -100001, description: 'endpoint in cosmosClientOptions is required.', - helplink: 'https://aka.ms/M365AgentsErrorCodes/#-100001' + helplink: 'https://aka.ms/M365AgentsErrorCodes/#{errorCode}' }, /** @@ -43,7 +43,7 @@ export const Errors: { [key: string]: AgentErrorDefinition } = { MissingCosmosCredentials: { code: -100002, description: 'key or tokenProvider in cosmosClientOptions is required.', - helplink: 'https://aka.ms/M365AgentsErrorCodes/#-100002' + helplink: 'https://aka.ms/M365AgentsErrorCodes/#{errorCode}' }, /** @@ -52,7 +52,7 @@ export const Errors: { [key: string]: AgentErrorDefinition } = { MissingDatabaseId: { code: -100003, description: 'databaseId for CosmosDB is required.', - helplink: 'https://aka.ms/M365AgentsErrorCodes/#-100003' + helplink: 'https://aka.ms/M365AgentsErrorCodes/#{errorCode}' }, /** @@ -61,7 +61,7 @@ export const Errors: { [key: string]: AgentErrorDefinition } = { MissingContainerId: { code: -100004, description: 'containerId for CosmosDB is required.', - helplink: 'https://aka.ms/M365AgentsErrorCodes/#-100004' + helplink: 'https://aka.ms/M365AgentsErrorCodes/#{errorCode}' }, /** @@ -70,7 +70,7 @@ export const Errors: { [key: string]: AgentErrorDefinition } = { InvalidCompatibilityModeWithKeySuffix: { code: -100005, description: 'compatibilityMode cannot be true while using a keySuffix.', - helplink: 'https://aka.ms/M365AgentsErrorCodes/#-100005' + helplink: 'https://aka.ms/M365AgentsErrorCodes/#{errorCode}' }, /** @@ -79,7 +79,7 @@ export const Errors: { [key: string]: AgentErrorDefinition } = { InvalidKeySuffixCharacters: { code: -100006, description: 'Cannot use invalid Row Key characters: {keySuffix} in keySuffix', - helplink: 'https://aka.ms/M365AgentsErrorCodes/#-100006' + helplink: 'https://aka.ms/M365AgentsErrorCodes/#{errorCode}' }, /** @@ -88,7 +88,7 @@ export const Errors: { [key: string]: AgentErrorDefinition } = { MissingReadKeys: { code: -100007, description: 'Keys are required when reading.', - helplink: 'https://aka.ms/M365AgentsErrorCodes/#-100007' + helplink: 'https://aka.ms/M365AgentsErrorCodes/#{errorCode}' }, /** @@ -97,7 +97,7 @@ export const Errors: { [key: string]: AgentErrorDefinition } = { MissingWriteChanges: { code: -100008, description: 'Changes are required when writing.', - helplink: 'https://aka.ms/M365AgentsErrorCodes/#-100008' + helplink: 'https://aka.ms/M365AgentsErrorCodes/#{errorCode}' }, /** @@ -106,7 +106,7 @@ export const Errors: { [key: string]: AgentErrorDefinition } = { UnsupportedCustomPartitionKeyPath: { code: -100009, description: 'Custom Partition Key Paths are not supported. {containerId} has a custom Partition Key Path of {partitionKeyPath}.', - helplink: 'https://aka.ms/M365AgentsErrorCodes/#-100009' + helplink: 'https://aka.ms/M365AgentsErrorCodes/#{errorCode}' }, /** @@ -115,7 +115,7 @@ export const Errors: { [key: string]: AgentErrorDefinition } = { ContainerNotFound: { code: -100010, description: 'Container {containerId} not found.', - helplink: 'https://aka.ms/M365AgentsErrorCodes/#-100010' + helplink: 'https://aka.ms/M365AgentsErrorCodes/#{errorCode}' }, /** @@ -124,7 +124,7 @@ export const Errors: { [key: string]: AgentErrorDefinition } = { MissingKeyParameter: { code: -100011, description: "The 'key' parameter is required.", - helplink: 'https://aka.ms/M365AgentsErrorCodes/#-100011' + helplink: 'https://aka.ms/M365AgentsErrorCodes/#{errorCode}' }, /** @@ -133,7 +133,7 @@ export const Errors: { [key: string]: AgentErrorDefinition } = { ContainerReadNotFound: { code: -100012, description: 'Not Found', - helplink: 'https://aka.ms/M365AgentsErrorCodes/#-100012' + helplink: 'https://aka.ms/M365AgentsErrorCodes/#{errorCode}' }, /** @@ -142,7 +142,7 @@ export const Errors: { [key: string]: AgentErrorDefinition } = { ContainerReadBadRequest: { code: -100013, description: 'Error reading from container. You might be attempting to read from a non-partitioned container or a container that does not use \'/id\' as the partitionKeyPath', - helplink: 'https://aka.ms/M365AgentsErrorCodes/#-100013' + helplink: 'https://aka.ms/M365AgentsErrorCodes/#{errorCode}' }, /** @@ -151,7 +151,7 @@ export const Errors: { [key: string]: AgentErrorDefinition } = { ContainerReadError: { code: -100014, description: 'Error reading from container', - helplink: 'https://aka.ms/M365AgentsErrorCodes/#-100014' + helplink: 'https://aka.ms/M365AgentsErrorCodes/#{errorCode}' }, /** @@ -160,7 +160,7 @@ export const Errors: { [key: string]: AgentErrorDefinition } = { DocumentUpsertError: { code: -100015, description: 'Error upserting document', - helplink: 'https://aka.ms/M365AgentsErrorCodes/#-100015' + helplink: 'https://aka.ms/M365AgentsErrorCodes/#{errorCode}' }, /** @@ -169,7 +169,7 @@ export const Errors: { [key: string]: AgentErrorDefinition } = { DocumentDeleteNotFound: { code: -100016, description: 'Not Found', - helplink: 'https://aka.ms/M365AgentsErrorCodes/#-100016' + helplink: 'https://aka.ms/M365AgentsErrorCodes/#{errorCode}' }, /** @@ -178,7 +178,7 @@ export const Errors: { [key: string]: AgentErrorDefinition } = { DocumentDeleteError: { code: -100017, description: 'Unable to delete document', - helplink: 'https://aka.ms/M365AgentsErrorCodes/#-100017' + helplink: 'https://aka.ms/M365AgentsErrorCodes/#{errorCode}' }, /** @@ -187,7 +187,7 @@ export const Errors: { [key: string]: AgentErrorDefinition } = { InitializationError: { code: -100018, description: 'Failed to initialize Cosmos DB database/container: {databaseId}/{containerId}', - helplink: 'https://aka.ms/M365AgentsErrorCodes/#-100018' + helplink: 'https://aka.ms/M365AgentsErrorCodes/#{errorCode}' }, /** @@ -196,6 +196,6 @@ export const Errors: { [key: string]: AgentErrorDefinition } = { MaxNestingDepthExceeded: { code: -100019, description: 'Maximum nesting depth of {maxDepth} exceeded. {additionalMessage}', - helplink: 'https://aka.ms/M365AgentsErrorCodes/#-100019' + helplink: 'https://aka.ms/M365AgentsErrorCodes/#{errorCode}' } } diff --git a/packages/agents-hosting-storage-cosmos/test/errorHelper.test.ts b/packages/agents-hosting-storage-cosmos/test/errorHelper.test.ts index 7c93312a..ba3f5b80 100644 --- a/packages/agents-hosting-storage-cosmos/test/errorHelper.test.ts +++ b/packages/agents-hosting-storage-cosmos/test/errorHelper.test.ts @@ -9,7 +9,7 @@ describe('Errors tests', () => { assert.strictEqual(error.code, -100000) assert.strictEqual(error.description, 'CosmosDbPartitionedStorageOptions is required.') - assert.strictEqual(error.helplink, 'https://aka.ms/M365AgentsErrorCodes/#-100000') + assert.strictEqual(error.helplink, 'https://aka.ms/M365AgentsErrorCodes/#{errorCode}') }) it('should have MissingCosmosEndpoint error definition', () => { @@ -17,7 +17,7 @@ describe('Errors tests', () => { assert.strictEqual(error.code, -100001) assert.strictEqual(error.description, 'endpoint in cosmosClientOptions is required.') - assert.strictEqual(error.helplink, 'https://aka.ms/M365AgentsErrorCodes/#-100001') + assert.strictEqual(error.helplink, 'https://aka.ms/M365AgentsErrorCodes/#{errorCode}') }) it('should have MissingCosmosCredentials error definition', () => { @@ -25,7 +25,7 @@ describe('Errors tests', () => { assert.strictEqual(error.code, -100002) assert.strictEqual(error.description, 'key or tokenProvider in cosmosClientOptions is required.') - assert.strictEqual(error.helplink, 'https://aka.ms/M365AgentsErrorCodes/#-100002') + assert.strictEqual(error.helplink, 'https://aka.ms/M365AgentsErrorCodes/#{errorCode}') }) it('should have all error codes in the correct range', () => { @@ -52,19 +52,19 @@ describe('Errors tests', () => { assert.strictEqual(codes.length, uniqueCodes.size, 'All error codes should be unique') }) - it('should have help links with correct format', () => { + it('should have help links with tokenized format', () => { const errorDefinitions = Object.values(Errors).filter( val => val && typeof val === 'object' && 'code' in val && 'description' in val && 'helplink' in val ) as AgentErrorDefinition[] errorDefinitions.forEach(errorDef => { assert.ok( - errorDef.helplink.startsWith('https://aka.ms/M365AgentsErrorCodes/#'), - `Help link should start with correct URL: ${errorDef.helplink}` + errorDef.helplink.includes('{errorCode}'), + `Help link should contain {errorCode} token: ${errorDef.helplink}` ) assert.ok( - errorDef.helplink.endsWith(errorDef.code.toString()), - `Help link should end with error code: ${errorDef.helplink}` + errorDef.helplink.startsWith('https://aka.ms/M365AgentsErrorCodes/#'), + `Help link should start with correct URL: ${errorDef.helplink}` ) }) }) From 25eb946457561f5eca442698b77517af5a8101e1 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 19 Nov 2025 21:28:15 +0000 Subject: [PATCH 16/18] Initial plan From 8ed1f18b5646d0930f66b83d312c3d98bd71260c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 19 Nov 2025 21:35:28 +0000 Subject: [PATCH 17/18] Improve error message quality in errorHelper.ts Co-authored-by: cleemullins <1165321+cleemullins@users.noreply.github.com> --- .../src/errorHelper.ts | 40 +++++++++---------- .../test/errorHelper.test.ts | 6 +-- 2 files changed, 23 insertions(+), 23 deletions(-) diff --git a/packages/agents-hosting-storage-cosmos/src/errorHelper.ts b/packages/agents-hosting-storage-cosmos/src/errorHelper.ts index ed496df2..c7a382a4 100644 --- a/packages/agents-hosting-storage-cosmos/src/errorHelper.ts +++ b/packages/agents-hosting-storage-cosmos/src/errorHelper.ts @@ -24,7 +24,7 @@ export const Errors: { [key: string]: AgentErrorDefinition } = { */ MissingCosmosDbStorageOptions: { code: -100000, - description: 'CosmosDbPartitionedStorageOptions is required.', + description: 'CosmosDbPartitionedStorageOptions is required. Provide a valid configuration object with cosmosClientOptions, databaseId, and containerId properties when initializing CosmosDbPartitionedStorage.', helplink: 'https://aka.ms/M365AgentsErrorCodes/#{errorCode}' }, @@ -33,7 +33,7 @@ export const Errors: { [key: string]: AgentErrorDefinition } = { */ MissingCosmosEndpoint: { code: -100001, - description: 'endpoint in cosmosClientOptions is required.', + description: 'The endpoint property in cosmosClientOptions is required. Provide your Cosmos DB account endpoint URL (e.g., https://your-account.documents.azure.com:443/).', helplink: 'https://aka.ms/M365AgentsErrorCodes/#{errorCode}' }, @@ -42,7 +42,7 @@ export const Errors: { [key: string]: AgentErrorDefinition } = { */ MissingCosmosCredentials: { code: -100002, - description: 'key or tokenProvider in cosmosClientOptions is required.', + description: 'Authentication credentials are required in cosmosClientOptions. Provide either a key (connection key) or tokenProvider (for token-based authentication).', helplink: 'https://aka.ms/M365AgentsErrorCodes/#{errorCode}' }, @@ -51,7 +51,7 @@ export const Errors: { [key: string]: AgentErrorDefinition } = { */ MissingDatabaseId: { code: -100003, - description: 'databaseId for CosmosDB is required.', + description: 'The databaseId property is required in CosmosDbPartitionedStorageOptions. Specify the name of the Cosmos DB database to use for storage.', helplink: 'https://aka.ms/M365AgentsErrorCodes/#{errorCode}' }, @@ -60,7 +60,7 @@ export const Errors: { [key: string]: AgentErrorDefinition } = { */ MissingContainerId: { code: -100004, - description: 'containerId for CosmosDB is required.', + description: 'The containerId property is required in CosmosDbPartitionedStorageOptions. Specify the name of the Cosmos DB container to use for storage.', helplink: 'https://aka.ms/M365AgentsErrorCodes/#{errorCode}' }, @@ -69,7 +69,7 @@ export const Errors: { [key: string]: AgentErrorDefinition } = { */ InvalidCompatibilityModeWithKeySuffix: { code: -100005, - description: 'compatibilityMode cannot be true while using a keySuffix.', + description: 'Configuration conflict: compatibilityMode cannot be enabled (true) when using a keySuffix. Either disable compatibilityMode or remove the keySuffix from your configuration.', helplink: 'https://aka.ms/M365AgentsErrorCodes/#{errorCode}' }, @@ -78,7 +78,7 @@ export const Errors: { [key: string]: AgentErrorDefinition } = { */ InvalidKeySuffixCharacters: { code: -100006, - description: 'Cannot use invalid Row Key characters: {keySuffix} in keySuffix', + description: 'The keySuffix "{keySuffix}" contains invalid characters. Keys cannot contain: \\, ?, /, #, tab, newline, carriage return, or *. Please remove these characters from your keySuffix configuration.', helplink: 'https://aka.ms/M365AgentsErrorCodes/#{errorCode}' }, @@ -87,7 +87,7 @@ export const Errors: { [key: string]: AgentErrorDefinition } = { */ MissingReadKeys: { code: -100007, - description: 'Keys are required when reading.', + description: 'The keys parameter is required when calling read(). Provide an array of storage keys to read.', helplink: 'https://aka.ms/M365AgentsErrorCodes/#{errorCode}' }, @@ -96,7 +96,7 @@ export const Errors: { [key: string]: AgentErrorDefinition } = { */ MissingWriteChanges: { code: -100008, - description: 'Changes are required when writing.', + description: 'The changes parameter is required when calling write(). Provide a StoreItems object containing the data to write.', helplink: 'https://aka.ms/M365AgentsErrorCodes/#{errorCode}' }, @@ -105,7 +105,7 @@ export const Errors: { [key: string]: AgentErrorDefinition } = { */ UnsupportedCustomPartitionKeyPath: { code: -100009, - description: 'Custom Partition Key Paths are not supported. {containerId} has a custom Partition Key Path of {partitionKeyPath}.', + description: 'The container "{containerId}" uses a custom partition key path "{partitionKeyPath}", which is not supported. This storage implementation requires containers to use either "/id" as the partition key path or no partition key (for compatibility mode). Create a new container with the correct partition key configuration.', helplink: 'https://aka.ms/M365AgentsErrorCodes/#{errorCode}' }, @@ -114,7 +114,7 @@ export const Errors: { [key: string]: AgentErrorDefinition } = { */ ContainerNotFound: { code: -100010, - description: 'Container {containerId} not found.', + description: 'The Cosmos DB container "{containerId}" was not found and could not be created. Verify the container exists or ensure the client has permissions to create it. If using compatibilityMode, the container must already exist.', helplink: 'https://aka.ms/M365AgentsErrorCodes/#{errorCode}' }, @@ -123,7 +123,7 @@ export const Errors: { [key: string]: AgentErrorDefinition } = { */ MissingKeyParameter: { code: -100011, - description: "The 'key' parameter is required.", + description: 'The key parameter is required and cannot be null or empty. Provide a valid storage key string.', helplink: 'https://aka.ms/M365AgentsErrorCodes/#{errorCode}' }, @@ -132,7 +132,7 @@ export const Errors: { [key: string]: AgentErrorDefinition } = { */ ContainerReadNotFound: { code: -100012, - description: 'Not Found', + description: 'The requested item was not found in the Cosmos DB container. This is typically not an error during read operations.', helplink: 'https://aka.ms/M365AgentsErrorCodes/#{errorCode}' }, @@ -141,7 +141,7 @@ export const Errors: { [key: string]: AgentErrorDefinition } = { */ ContainerReadBadRequest: { code: -100013, - description: 'Error reading from container. You might be attempting to read from a non-partitioned container or a container that does not use \'/id\' as the partitionKeyPath', + description: 'Bad request error while reading from the Cosmos DB container. This usually indicates a configuration mismatch: the container may be non-partitioned or uses a partition key path other than "/id". Verify your container\'s partition key configuration matches the storage implementation requirements.', helplink: 'https://aka.ms/M365AgentsErrorCodes/#{errorCode}' }, @@ -150,7 +150,7 @@ export const Errors: { [key: string]: AgentErrorDefinition } = { */ ContainerReadError: { code: -100014, - description: 'Error reading from container', + description: 'An unexpected error occurred while reading from the Cosmos DB container. Check the inner exception for details about the specific error.', helplink: 'https://aka.ms/M365AgentsErrorCodes/#{errorCode}' }, @@ -159,7 +159,7 @@ export const Errors: { [key: string]: AgentErrorDefinition } = { */ DocumentUpsertError: { code: -100015, - description: 'Error upserting document', + description: 'Failed to upsert (insert or update) a document in the Cosmos DB container. This may be due to concurrency conflicts, permission issues, or data size limits. Check the inner exception for specific details.', helplink: 'https://aka.ms/M365AgentsErrorCodes/#{errorCode}' }, @@ -168,7 +168,7 @@ export const Errors: { [key: string]: AgentErrorDefinition } = { */ DocumentDeleteNotFound: { code: -100016, - description: 'Not Found', + description: 'The document to delete was not found in the Cosmos DB container. This is typically not an error during delete operations.', helplink: 'https://aka.ms/M365AgentsErrorCodes/#{errorCode}' }, @@ -177,7 +177,7 @@ export const Errors: { [key: string]: AgentErrorDefinition } = { */ DocumentDeleteError: { code: -100017, - description: 'Unable to delete document', + description: 'Failed to delete a document from the Cosmos DB container. This may be due to permission issues or network problems. Check the inner exception for specific details.', helplink: 'https://aka.ms/M365AgentsErrorCodes/#{errorCode}' }, @@ -186,7 +186,7 @@ export const Errors: { [key: string]: AgentErrorDefinition } = { */ InitializationError: { code: -100018, - description: 'Failed to initialize Cosmos DB database/container: {databaseId}/{containerId}', + description: 'Failed to initialize the Cosmos DB database "{databaseId}" and container "{containerId}". Verify your connection credentials, ensure the account exists, and check that the client has appropriate permissions. See the inner exception for specific error details.', helplink: 'https://aka.ms/M365AgentsErrorCodes/#{errorCode}' }, @@ -195,7 +195,7 @@ export const Errors: { [key: string]: AgentErrorDefinition } = { */ MaxNestingDepthExceeded: { code: -100019, - description: 'Maximum nesting depth of {maxDepth} exceeded. {additionalMessage}', + description: 'The data structure exceeds the maximum nesting depth of {maxDepth} levels. {additionalMessage} This limit is imposed to prevent stack overflow errors when storing deeply nested objects in Cosmos DB.', helplink: 'https://aka.ms/M365AgentsErrorCodes/#{errorCode}' } } diff --git a/packages/agents-hosting-storage-cosmos/test/errorHelper.test.ts b/packages/agents-hosting-storage-cosmos/test/errorHelper.test.ts index ba3f5b80..4c755506 100644 --- a/packages/agents-hosting-storage-cosmos/test/errorHelper.test.ts +++ b/packages/agents-hosting-storage-cosmos/test/errorHelper.test.ts @@ -8,7 +8,7 @@ describe('Errors tests', () => { const error = Errors.MissingCosmosDbStorageOptions assert.strictEqual(error.code, -100000) - assert.strictEqual(error.description, 'CosmosDbPartitionedStorageOptions is required.') + assert.strictEqual(error.description, 'CosmosDbPartitionedStorageOptions is required. Provide a valid configuration object with cosmosClientOptions, databaseId, and containerId properties when initializing CosmosDbPartitionedStorage.') assert.strictEqual(error.helplink, 'https://aka.ms/M365AgentsErrorCodes/#{errorCode}') }) @@ -16,7 +16,7 @@ describe('Errors tests', () => { const error = Errors.MissingCosmosEndpoint assert.strictEqual(error.code, -100001) - assert.strictEqual(error.description, 'endpoint in cosmosClientOptions is required.') + assert.strictEqual(error.description, 'The endpoint property in cosmosClientOptions is required. Provide your Cosmos DB account endpoint URL (e.g., https://your-account.documents.azure.com:443/).') assert.strictEqual(error.helplink, 'https://aka.ms/M365AgentsErrorCodes/#{errorCode}') }) @@ -24,7 +24,7 @@ describe('Errors tests', () => { const error = Errors.MissingCosmosCredentials assert.strictEqual(error.code, -100002) - assert.strictEqual(error.description, 'key or tokenProvider in cosmosClientOptions is required.') + assert.strictEqual(error.description, 'Authentication credentials are required in cosmosClientOptions. Provide either a key (connection key) or tokenProvider (for token-based authentication).') assert.strictEqual(error.helplink, 'https://aka.ms/M365AgentsErrorCodes/#{errorCode}') }) From deb5ffedb7f7a0cf36a25b7b92de962d021494c6 Mon Sep 17 00:00:00 2001 From: Chris Mullins Date: Wed, 19 Nov 2025 16:42:31 -0800 Subject: [PATCH 18/18] Update packages/agents-hosting-storage-cosmos/src/errorHelper.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- packages/agents-hosting-storage-cosmos/src/errorHelper.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/agents-hosting-storage-cosmos/src/errorHelper.ts b/packages/agents-hosting-storage-cosmos/src/errorHelper.ts index c0896b10..c7a382a4 100644 --- a/packages/agents-hosting-storage-cosmos/src/errorHelper.ts +++ b/packages/agents-hosting-storage-cosmos/src/errorHelper.ts @@ -142,7 +142,6 @@ export const Errors: { [key: string]: AgentErrorDefinition } = { ContainerReadBadRequest: { code: -100013, description: 'Bad request error while reading from the Cosmos DB container. This usually indicates a configuration mismatch: the container may be non-partitioned or uses a partition key path other than "/id". Verify your container\'s partition key configuration matches the storage implementation requirements.', - description: 'Error reading from container. You might be attempting to read from a non-partitioned container or a container that does not use \'/id\' as the partitionKeyPath', helplink: 'https://aka.ms/M365AgentsErrorCodes/#{errorCode}' },