diff --git a/packages/agents-activity/src/activity.ts b/packages/agents-activity/src/activity.ts index 601b5cb5..a3e2f258 100644 --- a/packages/agents-activity/src/activity.ts +++ b/packages/agents-activity/src/activity.ts @@ -26,6 +26,8 @@ import { MessageReaction, messageReactionZodSchema } from './messageReaction' import { TextFormatTypes, textFormatTypesZodSchema } from './textFormatTypes' import { TextHighlight, textHighlightZodSchema } from './textHighlight' import { RoleTypes } from './conversation/roleTypes' +import { Errors } from './errorHelper' +import { ExceptionHelper } from './exceptionHelper' /** * Zod schema for validating an Activity object. @@ -316,13 +318,13 @@ export class Activity { */ constructor (t: ActivityTypes | string) { if (t === undefined) { - throw new Error('Invalid ActivityType: undefined') + throw ExceptionHelper.generateException(Error, Errors.InvalidActivityTypeUndefined) } if (t === null) { - throw new Error('Invalid ActivityType: null') + throw ExceptionHelper.generateException(Error, Errors.InvalidActivityTypeNull) } if ((typeof t === 'string') && (t.length === 0)) { - throw new Error('Invalid ActivityType: empty string') + throw ExceptionHelper.generateException(Error, Errors.InvalidActivityTypeEmptyString) } this.type = t @@ -382,7 +384,7 @@ export class Activity { // if they passed in a value but the channel is blank, this is invalid if (value && !channel) { - throw new Error(`Invalid channelId ${value}. Found subChannel but no main channel.`) + throw ExceptionHelper.generateException(Error, Errors.InvalidChannelIdSubChannelOnly, undefined, { channelId: value }) } this._channelId = channel if (subChannel) { @@ -418,7 +420,7 @@ export class Activity { */ set channelIdSubChannel (value) { if (!this._channelId) { - throw new Error('Primary channel must be set before setting subChannel') + throw ExceptionHelper.generateException(Error, Errors.PrimaryChannelNotSet) } this.channelId = `${this._channelId}${value ? `:${value}` : ''}` } @@ -467,13 +469,13 @@ export class Activity { */ public getConversationReference (): ConversationReference { if (this.recipient === null || this.recipient === undefined) { - throw new Error('Activity Recipient undefined') + throw ExceptionHelper.generateException(Error, Errors.ActivityRecipientUndefined) } if (this.conversation === null || this.conversation === undefined) { - throw new Error('Activity Conversation undefined') + throw ExceptionHelper.generateException(Error, Errors.ActivityConversationUndefined) } if (this.channelId === null || this.channelId === undefined) { - throw new Error('Activity ChannelId undefined') + throw ExceptionHelper.generateException(Error, Errors.ActivityChannelIdUndefined) } return { diff --git a/packages/agents-activity/src/errorHelper.ts b/packages/agents-activity/src/errorHelper.ts new file mode 100644 index 00000000..9a156eed --- /dev/null +++ b/packages/agents-activity/src/errorHelper.ts @@ -0,0 +1,93 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { AgentErrorDefinition } from './exceptionHelper' + +/** + * Error definitions for the Activity system. + * This contains localized error codes for the Activity subsystem of the AgentSDK. + * + * Each error definition includes an error code (starting from -110000), a description, and a help link + * pointing to an AKA link to get help for the given error. + * + * Usage example: + * ``` + * throw ExceptionHelper.generateException( + * Error, + * Errors.InvalidActivityTypeUndefined + * ); + * ``` + */ +export const Errors: { [key: string]: AgentErrorDefinition } = { + /** + * Error thrown when ActivityType is undefined. + */ + InvalidActivityTypeUndefined: { + code: -110000, + description: 'Invalid ActivityType: undefined', + helplink: 'https://aka.ms/M365AgentsErrorCodes/#{errorCode}' + }, + + /** + * Error thrown when ActivityType is null. + */ + InvalidActivityTypeNull: { + code: -110001, + description: 'Invalid ActivityType: null', + helplink: 'https://aka.ms/M365AgentsErrorCodes/#{errorCode}' + }, + + /** + * Error thrown when ActivityType is an empty string. + */ + InvalidActivityTypeEmptyString: { + code: -110002, + description: 'Invalid ActivityType: empty string', + helplink: 'https://aka.ms/M365AgentsErrorCodes/#{errorCode}' + }, + + /** + * Error thrown when channelId contains a subChannel but no main channel. + */ + InvalidChannelIdSubChannelOnly: { + code: -110003, + description: 'Invalid channelId {channelId}. Found subChannel but no main channel.', + helplink: 'https://aka.ms/M365AgentsErrorCodes/#{errorCode}' + }, + + /** + * Error thrown when attempting to set subChannel before setting primary channel. + */ + PrimaryChannelNotSet: { + code: -110004, + description: 'Primary channel must be set before setting subChannel', + helplink: 'https://aka.ms/M365AgentsErrorCodes/#{errorCode}' + }, + + /** + * Error thrown when Activity Recipient is undefined. + */ + ActivityRecipientUndefined: { + code: -110005, + description: 'Activity Recipient undefined', + helplink: 'https://aka.ms/M365AgentsErrorCodes/#{errorCode}' + }, + + /** + * Error thrown when Activity Conversation is undefined. + */ + ActivityConversationUndefined: { + code: -110006, + description: 'Activity Conversation undefined', + helplink: 'https://aka.ms/M365AgentsErrorCodes/#{errorCode}' + }, + + /** + * Error thrown when Activity ChannelId is undefined. + */ + ActivityChannelIdUndefined: { + code: -110007, + description: 'Activity ChannelId undefined', + helplink: 'https://aka.ms/M365AgentsErrorCodes/#{errorCode}' + } +} diff --git a/packages/agents-activity/src/index.ts b/packages/agents-activity/src/index.ts index 0126453f..620696d0 100644 --- a/packages/agents-activity/src/index.ts +++ b/packages/agents-activity/src/index.ts @@ -50,3 +50,4 @@ export { ActivityTreatments } from './activityTreatments' export { debug, Logger } from './logger' export { AgentErrorDefinition, AgentError, ExceptionHelper } from './exceptionHelper' +export { Errors } from './errorHelper' diff --git a/packages/agents-activity/test/errorHelper.test.ts b/packages/agents-activity/test/errorHelper.test.ts new file mode 100644 index 00000000..6de67786 --- /dev/null +++ b/packages/agents-activity/test/errorHelper.test.ts @@ -0,0 +1,81 @@ +import assert from 'assert' +import { describe, it } from 'node:test' +import { AgentErrorDefinition } from '../src/exceptionHelper' +import { Errors } from '../src/errorHelper' + +describe('Activity Errors tests', () => { + it('should have InvalidActivityTypeUndefined error definition', () => { + const error = Errors.InvalidActivityTypeUndefined + + assert.strictEqual(error.code, -110000) + assert.strictEqual(error.description, 'Invalid ActivityType: undefined') + assert.strictEqual(error.helplink, 'https://aka.ms/M365AgentsErrorCodes/#{errorCode}') + }) + + it('should have InvalidActivityTypeNull error definition', () => { + const error = Errors.InvalidActivityTypeNull + + assert.strictEqual(error.code, -110001) + assert.strictEqual(error.description, 'Invalid ActivityType: null') + assert.strictEqual(error.helplink, 'https://aka.ms/M365AgentsErrorCodes/#{errorCode}') + }) + + it('should have InvalidActivityTypeEmptyString error definition', () => { + const error = Errors.InvalidActivityTypeEmptyString + + assert.strictEqual(error.code, -110002) + assert.strictEqual(error.description, 'Invalid ActivityType: empty string') + assert.strictEqual(error.helplink, 'https://aka.ms/M365AgentsErrorCodes/#{errorCode}') + }) + + it('should have all error codes in the correct range', () => { + const errorDefinitions = Object.values(Errors).filter( + 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 -110000 to -110999 + errorDefinitions.forEach(errorDef => { + assert.ok(errorDef.code < 0, `Error code ${errorDef.code} should be negative`) + assert.ok(errorDef.code >= -110999, `Error code ${errorDef.code} should be >= -110999`) + assert.ok(errorDef.code <= -110000, `Error code ${errorDef.code} should be <= -110000`) + }) + }) + + it('should have unique error codes', () => { + const errorDefinitions = Object.values(Errors).filter( + val => val && typeof val === 'object' && 'code' in val && 'description' in val && 'helplink' in val + ) 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 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.includes('{errorCode}'), + `Help link should contain {errorCode} token: ${errorDef.helplink}` + ) + assert.ok( + errorDef.helplink.startsWith('https://aka.ms/M365AgentsErrorCodes/#'), + `Help link should start with correct URL: ${errorDef.helplink}` + ) + }) + }) + + it('should have non-empty descriptions', () => { + 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.description.length > 0, 'Description should not be empty') + }) + }) +}) diff --git a/packages/agents-hosting-storage-cosmos/src/cosmosDbPartitionedStorage.ts b/packages/agents-hosting-storage-cosmos/src/cosmosDbPartitionedStorage.ts index 40641095..79c5943a 100644 --- a/packages/agents-hosting-storage-cosmos/src/cosmosDbPartitionedStorage.ts +++ b/packages/agents-hosting-storage-cosmos/src/cosmosDbPartitionedStorage.ts @@ -362,7 +362,7 @@ export class CosmosDbPartitionedStorage implements Storage { errorObj, { maxDepth: maxDepthAllowed.toString(), - additionalMessage: additionalMessage + additionalMessage } ) } else if (obj && typeof obj === 'object') { diff --git a/packages/agents-hosting/src/activityHandler.ts b/packages/agents-hosting/src/activityHandler.ts index 4a90fdfc..f55903fb 100644 --- a/packages/agents-hosting/src/activityHandler.ts +++ b/packages/agents-hosting/src/activityHandler.ts @@ -4,7 +4,7 @@ */ import { debug } from '@microsoft/agents-activity/logger' import { TurnContext } from './turnContext' -import { Activity, ActivityTypes, Channels } from '@microsoft/agents-activity' +import { Activity, ActivityTypes, Channels, ExceptionHelper } from '@microsoft/agents-activity' import { StatusCodes } from './statusCodes' import { InvokeResponse } from './invoke/invokeResponse' import { InvokeException } from './invoke/invokeException' @@ -13,6 +13,7 @@ import { SearchInvokeValue } from './invoke/searchInvokeValue' import { SearchInvokeResponse } from './invoke/searchInvokeResponse' import { AdaptiveCardInvokeResponse } from './invoke/adaptiveCardInvokeResponse' import { tokenResponseEventName } from './tokenResponseEventName' +import { Errors } from './errorHelper' /** Symbol key for invoke response */ export const INVOKE_RESPONSE_KEY = Symbol('invokeResponse') @@ -254,9 +255,9 @@ export class ActivityHandler { * @throws Error if context is missing, activity is missing, or activity type is missing */ async run (context: TurnContext): Promise { - if (!context) throw new Error('Missing TurnContext parameter') - if (!context.activity) throw new Error('TurnContext does not include an activity') - if (!context.activity.type) throw new Error('Activity is missing its type') + if (!context) throw ExceptionHelper.generateException(Error, Errors.MissingTurnContext) + if (!context.activity) throw ExceptionHelper.generateException(Error, Errors.TurnContextMissingActivity) + if (!context.activity.type) throw ExceptionHelper.generateException(Error, Errors.ActivityMissingType) await this.onTurnActivity(context) } diff --git a/packages/agents-hosting/src/agent-client/agentClient.ts b/packages/agents-hosting/src/agent-client/agentClient.ts index 2678ea52..c7a7ce8e 100644 --- a/packages/agents-hosting/src/agent-client/agentClient.ts +++ b/packages/agents-hosting/src/agent-client/agentClient.ts @@ -1,9 +1,10 @@ import { AuthConfiguration, MsalTokenProvider } from '../auth' -import { Activity, ConversationReference, RoleTypes } from '@microsoft/agents-activity' +import { Activity, ConversationReference, RoleTypes, ExceptionHelper } from '@microsoft/agents-activity' import { v4 } from 'uuid' import { debug } from '@microsoft/agents-activity/logger' import { ConversationState } from '../state' import { TurnContext } from '../turnContext' +import { Errors } from '../errorHelper' const logger = debug('agents:agent-client') @@ -115,7 +116,7 @@ export class AgentClient { }) if (!response.ok) { await conversationDataAccessor.delete(context, { channelId: activityCopy.channelId!, conversationId: activityCopy.conversation!.id }) - throw new Error(`Failed to post activity to agent: ${response.statusText}`) + throw ExceptionHelper.generateException(Error, Errors.FailedToPostActivityToAgent, undefined, { statusText: response.statusText }) } return response.statusText } @@ -139,10 +140,10 @@ export class AgentClient { serviceUrl: process.env[`${agentName}_serviceUrl`]! } } else { - throw new Error(`Missing agent client config for agent ${agentName}`) + throw ExceptionHelper.generateException(Error, Errors.MissingAgentClientConfig, undefined, { agentName }) } } else { - throw new Error('Agent name is required') + throw ExceptionHelper.generateException(Error, Errors.AgentNameRequired) } } } diff --git a/packages/agents-hosting/src/app/adaptiveCards/activityValueParsers.ts b/packages/agents-hosting/src/app/adaptiveCards/activityValueParsers.ts index 32153ea2..220c9edc 100644 --- a/packages/agents-hosting/src/app/adaptiveCards/activityValueParsers.ts +++ b/packages/agents-hosting/src/app/adaptiveCards/activityValueParsers.ts @@ -3,8 +3,9 @@ * Licensed under the MIT License. */ +import { ExceptionHelper, activityZodSchema, AdaptiveCardInvokeAction, adaptiveCardInvokeActionZodSchema } from '@microsoft/agents-activity' +import { Errors } from '../../errorHelper' import { z } from 'zod' -import { activityZodSchema, AdaptiveCardInvokeAction, adaptiveCardInvokeActionZodSchema } from '@microsoft/agents-activity' // import { MessagingExtensionQuery, messagingExtensionQueryZodSchema } from '../messageExtension/messagingExtensionQuery' import { adaptiveCardsSearchParamsZodSchema } from './adaptiveCardsSearchParams' @@ -76,7 +77,7 @@ export function parseValueActionExecuteSelector (value: unknown): ValueAction | }) const safeParsedValue = actionValueExecuteSelector.passthrough().safeParse(value) if (!safeParsedValue.success) { - throw new Error(`Invalid action value: ${safeParsedValue.error}`) + throw ExceptionHelper.generateException(Error, Errors.InvalidActionValue, undefined, { error: JSON.stringify(safeParsedValue.error) }) } const parsedValue = safeParsedValue.data return { diff --git a/packages/agents-hosting/src/app/adaptiveCards/adaptiveCardsActions.ts b/packages/agents-hosting/src/app/adaptiveCards/adaptiveCardsActions.ts index 0a842020..a932948d 100644 --- a/packages/agents-hosting/src/app/adaptiveCards/adaptiveCardsActions.ts +++ b/packages/agents-hosting/src/app/adaptiveCards/adaptiveCardsActions.ts @@ -3,8 +3,9 @@ * Licensed under the MIT License. */ -import { Activity, ActivityTypes } from '@microsoft/agents-activity' +import { Activity, ActivityTypes, ExceptionHelper } from '@microsoft/agents-activity' import { AdaptiveCardInvokeResponse, AgentApplication, CardFactory, INVOKE_RESPONSE_KEY, InvokeResponse, MessageFactory, RouteSelector, TurnContext, TurnState } from '../../' +import { Errors } from '../../errorHelper' import { AdaptiveCardActionExecuteResponseType } from './adaptiveCardActionExecuteResponseType' import { parseAdaptiveCardInvokeAction, parseValueActionExecuteSelector, parseValueDataset, parseValueSearchQuery } from './activityValueParsers' import { AdaptiveCardsSearchParams } from './adaptiveCardsSearchParams' @@ -117,7 +118,7 @@ export class AdaptiveCardsActions { a?.name !== ACTION_INVOKE_NAME || (invokeAction?.action.type !== ACTION_EXECUTE_TYPE) ) { - throw new Error(`Unexpected AdaptiveCards.actionExecute() triggered for activity type: ${invokeAction?.action.type}` + throw ExceptionHelper.generateException(Error, Errors.UnexpectedActionExecute, undefined, { activityType: invokeAction?.action.type || 'unknown' } ) } @@ -196,7 +197,7 @@ export class AdaptiveCardsActions { this._app.addRoute(selector, async (context, state) => { const a = context?.activity if (a?.type !== ActivityTypes.Message || a?.text || typeof a?.value !== 'object') { - throw new Error(`Unexpected AdaptiveCards.actionSubmit() triggered for activity type: ${a?.type}`) + throw ExceptionHelper.generateException(Error, Errors.UnexpectedActionSubmit, undefined, { activityType: a?.type }) } await handler(context, state as TState, (parseAdaptiveCardInvokeAction(a.value)) as TData ?? {} as TData) @@ -227,7 +228,7 @@ export class AdaptiveCardsActions { async (context, state) => { const a = context?.activity if (a?.type !== 'invoke' || a?.name !== SEARCH_INVOKE_NAME) { - throw new Error(`Unexpected AdaptiveCards.search() triggered for activity type: ${a?.type}`) + throw ExceptionHelper.generateException(Error, Errors.UnexpectedSearch, undefined, { activityType: a?.type }) } const parsedQuery = parseValueSearchQuery(a.value) diff --git a/packages/agents-hosting/src/app/agentApplication.ts b/packages/agents-hosting/src/app/agentApplication.ts index ed823038..1e6a336b 100644 --- a/packages/agents-hosting/src/app/agentApplication.ts +++ b/packages/agents-hosting/src/app/agentApplication.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. */ -import { Activity, ActivityTypes, ConversationReference } from '@microsoft/agents-activity' +import { Activity, ActivityTypes, ConversationReference, ExceptionHelper } from '@microsoft/agents-activity' import { ResourceResponse } from '../connector-client' import { debug } from '@microsoft/agents-activity/logger' import { TurnContext } from '../turnContext' @@ -11,6 +11,7 @@ import { AdaptiveCardsActions } from './adaptiveCards' import { AgentApplicationOptions } from './agentApplicationOptions' import { ConversationUpdateEvents } from './conversationUpdateEvents' import { AgentExtension } from './extensions' +import { Errors } from '../errorHelper' import { RouteHandler } from './routeHandler' import { RouteSelector } from './routeSelector' import { TurnEvents } from './turnEvents' @@ -131,12 +132,12 @@ export class AgentApplication { } if (this._options.longRunningMessages && !this._adapter && !this._options.agentAppId) { - throw new Error('The Application.longRunningMessages property is unavailable because no adapter was configured in the app.') + throw ExceptionHelper.generateException(Error, Errors.LongRunningMessagesPropertyUnavailable) } if (this._options.transcriptLogger) { if (!this._options.adapter) { - throw new Error('The Application.transcriptLogger property is unavailable because no adapter was configured in the app.') + throw ExceptionHelper.generateException(Error, Errors.TranscriptLoggerPropertyUnavailable) } else { this._adapter?.use(new TranscriptLoggerMiddleware(this._options.transcriptLogger)) } @@ -152,7 +153,7 @@ export class AgentApplication { */ public get authorization (): Authorization { if (!this._authorization) { - throw new Error('The Application.authorization property is unavailable because no authorization options were configured.') + throw ExceptionHelper.generateException(Error, Errors.AuthorizationPropertyUnavailable) } return this._authorization } @@ -436,9 +437,7 @@ export class AgentApplication { if (this.options.authorization) { this.authorization.onSignInSuccess(handler) } else { - throw new Error( - 'The Application.authorization property is unavailable because no authorization options were configured.' - ) + throw ExceptionHelper.generateException(Error, Errors.AuthorizationPropertyUnavailable) } return this } @@ -466,9 +465,7 @@ export class AgentApplication { if (this.options.authorization) { this.authorization.onSignInFailure(handler) } else { - throw new Error( - 'The Application.authorization property is unavailable because no authorization options were configured.' - ) + throw ExceptionHelper.generateException(Error, Errors.AuthorizationPropertyUnavailable) } return this } @@ -804,7 +801,7 @@ export class AgentApplication { */ public registerExtension> (extension: T, regcb : (ext:T) => void): void { if (this._extensions.includes(extension)) { - throw new Error('Extension already registered') + throw ExceptionHelper.generateException(Error, Errors.ExtensionAlreadyRegistered) } this._extensions.push(extension) regcb(extension) diff --git a/packages/agents-hosting/src/app/auth/authorization.ts b/packages/agents-hosting/src/app/auth/authorization.ts index 8d5e3919..7a0d524e 100644 --- a/packages/agents-hosting/src/app/auth/authorization.ts +++ b/packages/agents-hosting/src/app/auth/authorization.ts @@ -5,6 +5,8 @@ import { debug } from '@microsoft/agents-activity/logger' import { TokenResponse } from '../../oauth' +import { ExceptionHelper } from '@microsoft/agents-activity' +import { Errors } from '../../errorHelper' import { TurnContext } from '../../turnContext' import { TurnState } from '../turnState' import { AuthorizationManager } from './authorizationManager' @@ -148,7 +150,7 @@ export class UserAuthorization implements Authorization { return { token } } - throw new Error('Invalid parameters for exchangeToken method.') + throw ExceptionHelper.generateException(Error, Errors.InvalidExchangeTokenParameters) } /** @@ -254,7 +256,7 @@ export class UserAuthorization implements Authorization { */ private getHandler (id: string) { if (!Object.prototype.hasOwnProperty.call(this.manager.handlers, id)) { - throw new Error(`Cannot find auth handler with ID '${id}'. Ensure it is configured in the agent application options.`) + throw ExceptionHelper.generateException(Error, Errors.CannotFindAuthHandler, undefined, { id }) } return this.manager.handlers[id] } diff --git a/packages/agents-hosting/src/app/auth/authorizationManager.ts b/packages/agents-hosting/src/app/auth/authorizationManager.ts index ff264017..4e863a56 100644 --- a/packages/agents-hosting/src/app/auth/authorizationManager.ts +++ b/packages/agents-hosting/src/app/auth/authorizationManager.ts @@ -3,9 +3,10 @@ * Licensed under the MIT License. */ -import { Activity, debug } from '@microsoft/agents-activity' +import { Activity, debug, ExceptionHelper } from '@microsoft/agents-activity' import { AgentApplication } from '../agentApplication' import { AgenticAuthorization, AzureBotAuthorization } from './handlers' +import { Errors } from '../../errorHelper' import { TurnContext } from '../../turnContext' import { HandlerStorage } from './handlerStorage' import { ActiveAuthorizationHandler, AuthorizationHandlerStatus, AuthorizationHandler, AuthorizationHandlerSettings, AuthorizationOptions } from './types' @@ -53,11 +54,11 @@ export class AuthorizationManager { */ constructor (private app: AgentApplication, connections: Connections) { if (!app.options.storage) { - throw new Error('Storage is required for Authorization. Ensure that a storage provider is configured in the AgentApplication options.') + throw ExceptionHelper.generateException(Error, Errors.StorageRequiredForAuthorization) } if (app.options.authorization === undefined || Object.keys(app.options.authorization).length === 0) { - throw new Error('The AgentApplication.authorization does not have any auth handlers') + throw ExceptionHelper.generateException(Error, Errors.NoAuthHandlersConfigured) } const settings: AuthorizationHandlerSettings = { storage: app.options.storage, connections } @@ -83,7 +84,7 @@ export class AuthorizationManager { // Validate supported types, agentic, and default (Azure Bot - undefined) const supportedTypes = ['agentic', undefined] if (!supportedTypes.includes(result.type)) { - throw new Error(`Unsupported authorization handler type: '${result.type}' for auth handler: '${id}'. Supported types are: '${supportedTypes.filter(Boolean).join('\', \'')}'.`) + throw ExceptionHelper.generateException(Error, Errors.UnsupportedAuthorizationHandlerType, undefined, { handlerType: result.type || 'undefined', handlerId: id, supportedTypes: supportedTypes.filter(Boolean).join(', ') }) } return result @@ -134,7 +135,7 @@ export class AuthorizationManager { } if (status !== AuthorizationHandlerStatus.APPROVED) { - throw new Error(this.prefix(handler.id, `Unexpected registration status: ${status}`)) + throw ExceptionHelper.generateException(Error, Errors.UnexpectedRegistrationStatus, undefined, { status }) } await storage.delete() @@ -183,7 +184,7 @@ export class AuthorizationManager { return await handler.signin(context, active) } catch (cause) { await storage.delete() - throw new Error(this.prefix(handler.id, 'Failed to sign in'), { cause }) + throw ExceptionHelper.generateException(Error, Errors.FailedToSignIn, cause instanceof Error ? cause : undefined) } } @@ -199,7 +200,7 @@ export class AuthorizationManager { return this._handlers[id] }) if (unknownHandlers) { - throw new Error(`Cannot find auth handlers with ID(s): ${unknownHandlers}`) + throw ExceptionHelper.generateException(Error, Errors.CannotFindAuthHandlers, undefined, { unknownHandlers }) } return handlers } diff --git a/packages/agents-hosting/src/app/auth/handlerStorage.ts b/packages/agents-hosting/src/app/auth/handlerStorage.ts index e0842e63..5fb92247 100644 --- a/packages/agents-hosting/src/app/auth/handlerStorage.ts +++ b/packages/agents-hosting/src/app/auth/handlerStorage.ts @@ -3,6 +3,8 @@ * Licensed under the MIT License. */ +import { ExceptionHelper } from '@microsoft/agents-activity' +import { Errors } from '../../errorHelper' import { ActiveAuthorizationHandler } from './types' import { TurnContext } from '../../turnContext' import { Storage } from '../../storage' @@ -25,7 +27,7 @@ export class HandlerStorage Channel: '${channel}', Connection: '${connection}'`), context.activity) @@ -356,7 +357,7 @@ export class AzureBotAuthorization implements AuthorizationHandler { } if (!this.isExchangeable(token)) { - throw new Error(this.prefix('The current token is not exchangeable for an on-behalf-of flow. Ensure the token audience starts with \'api://\'.')) + throw ExceptionHelper.generateException(Error, Errors.TokenNotExchangeable) } try { @@ -547,7 +548,7 @@ export class AzureBotAuthorization implements AuthorizationHandler { private async getUserTokenClient (context: TurnContext): Promise { const userTokenClient = context.turnState.get(context.adapter.UserTokenClientKey) if (!userTokenClient) { - throw new Error(this.prefix('The \'userTokenClient\' is not available in the adapter. Ensure that the adapter supports user token operations.')) + throw ExceptionHelper.generateException(Error, Errors.UserTokenClientNotAvailable) } return userTokenClient } diff --git a/packages/agents-hosting/src/app/streaming/streamingResponse.ts b/packages/agents-hosting/src/app/streaming/streamingResponse.ts index 71897aad..d17b8d69 100644 --- a/packages/agents-hosting/src/app/streaming/streamingResponse.ts +++ b/packages/agents-hosting/src/app/streaming/streamingResponse.ts @@ -3,7 +3,8 @@ * Licensed under the MIT License. */ -import { Activity, addAIToActivity, Attachment, Entity, ClientCitation, SensitivityUsageInfo } from '@microsoft/agents-activity' +import { ExceptionHelper, Activity, addAIToActivity, Attachment, Entity, ClientCitation, SensitivityUsageInfo } from '@microsoft/agents-activity' +import { Errors } from '../../errorHelper' import { TurnContext } from '../../turnContext' import { Citation } from './citation' import { CitationUtil } from './citationUtil' @@ -95,7 +96,7 @@ export class StreamingResponse { */ public queueInformativeUpdate (text: string): void { if (this._ended) { - throw new Error('The stream has already ended.') + throw ExceptionHelper.generateException(Error, Errors.StreamAlreadyEnded) } // Queue a typing activity @@ -123,7 +124,7 @@ export class StreamingResponse { */ public queueTextChunk (text: string, citations?: Citation[]): void { if (this._ended) { - throw new Error('The stream has already ended.') + throw ExceptionHelper.generateException(Error, Errors.StreamAlreadyEnded) } // Update full message text @@ -143,7 +144,7 @@ export class StreamingResponse { */ public endStream (): Promise { if (this._ended) { - throw new Error('The stream has already ended.') + throw ExceptionHelper.generateException(Error, Errors.StreamAlreadyEnded) } // Queue final message diff --git a/packages/agents-hosting/src/app/turnState.ts b/packages/agents-hosting/src/app/turnState.ts index 2c608757..b5fed663 100644 --- a/packages/agents-hosting/src/app/turnState.ts +++ b/packages/agents-hosting/src/app/turnState.ts @@ -6,6 +6,8 @@ import { Storage, StoreItems } from '../storage' import { AppMemory } from './appMemory' import { TurnStateEntry } from './turnStateEntry' +import { ExceptionHelper } from '@microsoft/agents-activity' +import { Errors } from '../errorHelper' import { TurnContext } from '../turnContext' import { debug } from '@microsoft/agents-activity/logger' @@ -87,7 +89,7 @@ export class TurnState< public get conversation (): TConversationState { const scope = this.getScope(CONVERSATION_SCOPE) if (!scope) { - throw new Error(this._stateNotLoadedString) + throw ExceptionHelper.generateException(Error, Errors.TurnStateNotLoaded) } return scope.value as TConversationState } @@ -101,7 +103,7 @@ export class TurnState< public set conversation (value: TConversationState) { const scope = this.getScope(CONVERSATION_SCOPE) if (!scope) { - throw new Error(this._stateNotLoadedString) + throw ExceptionHelper.generateException(Error, Errors.TurnStateNotLoaded) } scope.replace(value as Record) } @@ -127,7 +129,7 @@ export class TurnState< public get user (): TUserState { const scope = this.getScope(USER_SCOPE) if (!scope) { - throw new Error(this._stateNotLoadedString) + throw ExceptionHelper.generateException(Error, Errors.TurnStateNotLoaded) } return scope.value as TUserState } @@ -141,7 +143,7 @@ export class TurnState< public set user (value: TUserState) { const scope = this.getScope(USER_SCOPE) if (!scope) { - throw new Error(this._stateNotLoadedString) + throw ExceptionHelper.generateException(Error, Errors.TurnStateNotLoaded) } scope.replace(value as Record) } @@ -157,7 +159,7 @@ export class TurnState< public deleteConversationState (): void { const scope = this.getScope(CONVERSATION_SCOPE) if (!scope) { - throw new Error(this._stateNotLoadedString) + throw ExceptionHelper.generateException(Error, Errors.TurnStateNotLoaded) } scope.delete() } @@ -173,7 +175,7 @@ export class TurnState< public deleteUserState (): void { const scope = this.getScope(USER_SCOPE) if (!scope) { - throw new Error(this._stateNotLoadedString) + throw ExceptionHelper.generateException(Error, Errors.TurnStateNotLoaded) } scope.delete() } @@ -316,7 +318,7 @@ export class TurnState< } if (!this._isLoaded) { - throw new Error(this._stateNotLoadedString) + throw ExceptionHelper.generateException(Error, Errors.TurnStateNotLoaded) } let changes: StoreItems | undefined @@ -378,19 +380,19 @@ export class TurnState< const userId = activity?.from?.id if (!channelId) { - throw new Error('missing context.activity.channelId') + throw ExceptionHelper.generateException(Error, Errors.MissingContextActivityChannelId) } if (!agentId) { - throw new Error('missing context.activity.recipient.id') + throw ExceptionHelper.generateException(Error, Errors.MissingContextActivityRecipientId) } if (!conversationId) { - throw new Error('missing context.activity.conversation.id') + throw ExceptionHelper.generateException(Error, Errors.MissingContextActivityConversationId) } if (!userId) { - throw new Error('missing context.activity.from.id') + throw ExceptionHelper.generateException(Error, Errors.MissingContextActivityFromId) } const keys: Record = {} @@ -413,14 +415,14 @@ export class TurnState< private getScopeAndName (path: string): { scope: TurnStateEntry; name: string } { const parts = path.split('.') if (parts.length > 2) { - throw new Error(`Invalid state path: ${path}`) + throw ExceptionHelper.generateException(Error, Errors.InvalidStatePath, undefined, { path }) } else if (parts.length === 1) { parts.unshift(TEMP_SCOPE) } const scope = this.getScope(parts[0]) if (scope === undefined) { - throw new Error(`Invalid state scope: ${parts[0]}`) + throw ExceptionHelper.generateException(Error, Errors.InvalidStateScope, undefined, { scope: parts[0] }) } return { scope, name: parts[1] } } diff --git a/packages/agents-hosting/src/app/turnStateProperty.ts b/packages/agents-hosting/src/app/turnStateProperty.ts index 29aba4d2..143cd3ac 100644 --- a/packages/agents-hosting/src/app/turnStateProperty.ts +++ b/packages/agents-hosting/src/app/turnStateProperty.ts @@ -3,6 +3,8 @@ * Licensed under the MIT License. */ +import { ExceptionHelper } from '@microsoft/agents-activity' +import { Errors } from '../errorHelper' import { TurnContext } from '../turnContext' import { StatePropertyAccessor } from '../state' import { TurnStateEntry } from './turnStateEntry' @@ -27,12 +29,12 @@ export class TurnStateProperty implements StatePropertyAccessor { const scope = state.getScope(scopeName) if (!scope) { - throw new Error(`TurnStateProperty: TurnState missing state scope named "${scope}".`) + throw ExceptionHelper.generateException(Error, Errors.TurnStateMissingScope, undefined, { scope: scopeName }) } this._state = scope if (!this._state) { - throw new Error(`TurnStateProperty: TurnState missing state scope named "${scope}".`) + throw ExceptionHelper.generateException(Error, Errors.TurnStateMissingScope, undefined, { scope: scopeName }) } } diff --git a/packages/agents-hosting/src/auth/authConfiguration.ts b/packages/agents-hosting/src/auth/authConfiguration.ts index 608659b1..f376df2e 100644 --- a/packages/agents-hosting/src/auth/authConfiguration.ts +++ b/packages/agents-hosting/src/auth/authConfiguration.ts @@ -3,6 +3,8 @@ * Licensed under the MIT License. */ +import { ExceptionHelper } from '@microsoft/agents-activity' +import { Errors } from '../errorHelper' import { debug } from '@microsoft/agents-activity/logger' import { ConnectionMapItem } from './msalConnectionManager' import objectPath from 'object-path' @@ -131,13 +133,13 @@ export const loadAuthConfigFromEnv = (cnxName?: string): AuthConfiguration => { if (entry) { authConfig = entry } else { - throw new Error(`Connection "${cnxName}" not found in environment.`) + throw ExceptionHelper.generateException(Error, Errors.ConnectionNotFoundInEnvironment, undefined, { cnxName }) } } else { const defaultItem = envConnections.connectionsMap.find((item) => item.serviceUrl === '*') const defaultConn = defaultItem ? envConnections.connections.get(defaultItem.connection) : undefined if (!defaultConn) { - throw new Error('No default connection found in environment connections.') + throw ExceptionHelper.generateException(Error, Errors.NoDefaultConnection) } authConfig = defaultConn } @@ -173,7 +175,7 @@ export const loadPrevAuthConfigFromEnv: () => AuthConfiguration = () => { if (envConnections.connectionsMap.length === 0) { // No connections provided, we need to populate the connection map with the old config settings if (process.env.MicrosoftAppId === undefined && process.env.NODE_ENV === 'production') { - throw new Error('ClientId required in production') + throw ExceptionHelper.generateException(Error, Errors.ClientIdRequiredInProduction) } const authority = process.env.authorityEndpoint ?? 'https://login.microsoftonline.com' authConfig = { @@ -200,7 +202,7 @@ export const loadPrevAuthConfigFromEnv: () => AuthConfiguration = () => { const defaultItem = envConnections.connectionsMap.find((item) => item.serviceUrl === '*') const defaultConn = defaultItem ? envConnections.connections.get(defaultItem.connection) : undefined if (!defaultConn) { - throw new Error('No default connection found in environment connections.') + throw ExceptionHelper.generateException(Error, Errors.NoDefaultConnection) } authConfig = defaultConn } @@ -303,7 +305,7 @@ export function getAuthConfigWithDefaults (config?: AuthConfiguration): AuthConf const defaultItem = connections.connectionsMap?.find((item) => item.serviceUrl === '*') const defaultConn = defaultItem ? connections.connections?.get(defaultItem.connection) : undefined if (!defaultConn) { - throw new Error('No default connection found in environment connections.') + throw ExceptionHelper.generateException(Error, Errors.NoDefaultConnection) } mergedConfig = buildLegacyAuthConfig(undefined, defaultConn) } @@ -321,10 +323,10 @@ function buildLegacyAuthConfig (envPrefix: string = '', customConfig?: AuthConfi const clientId = customConfig?.clientId ?? process.env[`${prefix}clientId`] if (!clientId && !envPrefix && process.env.NODE_ENV === 'production') { - throw new Error('ClientId required in production') + throw ExceptionHelper.generateException(Error, Errors.ClientIdRequiredInProduction) } if (!clientId && envPrefix) { - throw new Error(`ClientId not found for connection: ${envPrefix}`) + throw ExceptionHelper.generateException(Error, Errors.ClientIdNotFoundForConnection, undefined, { envPrefix }) } const tenantId = customConfig?.tenantId ?? process.env[`${prefix}tenantId`] diff --git a/packages/agents-hosting/src/auth/jwt-middleware.ts b/packages/agents-hosting/src/auth/jwt-middleware.ts index 4541ff16..9b9f1124 100644 --- a/packages/agents-hosting/src/auth/jwt-middleware.ts +++ b/packages/agents-hosting/src/auth/jwt-middleware.ts @@ -9,6 +9,8 @@ import { Request } from './request' import jwksRsa, { JwksClient, SigningKey } from 'jwks-rsa' import jwt, { JwtHeader, JwtPayload, SignCallback, GetPublicKeyOrSecret } from 'jsonwebtoken' import { debug } from '@microsoft/agents-activity/logger' +import { ExceptionHelper } from '@microsoft/agents-activity' +import { Errors } from '../errorHelper' const logger = debug('agents:jwt-middleware') @@ -23,7 +25,7 @@ const verifyToken = async (raw: string, config: AuthConfiguration): Promise { logger.debug('Getting agentic instance token') if (!this.connectionSettings) { - throw new Error('Connection settings must be provided when calling getAgenticInstanceToken') + throw ExceptionHelper.generateException(Error, Errors.ConnectionSettingsRequiredForGetAgenticInstanceToken) } const appToken = await this.getAgenticApplicationToken(tenantId, agentAppInstanceId) const cca = new ConfidentialClientApplication({ @@ -150,7 +152,7 @@ export class MsalTokenProvider implements AuthProvider { }) if (!token?.accessToken) { - throw new Error(`Failed to acquire instance token for agent instance: ${agentAppInstanceId}`) + throw ExceptionHelper.generateException(Error, Errors.FailedToAcquireInstanceToken, undefined, { agentAppInstanceId }) } return token.accessToken @@ -187,7 +189,7 @@ export class MsalTokenProvider implements AuthProvider { */ private async acquireTokenByForAgenticScenarios (tenantId: string, clientId: string, clientAssertion: string | undefined, scopes: string[], tokenBodyParameters: { [key: string]: any }): Promise { if (!this.connectionSettings) { - throw new Error('Connection settings must be provided when calling getAgenticInstanceToken') + throw ExceptionHelper.generateException(Error, Errors.ConnectionSettingsRequiredForGetAgenticInstanceToken) } // Check cache first @@ -241,7 +243,7 @@ export class MsalTokenProvider implements AuthProvider { }) if (!token) { - throw new Error(`Failed to acquire instance token for user token: ${agentAppInstanceId}`) + throw ExceptionHelper.generateException(Error, Errors.FailedToAcquireInstanceTokenForUserToken, undefined, { agentAppInstanceId }) } return token @@ -249,7 +251,7 @@ export class MsalTokenProvider implements AuthProvider { public async getAgenticApplicationToken (tenantId: string, agentAppInstanceId: string): Promise { if (!this.connectionSettings?.clientId) { - throw new Error('Connection settings must be provided when calling getAgenticApplicationToken') + throw ExceptionHelper.generateException(Error, Errors.ConnectionSettingsRequiredForGetAgenticApplicationToken) } logger.debug('Getting agentic application token') const token = await this.acquireTokenByForAgenticScenarios(tenantId, this.connectionSettings.clientId, undefined, ['api://AzureAdTokenExchange/.default'], { @@ -258,7 +260,7 @@ export class MsalTokenProvider implements AuthProvider { }) if (!token) { - throw new Error(`Failed to acquire token for agent instance: ${agentAppInstanceId}`) + throw ExceptionHelper.generateException(Error, Errors.FailedToAcquireTokenForAgentInstance, undefined, { agentAppInstanceId }) } return token diff --git a/packages/agents-hosting/src/baseAdapter.ts b/packages/agents-hosting/src/baseAdapter.ts index ea9e3a3b..2a3454f4 100644 --- a/packages/agents-hosting/src/baseAdapter.ts +++ b/packages/agents-hosting/src/baseAdapter.ts @@ -6,11 +6,12 @@ import { Middleware, MiddlewareHandler, MiddlewareSet } from './middlewareSet' import { TurnContext } from './turnContext' import { debug } from '@microsoft/agents-activity/logger' -import { Activity, ConversationReference } from '@microsoft/agents-activity' +import { Activity, ConversationReference, ExceptionHelper } from '@microsoft/agents-activity' import { ResourceResponse } from './connector-client/resourceResponse' import { AttachmentData } from './connector-client/attachmentData' import { AttachmentInfo } from './connector-client/attachmentInfo' import { JwtPayload } from 'jsonwebtoken' +import { Errors } from './errorHelper' const logger = debug('agents:base-adapter') @@ -202,7 +203,7 @@ export abstract class BaseAdapter { if (err instanceof Error) { await this.onTurnError(pContext.proxy, err) } else { - throw new Error('Unknown error type: ' + err.message) + throw ExceptionHelper.generateException(Error, Errors.UnknownErrorType, undefined, { message: err.message }) } } else { throw err diff --git a/packages/agents-hosting/src/cloudAdapter.ts b/packages/agents-hosting/src/cloudAdapter.ts index b54fc599..33140783 100644 --- a/packages/agents-hosting/src/cloudAdapter.ts +++ b/packages/agents-hosting/src/cloudAdapter.ts @@ -13,7 +13,7 @@ import { AuthConfiguration, getAuthConfigWithDefaults } from './auth/authConfigu import { AuthProvider } from './auth/authProvider' import { ApxProductionScope } from './auth/authConstants' import { MsalConnectionManager } from './auth/msalConnectionManager' -import { Activity, ActivityEventNames, ActivityTypes, Channels, ConversationReference, DeliveryModes, ConversationParameters, RoleTypes } from '@microsoft/agents-activity' +import { Activity, ActivityEventNames, ActivityTypes, Channels, ConversationReference, DeliveryModes, ConversationParameters, RoleTypes, ExceptionHelper } from '@microsoft/agents-activity' import { ResourceResponse } from './connector-client/resourceResponse' import * as uuid from 'uuid' import { debug } from '@microsoft/agents-activity/logger' @@ -23,6 +23,7 @@ import { AttachmentInfo } from './connector-client/attachmentInfo' import { AttachmentData } from './connector-client/attachmentData' import { normalizeIncomingActivity } from './activityWireCompat' import { UserTokenClient } from './oauth' +import { Errors } from './errorHelper' import { HeaderPropagation, HeaderPropagationCollection, HeaderPropagationDefinition } from './headerPropagation' import { JwtPayload } from 'jsonwebtoken' import { getTokenServiceEndpoint } from './oauth/customUserTokenAPI' @@ -65,7 +66,7 @@ export class CloudAdapter extends BaseAdapter { */ protected resolveIfConnectorClientIsNeeded (activity: Activity): boolean { if (!activity) { - throw new TypeError('`activity` parameter required') + throw ExceptionHelper.generateException(TypeError, Errors.ActivityParameterRequired) } switch (activity.deliveryMode) { @@ -145,7 +146,7 @@ export class CloudAdapter extends BaseAdapter { headers ) } else { - throw new Error('Could not create connector client for agentic user') + throw ExceptionHelper.generateException(Error, Errors.CouldNotCreateConnectorClient) } } else { // ABS tokens will not have an azp/appid so use the botframework scope. @@ -249,15 +250,15 @@ export class CloudAdapter extends BaseAdapter { */ async sendActivities (context: TurnContext, activities: Activity[]): Promise { if (!context) { - throw new TypeError('`context` parameter required') + throw ExceptionHelper.generateException(TypeError, Errors.ContextParameterRequired) } if (!activities) { - throw new TypeError('`activities` parameter required') + throw ExceptionHelper.generateException(TypeError, Errors.ActivitiesParameterRequired) } if (activities.length === 0) { - throw new Error('Expecting one or more activities, but the array was empty.') + throw ExceptionHelper.generateException(Error, Errors.EmptyActivitiesArray) } const responses: ResourceResponse[] = [] @@ -271,7 +272,7 @@ export class CloudAdapter extends BaseAdapter { // no-op } else { if (!activity.serviceUrl || (activity.conversation == null) || !activity.conversation.id) { - throw new Error('Invalid activity object') + throw ExceptionHelper.generateException(Error, Errors.InvalidActivityObject) } if (activity.replyToId) { @@ -320,7 +321,7 @@ export class CloudAdapter extends BaseAdapter { res.end() } if (!request.body) { - throw new TypeError('`request.body` parameter required, make sure express.json() is used as middleware') + throw ExceptionHelper.generateException(TypeError, Errors.RequestBodyRequired) } const incoming = normalizeIncomingActivity(request.body!) const activity = Activity.fromObject(incoming) @@ -387,15 +388,15 @@ export class CloudAdapter extends BaseAdapter { */ async updateActivity (context: TurnContext, activity: Activity): Promise { if (!context) { - throw new TypeError('`context` parameter required') + throw ExceptionHelper.generateException(TypeError, Errors.ContextParameterRequired) } if (!activity) { - throw new TypeError('`activity` parameter required') + throw ExceptionHelper.generateException(TypeError, Errors.ActivityParameterRequired) } if (!activity.serviceUrl || (activity.conversation == null) || !activity.conversation.id || !activity.id) { - throw new Error('Invalid activity object') + throw ExceptionHelper.generateException(Error, Errors.InvalidActivityObject) } const response = await context.turnState.get(this.ConnectorClientKey).updateActivity( @@ -415,11 +416,11 @@ export class CloudAdapter extends BaseAdapter { */ async deleteActivity (context: TurnContext, reference: Partial): Promise { if (!context) { - throw new TypeError('`context` parameter required') + throw ExceptionHelper.generateException(TypeError, Errors.ContextParameterRequired) } if (!reference || !reference.serviceUrl || (reference.conversation == null) || !reference.conversation.id || !reference.activityId) { - throw new Error('Invalid conversation reference object') + throw ExceptionHelper.generateException(Error, Errors.InvalidConversationReference) } await context.turnState.get(this.ConnectorClientKey).deleteActivity(reference.conversation.id, reference.activityId) @@ -437,11 +438,11 @@ export class CloudAdapter extends BaseAdapter { logic: (revocableContext: TurnContext) => Promise, isResponse: Boolean = false): Promise { if (!reference || !reference.serviceUrl || (reference.conversation == null) || !reference.conversation.id) { - throw new Error('continueConversation: Invalid conversation reference object') + throw ExceptionHelper.generateException(Error, Errors.ContinueConversationInvalidReference) } if (!botAppIdOrIdentity) { - throw new TypeError('continueConversation: botAppIdOrIdentity is required') + throw ExceptionHelper.generateException(TypeError, Errors.ContinueConversationBotAppIdRequired) } const botAppId = typeof botAppIdOrIdentity === 'string' ? botAppIdOrIdentity : botAppIdOrIdentity.aud as string @@ -548,10 +549,14 @@ export class CloudAdapter extends BaseAdapter { logic: (context: TurnContext) => Promise ): Promise { if (typeof serviceUrl !== 'string' || !serviceUrl) { - throw new TypeError('`serviceUrl` must be a non-empty string') + throw ExceptionHelper.generateException(TypeError, Errors.ServiceUrlRequired) + } + if (!conversationParameters) { + throw ExceptionHelper.generateException(TypeError, Errors.ConversationParametersRequired) + } + if (!logic) { + throw ExceptionHelper.generateException(TypeError, Errors.LogicRequired) } - if (!conversationParameters) throw new TypeError('`conversationParameters` must be defined') - if (!logic) throw new TypeError('`logic` must be defined') const identity = CloudAdapter.createIdentity(audience) const restClient = await this.createConnectorClient(serviceUrl, audience, identity) @@ -578,15 +583,15 @@ export class CloudAdapter extends BaseAdapter { */ async uploadAttachment (context: TurnContext, conversationId: string, attachmentData: AttachmentData): Promise { if (context === undefined) { - throw new Error('context is required') + throw ExceptionHelper.generateException(Error, Errors.ContextRequired) } if (conversationId === undefined) { - throw new Error('conversationId is required') + throw ExceptionHelper.generateException(Error, Errors.ConversationIdRequired) } if (attachmentData === undefined) { - throw new Error('attachmentData is required') + throw ExceptionHelper.generateException(Error, Errors.AttachmentDataRequired) } return await context.turnState.get(this.ConnectorClientKey).uploadAttachment(conversationId, attachmentData) @@ -600,11 +605,11 @@ export class CloudAdapter extends BaseAdapter { */ async getAttachmentInfo (context: TurnContext, attachmentId: string): Promise { if (context === undefined) { - throw new Error('context is required') + throw ExceptionHelper.generateException(Error, Errors.ContextRequired) } if (attachmentId === undefined) { - throw new Error('attachmentId is required') + throw ExceptionHelper.generateException(Error, Errors.AttachmentIdRequired) } return await context.turnState.get(this.ConnectorClientKey).getAttachmentInfo(attachmentId) @@ -619,15 +624,15 @@ export class CloudAdapter extends BaseAdapter { */ async getAttachment (context: TurnContext, attachmentId: string, viewId: string): Promise { if (context === undefined) { - throw new Error('context is required') + throw ExceptionHelper.generateException(Error, Errors.ContextRequired) } if (attachmentId === undefined) { - throw new Error('attachmentId is required') + throw ExceptionHelper.generateException(Error, Errors.AttachmentIdRequired) } if (viewId === undefined) { - throw new Error('viewId is required') + throw ExceptionHelper.generateException(Error, Errors.ViewIdRequired) } return await context.turnState.get(this.ConnectorClientKey).getAttachment(attachmentId, viewId) diff --git a/packages/agents-hosting/src/connector-client/connectorClient.ts b/packages/agents-hosting/src/connector-client/connectorClient.ts index e84624a0..62374469 100644 --- a/packages/agents-hosting/src/connector-client/connectorClient.ts +++ b/packages/agents-hosting/src/connector-client/connectorClient.ts @@ -3,7 +3,7 @@ import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios' import { AuthConfiguration } from '../auth/authConfiguration' import { AuthProvider } from '../auth/authProvider' import { debug } from '@microsoft/agents-activity/logger' -import { Activity, ChannelAccount, ConversationParameters, RoleTypes, Channels } from '@microsoft/agents-activity' +import { Activity, ChannelAccount, ConversationParameters, RoleTypes, Channels, ExceptionHelper } from '@microsoft/agents-activity' import { ConversationsResult } from './conversationsResult' import { ConversationResourceResponse } from './conversationResourceResponse' import { ResourceResponse } from './resourceResponse' @@ -12,6 +12,7 @@ import { AttachmentData } from './attachmentData' import { normalizeOutgoingActivity } from '../activityWireCompat' import { getProductInfo } from '../getProductInfo' import { HeaderPropagation, HeaderPropagationCollection } from '../headerPropagation' +import { Errors } from '../errorHelper' const logger = debug('agents:connector-client') export { getProductInfo } @@ -143,7 +144,7 @@ export class ConnectorClient { public async getConversationMember (userId: string, conversationId: string): Promise { if (!userId || !conversationId) { - throw new Error('userId and conversationId are required') + throw ExceptionHelper.generateException(Error, Errors.UserIdAndConversationIdRequired) } const config: AxiosRequestConfig = { method: 'get', @@ -192,7 +193,7 @@ export class ConnectorClient { ): Promise { logger.debug(`Replying to activity: ${activityId} in conversation: ${conversationId}`) if (!conversationId || !activityId) { - throw new Error('conversationId and activityId are required') + throw ExceptionHelper.generateException(Error, Errors.ConversationIdAndActivityIdRequired) } const trimmedConversationId: string = this.conditionallyTruncateConversationId(conversationId, body) @@ -242,7 +243,7 @@ export class ConnectorClient { ): Promise { logger.debug(`Send to conversation: ${conversationId} activity: ${body.id}`) if (!conversationId) { - throw new Error('conversationId is required') + throw ExceptionHelper.generateException(Error, Errors.ConversationIdRequired) } const trimmedConversationId: string = this.conditionallyTruncateConversationId(conversationId, body) @@ -272,7 +273,7 @@ export class ConnectorClient { body: Activity ): Promise { if (!conversationId || !activityId) { - throw new Error('conversationId and activityId are required') + throw ExceptionHelper.generateException(Error, Errors.ConversationIdAndActivityIdRequired) } const config: AxiosRequestConfig = { method: 'put', @@ -297,7 +298,7 @@ export class ConnectorClient { activityId: string ): Promise { if (!conversationId || !activityId) { - throw new Error('conversationId and activityId are required') + throw ExceptionHelper.generateException(Error, Errors.ConversationIdAndActivityIdRequired) } const config: AxiosRequestConfig = { method: 'delete', @@ -321,7 +322,7 @@ export class ConnectorClient { body: AttachmentData ): Promise { if (conversationId === undefined) { - throw new Error('conversationId is required') + throw ExceptionHelper.generateException(Error, Errors.ConversationIdRequired) } const config: AxiosRequestConfig = { method: 'post', @@ -344,7 +345,7 @@ export class ConnectorClient { attachmentId: string ): Promise { if (attachmentId === undefined) { - throw new Error('attachmentId is required') + throw ExceptionHelper.generateException(Error, Errors.AttachmentIdRequired) } const config: AxiosRequestConfig = { method: 'get', @@ -368,10 +369,10 @@ export class ConnectorClient { viewId: string ): Promise { if (attachmentId === undefined) { - throw new Error('attachmentId is required') + throw ExceptionHelper.generateException(Error, Errors.AttachmentIdRequired) } if (viewId === undefined) { - throw new Error('viewId is required') + throw ExceptionHelper.generateException(Error, Errors.ViewIdRequired) } const config: AxiosRequestConfig = { method: 'get', diff --git a/packages/agents-hosting/src/errorHelper.ts b/packages/agents-hosting/src/errorHelper.ts new file mode 100644 index 00000000..52eb5be1 --- /dev/null +++ b/packages/agents-hosting/src/errorHelper.ts @@ -0,0 +1,879 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { AgentErrorDefinition } from '@microsoft/agents-activity' + +/** + * Error definitions for the Hosting system. + * This contains localized error codes for the Hosting subsystem of the AgentSDK. + * + * Each error definition includes an error code (starting from -120000), a description, and a help link + * pointing to an AKA link to get help for the given error. + * + * Usage example: + * ``` + * throw ExceptionHelper.generateException( + * Error, + * Errors.MissingTurnContext + * ); + * ``` + */ +export const Errors: { [key: string]: AgentErrorDefinition } = { + // Activity Handler Errors (-120000 to -120019) + /** + * Error thrown when TurnContext parameter is missing. + */ + MissingTurnContext: { + code: -120000, + description: 'Missing TurnContext parameter', + helplink: 'https://aka.ms/M365AgentsErrorCodes/#{errorCode}' + }, + + /** + * Error thrown when TurnContext does not include an activity. + */ + TurnContextMissingActivity: { + code: -120001, + description: 'TurnContext does not include an activity', + helplink: 'https://aka.ms/M365AgentsErrorCodes/#{errorCode}' + }, + + /** + * Error thrown when Activity is missing its type. + */ + ActivityMissingType: { + code: -120002, + description: 'Activity is missing its type', + helplink: 'https://aka.ms/M365AgentsErrorCodes/#{errorCode}' + }, + + /** + * Error thrown when activity object is invalid. + */ + InvalidActivityObject: { + code: -120003, + description: 'Invalid activity object', + helplink: 'https://aka.ms/M365AgentsErrorCodes/#{errorCode}' + }, + + /** + * Error thrown when Activity is required. + */ + ActivityRequired: { + code: -120004, + description: 'Activity is required.', + helplink: 'https://aka.ms/M365AgentsErrorCodes/#{errorCode}' + }, + + // Cloud Adapter Errors (-120020 to -120039) + /** + * Error thrown when activity parameter is required. + */ + ActivityParameterRequired: { + code: -120020, + description: '`activity` parameter required', + helplink: 'https://aka.ms/M365AgentsErrorCodes/#{errorCode}' + }, + + /** + * Error thrown when context parameter is required. + */ + ContextParameterRequired: { + code: -120021, + description: '`context` parameter required', + helplink: 'https://aka.ms/M365AgentsErrorCodes/#{errorCode}' + }, + + /** + * Error thrown when activities parameter is required. + */ + ActivitiesParameterRequired: { + code: -120022, + description: '`activities` parameter required', + helplink: 'https://aka.ms/M365AgentsErrorCodes/#{errorCode}' + }, + + /** + * Error thrown when expecting one or more activities, but the array was empty. + */ + EmptyActivitiesArray: { + code: -120023, + description: 'Expecting one or more activities, but the array was empty.', + helplink: 'https://aka.ms/M365AgentsErrorCodes/#{errorCode}' + }, + + /** + * Error thrown when request.body parameter is required. + */ + RequestBodyRequired: { + code: -120024, + description: '`request.body` parameter required, make sure express.json() is used as middleware', + helplink: 'https://aka.ms/M365AgentsErrorCodes/#{errorCode}' + }, + + /** + * Error thrown when conversation reference object is invalid. + */ + InvalidConversationReference: { + code: -120025, + description: 'Invalid conversation reference object', + helplink: 'https://aka.ms/M365AgentsErrorCodes/#{errorCode}' + }, + + /** + * Error thrown when continueConversation has invalid conversation reference object. + */ + ContinueConversationInvalidReference: { + code: -120026, + description: 'continueConversation: Invalid conversation reference object', + helplink: 'https://aka.ms/M365AgentsErrorCodes/#{errorCode}' + }, + + /** + * Error thrown when continueConversation requires botAppIdOrIdentity. + */ + ContinueConversationBotAppIdRequired: { + code: -120027, + description: 'continueConversation: botAppIdOrIdentity is required', + helplink: 'https://aka.ms/M365AgentsErrorCodes/#{errorCode}' + }, + + /** + * Error thrown when serviceUrl must be a non-empty string. + */ + ServiceUrlRequired: { + code: -120028, + description: '`serviceUrl` must be a non-empty string', + helplink: 'https://aka.ms/M365AgentsErrorCodes/#{errorCode}' + }, + + /** + * Error thrown when conversationParameters must be defined. + */ + ConversationParametersRequired: { + code: -120029, + description: '`conversationParameters` must be defined', + helplink: 'https://aka.ms/M365AgentsErrorCodes/#{errorCode}' + }, + + /** + * Error thrown when logic must be defined. + */ + LogicRequired: { + code: -120030, + description: '`logic` must be defined', + helplink: 'https://aka.ms/M365AgentsErrorCodes/#{errorCode}' + }, + + /** + * Error thrown when context is required. + */ + ContextRequired: { + code: -120031, + description: 'context is required', + helplink: 'https://aka.ms/M365AgentsErrorCodes/#{errorCode}' + }, + + /** + * Error thrown when conversationId is required. + */ + ConversationIdRequired: { + code: -120032, + description: 'conversationId is required', + helplink: 'https://aka.ms/M365AgentsErrorCodes/#{errorCode}' + }, + + /** + * Error thrown when attachmentData is required. + */ + AttachmentDataRequired: { + code: -120033, + description: 'attachmentData is required', + helplink: 'https://aka.ms/M365AgentsErrorCodes/#{errorCode}' + }, + + /** + * Error thrown when attachmentId is required. + */ + AttachmentIdRequired: { + code: -120034, + description: 'attachmentId is required', + helplink: 'https://aka.ms/M365AgentsErrorCodes/#{errorCode}' + }, + + /** + * Error thrown when viewId is required. + */ + ViewIdRequired: { + code: -120035, + description: 'viewId is required', + helplink: 'https://aka.ms/M365AgentsErrorCodes/#{errorCode}' + }, + + /** + * Error thrown when could not create connector client for agentic user. + */ + CouldNotCreateConnectorClient: { + code: -120036, + description: 'Could not create connector client for agentic user', + helplink: 'https://aka.ms/M365AgentsErrorCodes/#{errorCode}' + }, + + // Storage Errors (-120040 to -120049) + /** + * Error thrown when Keys are required when reading. + */ + KeysRequiredForReading: { + code: -120040, + description: 'Keys are required when reading.', + helplink: 'https://aka.ms/M365AgentsErrorCodes/#{errorCode}' + }, + + /** + * Error thrown when Changes are required when writing. + */ + ChangesRequiredForWriting: { + code: -120041, + description: 'Changes are required when writing.', + helplink: 'https://aka.ms/M365AgentsErrorCodes/#{errorCode}' + }, + + /** + * Error thrown when there is an eTag conflict during storage write. + */ + StorageEtagConflict: { + code: -120042, + description: 'Storage: error writing "{key}" due to eTag conflict.', + helplink: 'https://aka.ms/M365AgentsErrorCodes/#{errorCode}' + }, + + // State Errors (-120050 to -120059) + /** + * Error thrown when activity.channelId is missing. + */ + MissingActivityChannelId: { + code: -120050, + description: 'missing activity.channelId', + helplink: 'https://aka.ms/M365AgentsErrorCodes/#{errorCode}' + }, + + /** + * Error thrown when activity.conversation.id is missing. + */ + MissingActivityConversationId: { + code: -120051, + description: 'missing activity.conversation.id', + helplink: 'https://aka.ms/M365AgentsErrorCodes/#{errorCode}' + }, + + /** + * Error thrown when activity.from.id is missing. + */ + MissingActivityFromId: { + code: -120052, + description: 'missing activity.from.id', + helplink: 'https://aka.ms/M365AgentsErrorCodes/#{errorCode}' + }, + + /** + * Error thrown when context.activity.channelId is missing. + */ + MissingContextActivityChannelId: { + code: -120053, + description: 'missing context.activity.channelId', + helplink: 'https://aka.ms/M365AgentsErrorCodes/#{errorCode}' + }, + + /** + * Error thrown when context.activity.conversation.id is missing. + */ + MissingContextActivityConversationId: { + code: -120054, + description: 'missing context.activity.conversation.id', + helplink: 'https://aka.ms/M365AgentsErrorCodes/#{errorCode}' + }, + + /** + * Error thrown when context.activity.from.id is missing. + */ + MissingContextActivityFromId: { + code: -120055, + description: 'missing context.activity.from.id', + helplink: 'https://aka.ms/M365AgentsErrorCodes/#{errorCode}' + }, + + /** + * Error thrown when context.activity.recipient.id is missing. + */ + MissingContextActivityRecipientId: { + code: -120056, + description: 'missing context.activity.recipient.id', + helplink: 'https://aka.ms/M365AgentsErrorCodes/#{errorCode}' + }, + + // Turn Context Errors (-120060 to -120069) + /** + * Error thrown when attempting to set responded to false. + */ + CannotSetRespondedToFalse: { + code: -120060, + description: "TurnContext: cannot set 'responded' to a value of 'false'.", + helplink: 'https://aka.ms/M365AgentsErrorCodes/#{errorCode}' + }, + + // Header Propagation Errors (-120070 to -120079) + /** + * Error thrown when Headers must be provided. + */ + HeadersRequired: { + code: -120070, + description: 'Headers must be provided.', + helplink: 'https://aka.ms/M365AgentsErrorCodes/#{errorCode}' + }, + + // Middleware Errors (-120080 to -120089) + /** + * Error thrown when invalid plugin type being added to MiddlewareSet. + */ + InvalidMiddlewarePlugin: { + code: -120080, + description: 'MiddlewareSet.use(): invalid plugin type being added.', + helplink: 'https://aka.ms/M365AgentsErrorCodes/#{errorCode}' + }, + + // Transcript Logger Errors (-120090 to -120099) + /** + * Error thrown when TranscriptLoggerMiddleware requires a TranscriptLogger instance. + */ + TranscriptLoggerRequired: { + code: -120090, + description: 'TranscriptLoggerMiddleware requires a TranscriptLogger instance.', + helplink: 'https://aka.ms/M365AgentsErrorCodes/#{errorCode}' + }, + + /** + * Error thrown when channelId is required for transcript operations. + */ + TranscriptChannelIdRequired: { + code: -120091, + description: 'channelId is required.', + helplink: 'https://aka.ms/M365AgentsErrorCodes/#{errorCode}' + }, + + /** + * Error thrown when conversationId is required for transcript operations. + */ + TranscriptConversationIdRequired: { + code: -120092, + description: 'conversationId is required.', + helplink: 'https://aka.ms/M365AgentsErrorCodes/#{errorCode}' + }, + + // Connector Client Errors (-120100 to -120109) + /** + * Error thrown when userId and conversationId are required. + */ + UserIdAndConversationIdRequired: { + code: -120100, + description: 'userId and conversationId are required', + helplink: 'https://aka.ms/M365AgentsErrorCodes/#{errorCode}' + }, + + /** + * Error thrown when conversationId and activityId are required. + */ + ConversationIdAndActivityIdRequired: { + code: -120101, + description: 'conversationId and activityId are required', + helplink: 'https://aka.ms/M365AgentsErrorCodes/#{errorCode}' + }, + + // Agent Client Errors (-120110 to -120119) + /** + * Error thrown when failed to post activity to agent. + */ + FailedToPostActivityToAgent: { + code: -120110, + description: 'Failed to post activity to agent: {statusText}', + helplink: 'https://aka.ms/M365AgentsErrorCodes/#{errorCode}' + }, + + /** + * Error thrown when missing agent client config for agent. + */ + MissingAgentClientConfig: { + code: -120111, + description: 'Missing agent client config for agent {agentName}', + helplink: 'https://aka.ms/M365AgentsErrorCodes/#{errorCode}' + }, + + /** + * Error thrown when Agent name is required. + */ + AgentNameRequired: { + code: -120112, + description: 'Agent name is required', + helplink: 'https://aka.ms/M365AgentsErrorCodes/#{errorCode}' + }, + + // OAuth Errors (-120120 to -120129) + /** + * Error thrown when failed to sign out. + */ + FailedToSignOut: { + code: -120120, + description: 'Failed to sign out', + helplink: 'https://aka.ms/M365AgentsErrorCodes/#{errorCode}' + }, + + // Auth Configuration Errors (-120130 to -120159) + /** + * Error thrown when Connection not found in environment. + */ + ConnectionNotFoundInEnvironment: { + code: -120130, + description: 'Connection "{cnxName}" not found in environment.', + helplink: 'https://aka.ms/M365AgentsErrorCodes/#{errorCode}' + }, + + /** + * Error thrown when No default connection found in environment connections. + */ + NoDefaultConnection: { + code: -120131, + description: 'No default connection found in environment connections.', + helplink: 'https://aka.ms/M365AgentsErrorCodes/#{errorCode}' + }, + + /** + * Error thrown when ClientId required in production. + */ + ClientIdRequiredInProduction: { + code: -120132, + description: 'ClientId required in production', + helplink: 'https://aka.ms/M365AgentsErrorCodes/#{errorCode}' + }, + + /** + * Error thrown when ClientId not found for connection. + */ + ClientIdNotFoundForConnection: { + code: -120133, + description: 'ClientId not found for connection: {envPrefix}', + helplink: 'https://aka.ms/M365AgentsErrorCodes/#{errorCode}' + }, + + // MSAL Connection Manager Errors (-120160 to -120169) + /** + * Error thrown when Connection not found. + */ + ConnectionNotFound: { + code: -120160, + description: 'Connection not found: {connectionName}', + helplink: 'https://aka.ms/M365AgentsErrorCodes/#{errorCode}' + }, + + /** + * Error thrown when No connections found for this Agent in the Connections Configuration. + */ + NoConnectionsFound: { + code: -120161, + description: 'No connections found for this Agent in the Connections Configuration.', + helplink: 'https://aka.ms/M365AgentsErrorCodes/#{errorCode}' + }, + + /** + * Error thrown when Identity is required to get the token provider. + */ + IdentityRequiredForTokenProvider: { + code: -120162, + description: 'Identity is required to get the token provider.', + helplink: 'https://aka.ms/M365AgentsErrorCodes/#{errorCode}' + }, + + /** + * Error thrown when Audience and Service URL are required to get the token provider. + */ + AudienceAndServiceUrlRequired: { + code: -120163, + description: 'Audience and Service URL are required to get the token provider.', + helplink: 'https://aka.ms/M365AgentsErrorCodes/#{errorCode}' + }, + + /** + * Error thrown when No connection found for audience and serviceUrl. + */ + NoConnectionForAudienceAndServiceUrl: { + code: -120164, + description: 'No connection found for audience: {audience} and serviceUrl: {serviceUrl}', + helplink: 'https://aka.ms/M365AgentsErrorCodes/#{errorCode}' + }, + + // MSAL Token Provider Errors (-120170 to -120189) + /** + * Error thrown when Connection settings must be provided to constructor when calling getAccessToken. + */ + ConnectionSettingsRequiredForGetAccessToken: { + code: -120170, + description: 'Connection settings must be provided to constructor when calling getAccessToken(scope)', + helplink: 'https://aka.ms/M365AgentsErrorCodes/#{errorCode}' + }, + + /** + * Error thrown when Invalid authConfig. + */ + InvalidAuthConfig: { + code: -120171, + description: 'Invalid authConfig. ', + helplink: 'https://aka.ms/M365AgentsErrorCodes/#{errorCode}' + }, + + /** + * Error thrown when Failed to acquire token. + */ + FailedToAcquireToken: { + code: -120172, + description: 'Failed to acquire token', + helplink: 'https://aka.ms/M365AgentsErrorCodes/#{errorCode}' + }, + + /** + * Error thrown when Connection settings must be provided to constructor when calling acquireTokenOnBehalfOf. + */ + ConnectionSettingsRequiredForAcquireTokenOnBehalfOf: { + code: -120173, + description: 'Connection settings must be provided to constructor when calling acquireTokenOnBehalfOf(scopes, oboAssertion)', + helplink: 'https://aka.ms/M365AgentsErrorCodes/#{errorCode}' + }, + + /** + * Error thrown when Connection settings must be provided when calling getAgenticInstanceToken. + */ + ConnectionSettingsRequiredForGetAgenticInstanceToken: { + code: -120174, + description: 'Connection settings must be provided when calling getAgenticInstanceToken', + helplink: 'https://aka.ms/M365AgentsErrorCodes/#{errorCode}' + }, + + /** + * Error thrown when Failed to acquire instance token for agent instance. + */ + FailedToAcquireInstanceToken: { + code: -120175, + description: 'Failed to acquire instance token for agent instance: {agentAppInstanceId}', + helplink: 'https://aka.ms/M365AgentsErrorCodes/#{errorCode}' + }, + + /** + * Error thrown when Failed to acquire instance token for user token. + */ + FailedToAcquireInstanceTokenForUserToken: { + code: -120176, + description: 'Failed to acquire instance token for user token: {agentAppInstanceId}', + helplink: 'https://aka.ms/M365AgentsErrorCodes/#{errorCode}' + }, + + /** + * Error thrown when Connection settings must be provided when calling getAgenticApplicationToken. + */ + ConnectionSettingsRequiredForGetAgenticApplicationToken: { + code: -120177, + description: 'Connection settings must be provided when calling getAgenticApplicationToken', + helplink: 'https://aka.ms/M365AgentsErrorCodes/#{errorCode}' + }, + + /** + * Error thrown when Failed to acquire token for agent instance. + */ + FailedToAcquireTokenForAgentInstance: { + code: -120178, + description: 'Failed to acquire token for agent instance: {agentAppInstanceId}', + helplink: 'https://aka.ms/M365AgentsErrorCodes/#{errorCode}' + }, + + // JWT Middleware Errors (-120190 to -120199) + /** + * Error thrown when token is invalid. + */ + InvalidToken: { + code: -120190, + description: 'invalid token', + helplink: 'https://aka.ms/M365AgentsErrorCodes/#{errorCode}' + }, + + // Base Adapter Errors (-120200 to -120209) + /** + * Error thrown when unknown error type. + */ + UnknownErrorType: { + code: -120200, + description: 'Unknown error type: {message}', + helplink: 'https://aka.ms/M365AgentsErrorCodes/#{errorCode}' + }, + + // App Authorization Errors (-120210 to -120249) + /** + * Error thrown when The AgentApplication.authorization does not have any auth handlers. + */ + NoAuthHandlersConfigured: { + code: -120210, + description: 'The AgentApplication.authorization does not have any auth handlers', + helplink: 'https://aka.ms/M365AgentsErrorCodes/#{errorCode}' + }, + + /** + * Error thrown when Unsupported authorization handler type. + */ + UnsupportedAuthorizationHandlerType: { + code: -120211, + description: "Unsupported authorization handler type: '{handlerType}' for auth handler: '{handlerId}'. Supported types are: '{supportedTypes}'.", + helplink: 'https://aka.ms/M365AgentsErrorCodes/#{errorCode}' + }, + + /** + * Error thrown when Unexpected registration status. + */ + UnexpectedRegistrationStatus: { + code: -120212, + description: 'Unexpected registration status: {status}', + helplink: 'https://aka.ms/M365AgentsErrorCodes/#{errorCode}' + }, + + /** + * Error thrown when Failed to sign in. + */ + FailedToSignIn: { + code: -120213, + description: 'Failed to sign in', + helplink: 'https://aka.ms/M365AgentsErrorCodes/#{errorCode}' + }, + + /** + * Error thrown when Cannot find auth handlers with IDs. + */ + CannotFindAuthHandlers: { + code: -120214, + description: 'Cannot find auth handlers with ID(s): {unknownHandlers}', + helplink: 'https://aka.ms/M365AgentsErrorCodes/#{errorCode}' + }, + + /** + * Error thrown when Storage option is not available in the app options. + */ + StorageOptionNotAvailable: { + code: -120215, + description: "The 'storage' option is not available in the app options. Ensure that the app is properly configured.", + helplink: 'https://aka.ms/M365AgentsErrorCodes/#{errorCode}' + }, + + /** + * Error thrown when Connections option is not available in the app options. + */ + ConnectionsOptionNotAvailable: { + code: -120216, + description: "The 'connections' option is not available in the app options. Ensure that the app is properly configured.", + helplink: 'https://aka.ms/M365AgentsErrorCodes/#{errorCode}' + }, + + /** + * Error thrown when The name property or connectionName env variable is required. + */ + ConnectionNameRequired: { + code: -120217, + description: "The 'name' property or '{handlerId}_connectionName' env variable is required to initialize the handler.", + helplink: 'https://aka.ms/M365AgentsErrorCodes/#{errorCode}' + }, + + /** + * Error thrown when Both activity.channelId and activity.from.id are required to perform signout. + */ + ChannelIdAndFromIdRequiredForSignout: { + code: -120218, + description: "Both 'activity.channelId' and 'activity.from.id' are required to perform signout.", + helplink: 'https://aka.ms/M365AgentsErrorCodes/#{errorCode}' + }, + + /** + * Error thrown when The current token is not exchangeable for an on-behalf-of flow. + */ + TokenNotExchangeable: { + code: -120219, + description: "The current token is not exchangeable for an on-behalf-of flow. Ensure the token audience starts with 'api://'.", + helplink: 'https://aka.ms/M365AgentsErrorCodes/#{errorCode}' + }, + + /** + * Error thrown when The userTokenClient is not available in the adapter. + */ + UserTokenClientNotAvailable: { + code: -120220, + description: "The 'userTokenClient' is not available in the adapter. Ensure that the adapter supports user token operations.", + helplink: 'https://aka.ms/M365AgentsErrorCodes/#{errorCode}' + }, + + /** + * Error thrown when At least one scope must be specified for the Agentic authorization handler. + */ + ScopeRequired: { + code: -120221, + description: 'At least one scope must be specified for the Agentic authorization handler.', + helplink: 'https://aka.ms/M365AgentsErrorCodes/#{errorCode}' + }, + + /** + * Error thrown when Invalid parameters for exchangeToken method. + */ + InvalidExchangeTokenParameters: { + code: -120222, + description: 'Invalid parameters for exchangeToken method.', + helplink: 'https://aka.ms/M365AgentsErrorCodes/#{errorCode}' + }, + + /** + * Error thrown when Cannot find auth handler with ID. + */ + CannotFindAuthHandler: { + code: -120223, + description: "Cannot find auth handler with ID '{id}'. Ensure it is configured in the agent application options.", + helplink: 'https://aka.ms/M365AgentsErrorCodes/#{errorCode}' + }, + + /** + * Error thrown when Both activity.channelId and activity.from.id are required to generate the HandlerStorage key. + */ + ChannelIdAndFromIdRequiredForHandlerStorage: { + code: -120224, + description: "Both 'activity.channelId' and 'activity.from.id' are required to generate the HandlerStorage key.", + helplink: 'https://aka.ms/M365AgentsErrorCodes/#{errorCode}' + }, + + // App Application Errors (-120250 to -120269) + /** + * Error thrown when Storage is required for Authorization. + */ + StorageRequiredForAuthorization: { + code: -120250, + description: 'Storage is required for Authorization. Ensure that a storage provider is configured in the AgentApplication options.', + helplink: 'https://aka.ms/M365AgentsErrorCodes/#{errorCode}' + }, + + /** + * Error thrown when The Application.authorization property is unavailable. + */ + AuthorizationPropertyUnavailable: { + code: -120251, + description: 'The Application.authorization property is unavailable because no authorization options were configured.', + helplink: 'https://aka.ms/M365AgentsErrorCodes/#{errorCode}' + }, + + /** + * Error thrown when The Application.longRunningMessages property is unavailable. + */ + LongRunningMessagesPropertyUnavailable: { + code: -120252, + description: 'The Application.longRunningMessages property is unavailable because no adapter was configured in the app.', + helplink: 'https://aka.ms/M365AgentsErrorCodes/#{errorCode}' + }, + + /** + * Error thrown when The Application.transcriptLogger property is unavailable. + */ + TranscriptLoggerPropertyUnavailable: { + code: -120253, + description: 'The Application.transcriptLogger property is unavailable because no adapter was configured in the app.', + helplink: 'https://aka.ms/M365AgentsErrorCodes/#{errorCode}' + }, + + /** + * Error thrown when Extension already registered. + */ + ExtensionAlreadyRegistered: { + code: -120254, + description: 'Extension already registered', + helplink: 'https://aka.ms/M365AgentsErrorCodes/#{errorCode}' + }, + + // App Turn State Errors (-120270 to -120279) + /** + * Error thrown when TurnState hasn't been loaded. + */ + TurnStateNotLoaded: { + code: -120270, + description: "TurnState hasn't been loaded. Call load() first.", + helplink: 'https://aka.ms/M365AgentsErrorCodes/#{errorCode}' + }, + + /** + * Error thrown when TurnState missing state scope. + */ + TurnStateMissingScope: { + code: -120271, + description: 'TurnStateProperty: TurnState missing state scope named "{scope}".', + helplink: 'https://aka.ms/M365AgentsErrorCodes/#{errorCode}' + }, + + /** + * Error thrown when Invalid state scope. + */ + InvalidStateScope: { + code: -120272, + description: 'Invalid state scope: {scope}', + helplink: 'https://aka.ms/M365AgentsErrorCodes/#{errorCode}' + }, + + /** + * Error thrown when Invalid state path. + */ + InvalidStatePath: { + code: -120273, + description: 'Invalid state path: {path}', + helplink: 'https://aka.ms/M365AgentsErrorCodes/#{errorCode}' + }, + + // App Adaptive Cards Errors (-120280 to -120289) + /** + * Error thrown when Invalid action value. + */ + InvalidActionValue: { + code: -120280, + description: 'Invalid action value: {error}', + helplink: 'https://aka.ms/M365AgentsErrorCodes/#{errorCode}' + }, + + /** + * Error thrown when Unexpected AdaptiveCards.actionExecute() triggered for activity type. + */ + UnexpectedActionExecute: { + code: -120281, + description: 'Unexpected AdaptiveCards.actionExecute() triggered for activity type: {activityType}', + helplink: 'https://aka.ms/M365AgentsErrorCodes/#{errorCode}' + }, + + /** + * Error thrown when Unexpected AdaptiveCards.actionSubmit() triggered for activity type. + */ + UnexpectedActionSubmit: { + code: -120282, + description: 'Unexpected AdaptiveCards.actionSubmit() triggered for activity type: {activityType}', + helplink: 'https://aka.ms/M365AgentsErrorCodes/#{errorCode}' + }, + + /** + * Error thrown when Unexpected AdaptiveCards.search() triggered for activity type. + */ + UnexpectedSearch: { + code: -120283, + description: 'Unexpected AdaptiveCards.search() triggered for activity type: {activityType}', + helplink: 'https://aka.ms/M365AgentsErrorCodes/#{errorCode}' + }, + + // App Streaming Errors (-120290 to -120299) + /** + * Error thrown when The stream has already ended. + */ + StreamAlreadyEnded: { + code: -120290, + description: 'The stream has already ended.', + helplink: 'https://aka.ms/M365AgentsErrorCodes/#{errorCode}' + } +} diff --git a/packages/agents-hosting/src/headerPropagation.ts b/packages/agents-hosting/src/headerPropagation.ts index 4b7b1d5f..fe84d393 100644 --- a/packages/agents-hosting/src/headerPropagation.ts +++ b/packages/agents-hosting/src/headerPropagation.ts @@ -3,6 +3,9 @@ * Licensed under the MIT License. */ +import { ExceptionHelper } from '@microsoft/agents-activity' +import { Errors } from './errorHelper' + /** * A class that implements the HeaderPropagationCollection interface. * It filters the incoming request headers based on the definition provided and loads them into the outgoing headers collection. @@ -23,7 +26,7 @@ export class HeaderPropagation implements HeaderPropagationCollection { constructor (headers: Record) { if (!headers) { - throw new Error('Headers must be provided.') + throw ExceptionHelper.generateException(Error, Errors.HeadersRequired) } this._incomingRequests = this.normalizeHeaders(headers) diff --git a/packages/agents-hosting/src/index.ts b/packages/agents-hosting/src/index.ts index d489aec8..d6f5f707 100644 --- a/packages/agents-hosting/src/index.ts +++ b/packages/agents-hosting/src/index.ts @@ -27,3 +27,5 @@ export * from './storage/storage' export * from './headerPropagation' export * from './agent-client' + +export { Errors } from './errorHelper' diff --git a/packages/agents-hosting/src/middlewareSet.ts b/packages/agents-hosting/src/middlewareSet.ts index 3a192ba2..499e67a8 100644 --- a/packages/agents-hosting/src/middlewareSet.ts +++ b/packages/agents-hosting/src/middlewareSet.ts @@ -1,6 +1,8 @@ /** * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. */ import { TurnContext } from './turnContext' import { debug } from '@microsoft/agents-activity/logger' +import { ExceptionHelper } from '@microsoft/agents-activity' +import { Errors } from './errorHelper' const logger = debug('agents:middleware') @@ -52,7 +54,7 @@ export class MiddlewareSet implements Middleware { typeof plugin === 'function' ? plugin : async (context, next) => await plugin.onTurn(context, next) ) } else { - throw new Error('MiddlewareSet.use(): invalid plugin type being added.') + throw ExceptionHelper.generateException(Error, Errors.InvalidMiddlewarePlugin) } }) return this diff --git a/packages/agents-hosting/src/oauth/userTokenClient.ts b/packages/agents-hosting/src/oauth/userTokenClient.ts index aec2dabb..a8cf3945 100644 --- a/packages/agents-hosting/src/oauth/userTokenClient.ts +++ b/packages/agents-hosting/src/oauth/userTokenClient.ts @@ -2,7 +2,7 @@ // Licensed under the MIT License. import axios, { AxiosInstance } from 'axios' -import { Activity, ConversationReference } from '@microsoft/agents-activity' +import { Activity, ConversationReference, ExceptionHelper } from '@microsoft/agents-activity' import { debug } from '@microsoft/agents-activity/logger' import { normalizeTokenExchangeState } from '../activityWireCompat' import { AadResourceUrls, SignInResource, TokenExchangeRequest, TokenOrSinginResourceResponse, TokenResponse, TokenStatus } from './userTokenClient.types' @@ -10,6 +10,7 @@ import { getProductInfo } from '../getProductInfo' import { AuthProvider, MsalTokenProvider } from '../auth' import { HeaderPropagationCollection } from '../headerPropagation' import { getTokenServiceEndpoint } from './customUserTokenAPI' +import { Errors } from '../errorHelper' const logger = debug('agents:user-token-client') @@ -157,7 +158,7 @@ export class UserTokenClient { const params = { userId, connectionName, channelId } const response = await this.client.delete('/api/usertoken/SignOut', { params }) if (response.status !== 200) { - throw new Error('Failed to sign out') + throw ExceptionHelper.generateException(Error, Errors.FailedToSignOut) } } diff --git a/packages/agents-hosting/src/state/conversationState.ts b/packages/agents-hosting/src/state/conversationState.ts index 8412e271..237a2b2a 100644 --- a/packages/agents-hosting/src/state/conversationState.ts +++ b/packages/agents-hosting/src/state/conversationState.ts @@ -6,7 +6,8 @@ import { AgentState } from './agentState' import { Storage } from '../storage/storage' import { TurnContext } from '../turnContext' -import { Activity } from '@microsoft/agents-activity' +import { Activity, ExceptionHelper } from '@microsoft/agents-activity' +import { Errors } from '../errorHelper' /** * Manages the state of a conversation. @@ -30,11 +31,11 @@ export class ConversationState extends AgentState { const conversationId = activity && (activity.conversation != null) && activity.conversation.id ? activity.conversation.id : undefined if (!channelId) { - throw new Error('missing activity.channelId') + throw ExceptionHelper.generateException(Error, Errors.MissingActivityChannelId) } if (!conversationId) { - throw new Error('missing activity.conversation.id') + throw ExceptionHelper.generateException(Error, Errors.MissingActivityConversationId) } return `${channelId}/conversations/${conversationId}/${this.namespace}` diff --git a/packages/agents-hosting/src/state/userState.ts b/packages/agents-hosting/src/state/userState.ts index b5965891..d5edd17f 100644 --- a/packages/agents-hosting/src/state/userState.ts +++ b/packages/agents-hosting/src/state/userState.ts @@ -6,7 +6,8 @@ import { AgentState } from './agentState' import { Storage } from '../storage/storage' import { TurnContext } from '../turnContext' -import { Activity } from '@microsoft/agents-activity' +import { Activity, ExceptionHelper } from '@microsoft/agents-activity' +import { Errors } from '../errorHelper' /** * Manages the state of a user. @@ -31,11 +32,11 @@ export class UserState extends AgentState { const userId = activity && (activity.from != null) && activity.from.id ? activity.from.id : undefined if (!channelId) { - throw new Error('missing activity.channelId') + throw ExceptionHelper.generateException(Error, Errors.MissingActivityChannelId) } if (!userId) { - throw new Error('missing activity.from.id') + throw ExceptionHelper.generateException(Error, Errors.MissingActivityFromId) } return `${channelId}/users/${userId}/${this.namespace}` diff --git a/packages/agents-hosting/src/storage/memoryStorage.ts b/packages/agents-hosting/src/storage/memoryStorage.ts index 5adc3f19..03901c65 100644 --- a/packages/agents-hosting/src/storage/memoryStorage.ts +++ b/packages/agents-hosting/src/storage/memoryStorage.ts @@ -5,6 +5,8 @@ import { Storage, StoreItem } from './storage' import { debug } from '@microsoft/agents-activity/logger' +import { ExceptionHelper } from '@microsoft/agents-activity' +import { Errors } from '../errorHelper' const logger = debug('agents:memory-storage') @@ -62,7 +64,7 @@ export class MemoryStorage implements Storage { */ async read (keys: string[]): Promise { if (!keys || keys.length === 0) { - throw new ReferenceError('Keys are required when reading.') + throw ExceptionHelper.generateException(ReferenceError, Errors.KeysRequiredForReading) } const data: StoreItem = {} @@ -92,7 +94,7 @@ export class MemoryStorage implements Storage { */ async write (changes: StoreItem): Promise { if (!changes || changes.length === 0) { - throw new ReferenceError('Changes are required when writing.') + throw ExceptionHelper.generateException(ReferenceError, Errors.ChangesRequiredForWriting) } for (const [key, newItem] of Object.entries(changes)) { @@ -105,7 +107,7 @@ export class MemoryStorage implements Storage { if (newItem.eTag === oldItem.eTag) { this.saveItem(key, newItem) } else { - throw new Error(`Storage: error writing "${key}" due to eTag conflict.`) + throw ExceptionHelper.generateException(Error, Errors.StorageEtagConflict, undefined, { key }) } } } diff --git a/packages/agents-hosting/src/transcript/consoleTranscriptLogger.ts b/packages/agents-hosting/src/transcript/consoleTranscriptLogger.ts index 0bfd0a40..88598670 100644 --- a/packages/agents-hosting/src/transcript/consoleTranscriptLogger.ts +++ b/packages/agents-hosting/src/transcript/consoleTranscriptLogger.ts @@ -1,5 +1,6 @@ -import { Activity } from '@microsoft/agents-activity' +import { Activity, ExceptionHelper } from '@microsoft/agents-activity' import { TranscriptLogger } from './transcriptLogger' +import { Errors } from '../errorHelper' /** * A transcript logger that logs activities to the console. @@ -12,7 +13,7 @@ export class ConsoleTranscriptLogger implements TranscriptLogger { */ logActivity (activity: Activity): void | Promise { if (!activity) { - throw new Error('Activity is required.') + throw ExceptionHelper.generateException(Error, Errors.ActivityRequired) } console.log('Activity Log:', activity) diff --git a/packages/agents-hosting/src/transcript/fileTranscriptLogger.ts b/packages/agents-hosting/src/transcript/fileTranscriptLogger.ts index cf4164f4..8f0850e6 100644 --- a/packages/agents-hosting/src/transcript/fileTranscriptLogger.ts +++ b/packages/agents-hosting/src/transcript/fileTranscriptLogger.ts @@ -4,12 +4,13 @@ */ import { debug } from '@microsoft/agents-activity/logger' +import { Activity, ActivityTypes, ExceptionHelper } from '@microsoft/agents-activity' import { PagedResult, TranscriptInfo } from './transcriptLogger' import { TranscriptStore } from './transcriptStore' import * as fs from 'fs/promises' import * as path from 'path' import { EOL } from 'os' -import { Activity, ActivityTypes } from '@microsoft/agents-activity' +import { Errors } from '../errorHelper' const logger = debug('agents:file-transcript-logger') @@ -47,7 +48,7 @@ export class FileTranscriptLogger implements TranscriptStore { */ async logActivity (activity: Activity): Promise { if (!activity) { - throw new Error('activity is required.') + throw ExceptionHelper.generateException(Error, Errors.ActivityRequired) } const transcriptFile = this.getTranscriptFile(activity.channelId!, activity.conversation?.id!) @@ -328,11 +329,11 @@ export class FileTranscriptLogger implements TranscriptStore { */ private getTranscriptFile (channelId: string, conversationId: string): string { if (!channelId?.trim()) { - throw new Error('channelId is required.') + throw ExceptionHelper.generateException(Error, Errors.TranscriptChannelIdRequired) } if (!conversationId?.trim()) { - throw new Error('conversationId is required.') + throw ExceptionHelper.generateException(Error, Errors.TranscriptConversationIdRequired) } // Get invalid filename characters (cross-platform) @@ -353,7 +354,7 @@ export class FileTranscriptLogger implements TranscriptStore { */ private getChannelFolder (channelId: string): string { if (!channelId?.trim()) { - throw new Error('channelId is required.') + throw ExceptionHelper.generateException(Error, Errors.TranscriptChannelIdRequired) } const invalidChars = this.getInvalidPathChars() diff --git a/packages/agents-hosting/src/transcript/transcriptLoggerMiddleware.ts b/packages/agents-hosting/src/transcript/transcriptLoggerMiddleware.ts index b7bd0977..a5a957ad 100644 --- a/packages/agents-hosting/src/transcript/transcriptLoggerMiddleware.ts +++ b/packages/agents-hosting/src/transcript/transcriptLoggerMiddleware.ts @@ -2,8 +2,9 @@ import { TurnContext } from '../turnContext' import { ResourceResponse } from '../connector-client' import { Middleware } from '../middlewareSet' import { TranscriptLogger } from './transcriptLogger' -import { Activity, ActivityEventNames, ActivityTypes, ConversationReference, RoleTypes } from '@microsoft/agents-activity' +import { Activity, ActivityEventNames, ActivityTypes, ConversationReference, RoleTypes, ExceptionHelper } from '@microsoft/agents-activity' import { debug } from '@microsoft/agents-activity/logger' +import { Errors } from '../errorHelper' const appLogger = debug('agents:rest-client') @@ -20,7 +21,7 @@ export class TranscriptLoggerMiddleware implements Middleware { */ constructor (logger: TranscriptLogger) { if (!logger) { - throw new Error('TranscriptLoggerMiddleware requires a TranscriptLogger instance.') + throw ExceptionHelper.generateException(Error, Errors.TranscriptLoggerRequired) } this.logger = logger diff --git a/packages/agents-hosting/src/turnContext.ts b/packages/agents-hosting/src/turnContext.ts index 1f5d3bba..bf79c7b1 100644 --- a/packages/agents-hosting/src/turnContext.ts +++ b/packages/agents-hosting/src/turnContext.ts @@ -1,13 +1,14 @@ /** * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. */ import { INVOKE_RESPONSE_KEY } from './activityHandler' import { BaseAdapter } from './baseAdapter' -import { Activity, ActivityTypes, ConversationReference, DeliveryModes, InputHints } from '@microsoft/agents-activity' +import { Activity, ActivityTypes, ConversationReference, DeliveryModes, InputHints, ExceptionHelper } from '@microsoft/agents-activity' import { ResourceResponse } from './connector-client/resourceResponse' import { TurnContextStateCollection } from './turnContextStateCollection' import { AttachmentInfo } from './connector-client/attachmentInfo' import { AttachmentData } from './connector-client/attachmentData' import { StreamingResponse } from './app/streaming/streamingResponse' import { JwtPayload } from 'jsonwebtoken' +import { Errors } from './errorHelper' /** * Defines a handler for processing and sending activities. @@ -372,7 +373,7 @@ export class TurnContext { set responded (value: boolean) { if (!value) { - throw new Error("TurnContext: cannot set 'responded' to a value of 'false'.") + throw ExceptionHelper.generateException(Error, Errors.CannotSetRespondedToFalse) } this._respondedRef.responded = true } diff --git a/packages/agents-hosting/test/errorHelper.test.ts b/packages/agents-hosting/test/errorHelper.test.ts new file mode 100644 index 00000000..883e71fc --- /dev/null +++ b/packages/agents-hosting/test/errorHelper.test.ts @@ -0,0 +1,81 @@ +import assert from 'assert' +import { describe, it } from 'node:test' +import { AgentErrorDefinition } from '@microsoft/agents-activity' +import { Errors } from '../src/errorHelper' + +describe('Hosting Errors tests', () => { + it('should have MissingTurnContext error definition', () => { + const error = Errors.MissingTurnContext + + assert.strictEqual(error.code, -120000) + assert.strictEqual(error.description, 'Missing TurnContext parameter') + assert.strictEqual(error.helplink, 'https://aka.ms/M365AgentsErrorCodes/#{errorCode}') + }) + + it('should have KeysRequiredForReading error definition', () => { + const error = Errors.KeysRequiredForReading + + assert.strictEqual(error.code, -120040) + assert.strictEqual(error.description, 'Keys are required when reading.') + assert.strictEqual(error.helplink, 'https://aka.ms/M365AgentsErrorCodes/#{errorCode}') + }) + + it('should have ChangesRequiredForWriting error definition', () => { + const error = Errors.ChangesRequiredForWriting + + assert.strictEqual(error.code, -120041) + assert.strictEqual(error.description, 'Changes are required when writing.') + assert.strictEqual(error.helplink, 'https://aka.ms/M365AgentsErrorCodes/#{errorCode}') + }) + + it('should have all error codes in the correct range', () => { + const errorDefinitions = Object.values(Errors).filter( + 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 -120000 to -120299 + errorDefinitions.forEach(errorDef => { + assert.ok(errorDef.code < 0, `Error code ${errorDef.code} should be negative`) + assert.ok(errorDef.code >= -120299, `Error code ${errorDef.code} should be >= -120299`) + assert.ok(errorDef.code <= -120000, `Error code ${errorDef.code} should be <= -120000`) + }) + }) + + it('should have unique error codes', () => { + const errorDefinitions = Object.values(Errors).filter( + val => val && typeof val === 'object' && 'code' in val && 'description' in val && 'helplink' in val + ) 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 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.includes('{errorCode}'), + `Help link should contain {errorCode} token: ${errorDef.helplink}` + ) + assert.ok( + errorDef.helplink.startsWith('https://aka.ms/M365AgentsErrorCodes/#'), + `Help link should start with correct URL: ${errorDef.helplink}` + ) + }) + }) + + it('should have non-empty descriptions', () => { + 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.description.length > 0, 'Description should not be empty') + }) + }) +}) diff --git a/packages/agents-hosting/test/hosting/adapter/cloudAdapter.test.ts b/packages/agents-hosting/test/hosting/adapter/cloudAdapter.test.ts index f1cf6761..e9ba9feb 100644 --- a/packages/agents-hosting/test/hosting/adapter/cloudAdapter.test.ts +++ b/packages/agents-hosting/test/hosting/adapter/cloudAdapter.test.ts @@ -172,21 +172,24 @@ describe('CloudAdapter', function () { describe('sendActivities', function () { it('throws for bad args', async function () { // @ts-expect-error - await assert.rejects(cloudAdapter.sendActivities(undefined, []), { - name: 'TypeError', - message: '`context` parameter required' + await assert.rejects(cloudAdapter.sendActivities(undefined, []), (err: Error) => { + assert.ok(err.name === 'TypeError') + assert.ok(err.message.includes('`context` parameter required')) + return true }) // @ts-expect-error - await assert.rejects(cloudAdapter.sendActivities(new TurnContext(cloudAdapter), undefined), { - name: 'TypeError', - message: '`activities` parameter required' + await assert.rejects(cloudAdapter.sendActivities(new TurnContext(cloudAdapter), undefined), (err: Error) => { + assert.ok(err.name === 'TypeError') + assert.ok(err.message.includes('`activities` parameter required')) + return true }) // @ts-expect-error - await assert.rejects(cloudAdapter.sendActivities(new TurnContext(cloudAdapter), []), { - name: 'Error', - message: 'Expecting one or more activities, but the array was empty.' + await assert.rejects(cloudAdapter.sendActivities(new TurnContext(cloudAdapter), []), (err: Error) => { + assert.ok(err.name === 'Error') + assert.ok(err.message.includes('Expecting one or more activities, but the array was empty.')) + return true }) }) @@ -291,15 +294,14 @@ describe('CloudAdapter', function () { const { logic } = bootstrap() - const error = new Error('continueConversation: Invalid conversation reference object') - await assert.rejects( cloudAdapter.continueConversation(authentication.clientId as string, conversationReference, (context) => { logic(context) - - throw error }), - error + (err: Error) => { + assert.ok(err.message.includes('continueConversation: Invalid conversation reference object')) + return true + } ) }) }) diff --git a/packages/agents-hosting/test/hosting/adapter/turnContext.test.ts b/packages/agents-hosting/test/hosting/adapter/turnContext.test.ts index 271504cb..00e4811a 100644 --- a/packages/agents-hosting/test/hosting/adapter/turnContext.test.ts +++ b/packages/agents-hosting/test/hosting/adapter/turnContext.test.ts @@ -116,8 +116,9 @@ describe('TurnContext', { timeout: 5000 }, function () { it('should throw if you set responded to false.', function () { const context = new TurnContext(new SimpleAdapter(), testMessage) context.responded = true - assert.throws(() => (context.responded = false), { - message: "TurnContext: cannot set 'responded' to a value of 'false'." + assert.throws(() => (context.responded = false), (err: Error) => { + assert.ok(err.message.includes("TurnContext: cannot set 'responded' to a value of 'false'.")) + return true }) }) diff --git a/packages/agents-hosting/test/hosting/app/authorization.test.ts b/packages/agents-hosting/test/hosting/app/authorization.test.ts index dbad865b..3fd8f24a 100644 --- a/packages/agents-hosting/test/hosting/app/authorization.test.ts +++ b/packages/agents-hosting/test/hosting/app/authorization.test.ts @@ -17,7 +17,10 @@ describe('AgentApplication', () => { authorization: {} }) assert.equal(app.options.authorization, undefined) - }, { message: 'Storage is required for Authorization. Ensure that a storage provider is configured in the AgentApplication options.' }) + }, (err: Error) => { + assert.ok(err.message.includes('Storage is required for Authorization. Ensure that a storage provider is configured in the AgentApplication options.')) + return true + }) }) it('should not allow empty handlers', () => { @@ -27,7 +30,10 @@ describe('AgentApplication', () => { authorization: {} }) assert.equal(app.options.authorization, undefined) - }, { message: 'The AgentApplication.authorization does not have any auth handlers' }) + }, (err: Error) => { + assert.ok(err.message.includes('The AgentApplication.authorization does not have any auth handlers')) + return true + }) }) it('should initialize successfully with valid auth configuration', () => { @@ -46,14 +52,20 @@ describe('AgentApplication', () => { assert.throws(() => { const auth = app.authorization assert.equal(auth, undefined) - }, { message: 'The Application.authorization property is unavailable because no authorization options were configured.' }) + }, (err: Error) => { + assert.ok(err.message.includes('The Application.authorization property is unavailable because no authorization options were configured.')) + return true + }) }) it('should throw when registering onSignInSuccess without authorization', () => { const app = new AgentApplication() assert.throws(() => { app.onSignInSuccess(async () => {}) - }, { message: 'The Application.authorization property is unavailable because no authorization options were configured.' }) + }, (err: Error) => { + assert.ok(err.message.includes('The Application.authorization property is unavailable because no authorization options were configured.')) + return true + }) }) it('should support multiple auth handlers', () => { @@ -104,15 +116,18 @@ describe('AgentApplication', () => { } }) - it('should throw when using a non-existent auth handler id', () => { + it('should throw when using a non-existent auth handler id', async () => { const app = new AgentApplication({ storage: new MemoryStorage(), authorization: { testAuth: { name: 'test' } } }) - assert.rejects(async () => { + await assert.rejects(async () => { await app.authorization.getToken({} as any, 'nonExistinghandler') - }, { message: "Cannot find auth handler with ID 'nonExistinghandler'. Ensure it is configured in the agent application options." }) + }, (err: Error) => { + assert.ok(err.message.includes("Cannot find auth handler with ID 'nonExistinghandler'. Ensure it is configured in the agent application options.")) + return true + }) }) }) diff --git a/packages/agents-hosting/test/hosting/fileTranscriptLogger.test.ts b/packages/agents-hosting/test/hosting/fileTranscriptLogger.test.ts index 09711c1a..47709990 100644 --- a/packages/agents-hosting/test/hosting/fileTranscriptLogger.test.ts +++ b/packages/agents-hosting/test/hosting/fileTranscriptLogger.test.ts @@ -28,7 +28,10 @@ describe('FileTranscriptLogger', () => { it('should throw error when logging null activity', async () => { await assert.rejects( async () => await store.logActivity(null as any), - /activity is required/ + (err: Error) => { + assert.ok(err.message.toLowerCase().includes('activity is required')) + return true + } ) }) diff --git a/packages/agents-hosting/test/hosting/jwt-middleware.test.ts b/packages/agents-hosting/test/hosting/jwt-middleware.test.ts index fb66893e..ec050ca4 100644 --- a/packages/agents-hosting/test/hosting/jwt-middleware.test.ts +++ b/packages/agents-hosting/test/hosting/jwt-middleware.test.ts @@ -84,7 +84,11 @@ describe('authorizeJWT', () => { await authorizeJWT(config)(req as Request, res as Response, next) assert((res.status as sinon.SinonStub).calledOnceWith(401)) - assert((res.send as sinon.SinonStub).calledOnceWith({ 'jwt-auth-error': 'invalid token' })) + const sendCall = (res.send as sinon.SinonStub).getCall(0) + assert(sendCall, 'send should be called') + const sentData = sendCall.args[0] + assert(sentData['jwt-auth-error'], 'jwt-auth-error should be present') + assert(sentData['jwt-auth-error'].includes('invalid token'), `jwt-auth-error should contain 'invalid token', got: ${sentData['jwt-auth-error']}`) assert((next as sinon.SinonStub).notCalled) verifyStub.restore() diff --git a/packages/agents-hosting/test/hosting/storage/memoryStorage.test.ts b/packages/agents-hosting/test/hosting/storage/memoryStorage.test.ts index 74485fcf..2a71df97 100644 --- a/packages/agents-hosting/test/hosting/storage/memoryStorage.test.ts +++ b/packages/agents-hosting/test/hosting/storage/memoryStorage.test.ts @@ -13,9 +13,8 @@ describe('MemoryStorage', () => { it('should throw an error if keys are empty', async () => { await assert.rejects( async () => await memoryStorage.read([]), - { - name: 'ReferenceError', - message: 'Keys are required when reading.' + (err: Error) => { + return err.name === 'ReferenceError' && err.message.includes('Keys are required when reading.') } ) }) @@ -24,9 +23,8 @@ describe('MemoryStorage', () => { await assert.rejects( // @ts-expect-error async () => await memoryStorage.read(null), - { - name: 'ReferenceError', - message: 'Keys are required when reading.' + (err: Error) => { + return err.name === 'ReferenceError' && err.message.includes('Keys are required when reading.') } ) }) @@ -47,9 +45,8 @@ describe('MemoryStorage', () => { it('should throw an error if changes are not empty array', async () => { await assert.rejects( async () => await memoryStorage.write([]), - { - name: 'ReferenceError', - message: 'Changes are required when writing.' + (err: Error) => { + return err.name === 'ReferenceError' && err.message.includes('Changes are required when writing.') } ) }) @@ -58,9 +55,8 @@ describe('MemoryStorage', () => { await assert.rejects( // @ts-expect-error async () => await memoryStorage.write(null), - { - name: 'ReferenceError', - message: 'Changes are required when writing.' + (err: Error) => { + return err.name === 'ReferenceError' && err.message.includes('Changes are required when writing.') } ) }) @@ -84,9 +80,8 @@ describe('MemoryStorage', () => { await assert.rejects( async () => await memoryStorage.write({ key1: { value: 'conflict', eTag: 'invalid' } }), - { - name: 'Error', - message: 'Storage: error writing "key1" due to eTag conflict.' + (err: Error) => { + return err.name === 'Error' && err.message.includes('Storage: error writing "key1" due to eTag conflict.') } ) })