From fa101aff6afe163627b08d791d22e7689910c30b Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 4 May 2026 11:45:09 +0000 Subject: [PATCH 1/6] feat(js): add discriminated AgentContext types per handler fixes NV-7509 Introduce handler-specific context types (AgentMessageContext, AgentActionContext, AgentReactionContext, AgentResolveContext) so each handler callback receives only the fields guaranteed to be non-null. - AgentMessageContext: ctx.message is non-null, no action/reaction - AgentActionContext: ctx.action is non-null - AgentReactionContext: ctx.reaction is non-null - AgentResolveContext: minimal context, no message/action/reaction The original AgentContext is preserved as deprecated for backward compatibility. AgentHandlers now uses the narrowed types. Adds 4 new tests verifying type-safe handler dispatch. Co-authored-by: Adam Chmara --- packages/framework/src/handler.ts | 49 ++++-- packages/framework/src/index.ts | 4 + .../src/resources/agent/agent.test.ts | 154 +++++++++++++++++- .../src/resources/agent/agent.types.ts | 56 ++++++- .../framework/src/resources/agent/index.ts | 4 + 5 files changed, 244 insertions(+), 23 deletions(-) diff --git a/packages/framework/src/handler.ts b/packages/framework/src/handler.ts index 92d92ec8b84..12e78ddee20 100644 --- a/packages/framework/src/handler.ts +++ b/packages/framework/src/handler.ts @@ -21,7 +21,15 @@ import { SigningKeyNotFoundError, } from './errors'; import { isPlatformError } from './errors/guard.errors'; -import type { Agent, AgentBridgeRequest, MessageContent } from './resources/agent'; +import type { + Agent, + AgentActionContext, + AgentBridgeRequest, + AgentMessageContext, + AgentReactionContext, + AgentResolveContext, + MessageContent, +} from './resources/agent'; import { AgentContextImpl, AgentDeliveryError, AgentEventEnum } from './resources/agent'; import type { Awaitable, EventTriggerParams, Workflow } from './types'; import { createHmacSubtle, initApiClient } from './utils'; @@ -309,23 +317,38 @@ export class NovuRequestHandler { } private async runAgentHandler(registeredAgent: Agent, event: string, ctx: AgentContextImpl): Promise { - const handlerMap: Partial Awaitable>> = { - [AgentEventEnum.ON_MESSAGE]: registeredAgent.handlers.onMessage, - [AgentEventEnum.ON_REACTION]: registeredAgent.handlers.onReaction, - [AgentEventEnum.ON_ACTION]: registeredAgent.handlers.onAction, - [AgentEventEnum.ON_RESOLVE]: registeredAgent.handlers.onResolve, - }; - - if (!Object.prototype.hasOwnProperty.call(handlerMap, event)) { + if (!Object.values(AgentEventEnum).includes(event as AgentEventEnum)) { throw new InvalidActionError(event, AgentEventEnum); } - const handler = handlerMap[event as AgentEventEnum]; - if (handler) { - const result = await handler(ctx); - if (result != null) await ctx.reply(result); + let result: MessageContent | void; + + switch (event as AgentEventEnum) { + case AgentEventEnum.ON_MESSAGE: { + result = await registeredAgent.handlers.onMessage(ctx as unknown as AgentMessageContext); + break; + } + case AgentEventEnum.ON_ACTION: { + result = registeredAgent.handlers.onAction + ? await registeredAgent.handlers.onAction(ctx as unknown as AgentActionContext) + : undefined; + break; + } + case AgentEventEnum.ON_REACTION: { + result = registeredAgent.handlers.onReaction + ? await registeredAgent.handlers.onReaction(ctx as unknown as AgentReactionContext) + : undefined; + break; + } + case AgentEventEnum.ON_RESOLVE: { + result = registeredAgent.handlers.onResolve + ? await registeredAgent.handlers.onResolve(ctx as unknown as AgentResolveContext) + : undefined; + break; + } } + if (result != null) await ctx.reply(result); await ctx.flush(); } diff --git a/packages/framework/src/index.ts b/packages/framework/src/index.ts index fc76897d05b..b393cb75609 100644 --- a/packages/framework/src/index.ts +++ b/packages/framework/src/index.ts @@ -4,6 +4,7 @@ export { NovuRequestHandler, type ServeHandlerOptions } from './handler'; export type { Agent, AgentAction, + AgentActionContext, AgentAttachment, AgentBridgeRequest, AgentContext, @@ -12,9 +13,12 @@ export type { AgentHistoryEntry, AgentMessage, AgentMessageAuthor, + AgentMessageContext, AgentPlatformContext, AgentReaction, + AgentReactionContext, AgentReplyPayload, + AgentResolveContext, AgentSubscriber, CardChild, CardElement, diff --git a/packages/framework/src/resources/agent/agent.test.ts b/packages/framework/src/resources/agent/agent.test.ts index bb489992fc7..e0919cbb3bd 100644 --- a/packages/framework/src/resources/agent/agent.test.ts +++ b/packages/framework/src/resources/agent/agent.test.ts @@ -6,7 +6,7 @@ import { PostActionEnum } from '../../constants'; import { NovuRequestHandler } from '../../handler'; import { AgentDeliveryError } from './agent.errors'; import { agent } from './agent.resource'; -import type { AgentBridgeRequest } from './agent.types'; +import type { AgentActionContext, AgentBridgeRequest, AgentMessageContext, AgentReactionContext, AgentResolveContext } from './agent.types'; import { Button, Card, CardText } from './index'; function createMockBridgeRequest(overrides?: Partial): AgentBridgeRequest { @@ -1609,4 +1609,156 @@ describe('agent dispatch via NovuRequestHandler', () => { expect(JSON.parse(replyCalls[0][1].body).reply.markdown).toBe('Thinking…'); expect(JSON.parse(replyCalls[1][1].body).reply.markdown).toBe('Final answer'); }); + + it('should provide discriminated AgentMessageContext to onMessage', async () => { + let messageText: string | undefined; + + const testBot = agent('test-bot', { + onMessage: async (ctx: AgentMessageContext) => { + messageText = ctx.message.text; + await ctx.reply(ctx.message.text); + }, + }); + + const handler = new NovuRequestHandler({ + frameworkName: 'test', + agents: [testBot], + client, + handler: () => { + const body = createMockBridgeRequest(); + const url = new URL(`http://localhost?action=${PostActionEnum.AGENT_EVENT}&agentId=test-bot&event=onMessage`); + + return { + body: () => body, + headers: () => null, + method: () => 'POST', + url: () => url, + transformResponse: (res: any) => res, + }; + }, + }); + + await handler.createHandler()(); + await vi.waitFor(() => expect(messageText).toBeDefined()); + + expect(messageText).toBe('Hello bot!'); + }); + + it('should provide discriminated AgentActionContext to onAction', async () => { + let actionId: string | undefined; + + const testBot = agent('test-bot', { + onMessage: async () => {}, + onAction: async (ctx: AgentActionContext) => { + actionId = ctx.action.actionId; + await ctx.reply('handled'); + }, + }); + + const handler = new NovuRequestHandler({ + frameworkName: 'test', + agents: [testBot], + client, + handler: () => { + const body = createMockBridgeRequest({ + event: 'onAction', + action: { actionId: 'confirm', value: 'yes' }, + message: null, + }); + const url = new URL(`http://localhost?action=${PostActionEnum.AGENT_EVENT}&agentId=test-bot&event=onAction`); + + return { + body: () => body, + headers: () => null, + method: () => 'POST', + url: () => url, + transformResponse: (res: any) => res, + }; + }, + }); + + await handler.createHandler()(); + await vi.waitFor(() => expect(actionId).toBeDefined()); + + expect(actionId).toBe('confirm'); + }); + + it('should provide discriminated AgentReactionContext to onReaction', async () => { + let emojiName: string | undefined; + + const testBot = agent('test-bot', { + onMessage: async () => {}, + onReaction: async (ctx: AgentReactionContext) => { + emojiName = ctx.reaction.emoji.name; + await ctx.reply('reaction noted'); + }, + }); + + const handler = new NovuRequestHandler({ + frameworkName: 'test', + agents: [testBot], + client, + handler: () => { + const body = createMockBridgeRequest({ + event: 'onReaction', + message: null, + reaction: { + messageId: 'msg-reacted', + emoji: { name: 'thumbs_up' }, + added: true, + message: null, + }, + }); + const url = new URL(`http://localhost?action=${PostActionEnum.AGENT_EVENT}&agentId=test-bot&event=onReaction`); + + return { + body: () => body, + headers: () => null, + method: () => 'POST', + url: () => url, + transformResponse: (res: any) => res, + }; + }, + }); + + await handler.createHandler()(); + await vi.waitFor(() => expect(emojiName).toBeDefined()); + + expect(emojiName).toBe('thumbs_up'); + }); + + it('should provide discriminated AgentResolveContext to onResolve', async () => { + let resolveEvent: string | undefined; + + const testBot = agent('test-bot', { + onMessage: async () => {}, + onResolve: async (ctx: AgentResolveContext) => { + resolveEvent = ctx.event; + ctx.metadata.set('resolved', true); + }, + }); + + const handler = new NovuRequestHandler({ + frameworkName: 'test', + agents: [testBot], + client, + handler: () => { + const body = createMockBridgeRequest({ event: 'onResolve', message: null }); + const url = new URL(`http://localhost?action=${PostActionEnum.AGENT_EVENT}&agentId=test-bot&event=onResolve`); + + return { + body: () => body, + headers: () => null, + method: () => 'POST', + url: () => url, + transformResponse: (res: any) => res, + }; + }, + }); + + await handler.createHandler()(); + await vi.waitFor(() => expect(resolveEvent).toBeDefined()); + + expect(resolveEvent).toBe('onResolve'); + }); }); diff --git a/packages/framework/src/resources/agent/agent.types.ts b/packages/framework/src/resources/agent/agent.types.ts index abdccb7db52..85581d29bd8 100644 --- a/packages/framework/src/resources/agent/agent.types.ts +++ b/packages/framework/src/resources/agent/agent.types.ts @@ -142,11 +142,7 @@ export interface ReplyHandle { edit(content: MessageContent, options?: { files?: FileRef[] }): Promise; } -export interface AgentContext { - readonly event: string; - readonly action: AgentAction | null; - readonly message: AgentMessage | null; - readonly reaction: AgentReaction | null; +interface AgentContextBase { readonly conversation: AgentConversation; readonly subscriber: AgentSubscriber | null; readonly history: AgentHistoryEntry[]; @@ -207,11 +203,53 @@ export interface AgentContext { addReaction(messageId: string, emojiName: Emoji): void; } +export interface AgentMessageContext extends AgentContextBase { + readonly event: 'onMessage'; + readonly message: AgentMessage; +} + +export interface AgentActionContext extends AgentContextBase { + readonly event: 'onAction'; + readonly action: AgentAction; +} + +export interface AgentReactionContext extends AgentContextBase { + readonly event: 'onReaction'; + readonly reaction: AgentReaction; +} + +export interface AgentResolveContext extends AgentContextBase { + readonly event: 'onResolve'; +} + +/** + * @deprecated Use the handler-specific context types instead: + * `AgentMessageContext`, `AgentActionContext`, `AgentReactionContext`, `AgentResolveContext`. + */ +export interface AgentContext { + readonly event: string; + readonly action: AgentAction | null; + readonly message: AgentMessage | null; + readonly reaction: AgentReaction | null; + readonly conversation: AgentConversation; + readonly subscriber: AgentSubscriber | null; + readonly history: AgentHistoryEntry[]; + readonly platform: string; + readonly platformContext: AgentPlatformContext; + reply(content: MessageContent, options?: { files?: FileRef[] }): Promise; + resolve(summary?: string): void; + metadata: { + set(key: string, value: unknown): void; + }; + trigger(workflowId: string, opts?: { to?: TriggerRecipientsPayload; payload?: Record }): void; + addReaction(messageId: string, emojiName: Emoji): void; +} + export interface AgentHandlers { - onMessage: (ctx: AgentContext) => Awaitable; - onReaction?: (ctx: AgentContext) => Awaitable; - onAction?: (ctx: AgentContext) => Awaitable; - onResolve?: (ctx: AgentContext) => Awaitable; + onMessage: (ctx: AgentMessageContext) => Awaitable; + onReaction?: (ctx: AgentReactionContext) => Awaitable; + onAction?: (ctx: AgentActionContext) => Awaitable; + onResolve?: (ctx: AgentResolveContext) => Awaitable; } export interface Agent { diff --git a/packages/framework/src/resources/agent/index.ts b/packages/framework/src/resources/agent/index.ts index 3b6edfbc3b4..b42da939b0d 100644 --- a/packages/framework/src/resources/agent/index.ts +++ b/packages/framework/src/resources/agent/index.ts @@ -17,6 +17,7 @@ export { agent } from './agent.resource'; export type { Agent, AgentAction, + AgentActionContext, AgentAttachment, AgentBridgeRequest, AgentContext, @@ -25,9 +26,12 @@ export type { AgentHistoryEntry, AgentMessage, AgentMessageAuthor, + AgentMessageContext, AgentPlatformContext, AgentReaction, + AgentReactionContext, AgentReplyPayload, + AgentResolveContext, AgentSubscriber, EditPayload, FileRef, From cadc81d93eddd4dbce75ea4a9cd0e3010fdec4d9 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 4 May 2026 11:51:45 +0000 Subject: [PATCH 2/6] refactor(js): consolidate discriminated context tests into a single test case Co-authored-by: Adam Chmara --- .../src/resources/agent/agent.test.ts | 182 +++++------------- 1 file changed, 46 insertions(+), 136 deletions(-) diff --git a/packages/framework/src/resources/agent/agent.test.ts b/packages/framework/src/resources/agent/agent.test.ts index e0919cbb3bd..0b1f2b17cfb 100644 --- a/packages/framework/src/resources/agent/agent.test.ts +++ b/packages/framework/src/resources/agent/agent.test.ts @@ -1610,155 +1610,65 @@ describe('agent dispatch via NovuRequestHandler', () => { expect(JSON.parse(replyCalls[1][1].body).reply.markdown).toBe('Final answer'); }); - it('should provide discriminated AgentMessageContext to onMessage', async () => { - let messageText: string | undefined; + it('should provide discriminated context types to each handler', async () => { + const captured: { message?: string; action?: string; reaction?: string; resolve?: string } = {}; const testBot = agent('test-bot', { onMessage: async (ctx: AgentMessageContext) => { - messageText = ctx.message.text; - await ctx.reply(ctx.message.text); + captured.message = ctx.message.text; }, - }); - - const handler = new NovuRequestHandler({ - frameworkName: 'test', - agents: [testBot], - client, - handler: () => { - const body = createMockBridgeRequest(); - const url = new URL(`http://localhost?action=${PostActionEnum.AGENT_EVENT}&agentId=test-bot&event=onMessage`); - - return { - body: () => body, - headers: () => null, - method: () => 'POST', - url: () => url, - transformResponse: (res: any) => res, - }; - }, - }); - - await handler.createHandler()(); - await vi.waitFor(() => expect(messageText).toBeDefined()); - - expect(messageText).toBe('Hello bot!'); - }); - - it('should provide discriminated AgentActionContext to onAction', async () => { - let actionId: string | undefined; - - const testBot = agent('test-bot', { - onMessage: async () => {}, onAction: async (ctx: AgentActionContext) => { - actionId = ctx.action.actionId; - await ctx.reply('handled'); + captured.action = ctx.action.actionId; }, - }); - - const handler = new NovuRequestHandler({ - frameworkName: 'test', - agents: [testBot], - client, - handler: () => { - const body = createMockBridgeRequest({ - event: 'onAction', - action: { actionId: 'confirm', value: 'yes' }, - message: null, - }); - const url = new URL(`http://localhost?action=${PostActionEnum.AGENT_EVENT}&agentId=test-bot&event=onAction`); - - return { - body: () => body, - headers: () => null, - method: () => 'POST', - url: () => url, - transformResponse: (res: any) => res, - }; - }, - }); - - await handler.createHandler()(); - await vi.waitFor(() => expect(actionId).toBeDefined()); - - expect(actionId).toBe('confirm'); - }); - - it('should provide discriminated AgentReactionContext to onReaction', async () => { - let emojiName: string | undefined; - - const testBot = agent('test-bot', { - onMessage: async () => {}, onReaction: async (ctx: AgentReactionContext) => { - emojiName = ctx.reaction.emoji.name; - await ctx.reply('reaction noted'); + captured.reaction = ctx.reaction.emoji.name; }, - }); - - const handler = new NovuRequestHandler({ - frameworkName: 'test', - agents: [testBot], - client, - handler: () => { - const body = createMockBridgeRequest({ - event: 'onReaction', - message: null, - reaction: { - messageId: 'msg-reacted', - emoji: { name: 'thumbs_up' }, - added: true, - message: null, - }, - }); - const url = new URL(`http://localhost?action=${PostActionEnum.AGENT_EVENT}&agentId=test-bot&event=onReaction`); - - return { - body: () => body, - headers: () => null, - method: () => 'POST', - url: () => url, - transformResponse: (res: any) => res, - }; - }, - }); - - await handler.createHandler()(); - await vi.waitFor(() => expect(emojiName).toBeDefined()); - - expect(emojiName).toBe('thumbs_up'); - }); + onResolve: async (ctx: AgentResolveContext) => { + captured.resolve = ctx.event; + }, + }); + + const dispatch = (event: string, overrides?: Partial) => { + const h = new NovuRequestHandler({ + frameworkName: 'test', + agents: [testBot], + client, + handler: () => { + const body = createMockBridgeRequest({ event, ...overrides }); + const url = new URL( + `http://localhost?action=${PostActionEnum.AGENT_EVENT}&agentId=test-bot&event=${event}` + ); + + return { + body: () => body, + headers: () => null, + method: () => 'POST', + url: () => url, + transformResponse: (res: any) => res, + }; + }, + }); - it('should provide discriminated AgentResolveContext to onResolve', async () => { - let resolveEvent: string | undefined; + return h.createHandler()(); + }; - const testBot = agent('test-bot', { - onMessage: async () => {}, - onResolve: async (ctx: AgentResolveContext) => { - resolveEvent = ctx.event; - ctx.metadata.set('resolved', true); - }, - }); + await dispatch('onMessage'); + await vi.waitFor(() => expect(captured.message).toBeDefined()); + expect(captured.message).toBe('Hello bot!'); - const handler = new NovuRequestHandler({ - frameworkName: 'test', - agents: [testBot], - client, - handler: () => { - const body = createMockBridgeRequest({ event: 'onResolve', message: null }); - const url = new URL(`http://localhost?action=${PostActionEnum.AGENT_EVENT}&agentId=test-bot&event=onResolve`); + await dispatch('onAction', { action: { actionId: 'confirm', value: 'yes' }, message: null }); + await vi.waitFor(() => expect(captured.action).toBeDefined()); + expect(captured.action).toBe('confirm'); - return { - body: () => body, - headers: () => null, - method: () => 'POST', - url: () => url, - transformResponse: (res: any) => res, - }; - }, + await dispatch('onReaction', { + message: null, + reaction: { messageId: 'msg-1', emoji: { name: 'thumbs_up' }, added: true, message: null }, }); + await vi.waitFor(() => expect(captured.reaction).toBeDefined()); + expect(captured.reaction).toBe('thumbs_up'); - await handler.createHandler()(); - await vi.waitFor(() => expect(resolveEvent).toBeDefined()); - - expect(resolveEvent).toBe('onResolve'); + await dispatch('onResolve', { message: null }); + await vi.waitFor(() => expect(captured.resolve).toBeDefined()); + expect(captured.resolve).toBe('onResolve'); }); }); From dd421647cbca7a1100c19baf107e9ab4cb32a4a3 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 4 May 2026 11:54:49 +0000 Subject: [PATCH 3/6] refactor(js): remove redundant discriminated context test Co-authored-by: Adam Chmara --- .../src/resources/agent/agent.test.ts | 63 +------------------ 1 file changed, 1 insertion(+), 62 deletions(-) diff --git a/packages/framework/src/resources/agent/agent.test.ts b/packages/framework/src/resources/agent/agent.test.ts index 0b1f2b17cfb..78c9f731441 100644 --- a/packages/framework/src/resources/agent/agent.test.ts +++ b/packages/framework/src/resources/agent/agent.test.ts @@ -6,7 +6,7 @@ import { PostActionEnum } from '../../constants'; import { NovuRequestHandler } from '../../handler'; import { AgentDeliveryError } from './agent.errors'; import { agent } from './agent.resource'; -import type { AgentActionContext, AgentBridgeRequest, AgentMessageContext, AgentReactionContext, AgentResolveContext } from './agent.types'; +import type { AgentBridgeRequest } from './agent.types'; import { Button, Card, CardText } from './index'; function createMockBridgeRequest(overrides?: Partial): AgentBridgeRequest { @@ -1610,65 +1610,4 @@ describe('agent dispatch via NovuRequestHandler', () => { expect(JSON.parse(replyCalls[1][1].body).reply.markdown).toBe('Final answer'); }); - it('should provide discriminated context types to each handler', async () => { - const captured: { message?: string; action?: string; reaction?: string; resolve?: string } = {}; - - const testBot = agent('test-bot', { - onMessage: async (ctx: AgentMessageContext) => { - captured.message = ctx.message.text; - }, - onAction: async (ctx: AgentActionContext) => { - captured.action = ctx.action.actionId; - }, - onReaction: async (ctx: AgentReactionContext) => { - captured.reaction = ctx.reaction.emoji.name; - }, - onResolve: async (ctx: AgentResolveContext) => { - captured.resolve = ctx.event; - }, - }); - - const dispatch = (event: string, overrides?: Partial) => { - const h = new NovuRequestHandler({ - frameworkName: 'test', - agents: [testBot], - client, - handler: () => { - const body = createMockBridgeRequest({ event, ...overrides }); - const url = new URL( - `http://localhost?action=${PostActionEnum.AGENT_EVENT}&agentId=test-bot&event=${event}` - ); - - return { - body: () => body, - headers: () => null, - method: () => 'POST', - url: () => url, - transformResponse: (res: any) => res, - }; - }, - }); - - return h.createHandler()(); - }; - - await dispatch('onMessage'); - await vi.waitFor(() => expect(captured.message).toBeDefined()); - expect(captured.message).toBe('Hello bot!'); - - await dispatch('onAction', { action: { actionId: 'confirm', value: 'yes' }, message: null }); - await vi.waitFor(() => expect(captured.action).toBeDefined()); - expect(captured.action).toBe('confirm'); - - await dispatch('onReaction', { - message: null, - reaction: { messageId: 'msg-1', emoji: { name: 'thumbs_up' }, added: true, message: null }, - }); - await vi.waitFor(() => expect(captured.reaction).toBeDefined()); - expect(captured.reaction).toBe('thumbs_up'); - - await dispatch('onResolve', { message: null }); - await vi.waitFor(() => expect(captured.resolve).toBeDefined()); - expect(captured.resolve).toBe('onResolve'); - }); }); From c098a40f75c562a03208b8447c873408baf1a5f1 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 4 May 2026 14:48:02 +0000 Subject: [PATCH 4/6] refactor(js): replace deprecated AgentContext with union type Since agents are unreleased, no backward compat needed. AgentContext is now a union of the four discriminated types. AgentContextImpl drops the implements clause since it is internal. Co-authored-by: Adam Chmara --- .../src/resources/agent/agent.context.ts | 3 +-- .../src/resources/agent/agent.types.ts | 23 +------------------ 2 files changed, 2 insertions(+), 24 deletions(-) diff --git a/packages/framework/src/resources/agent/agent.context.ts b/packages/framework/src/resources/agent/agent.context.ts index e5ef2dce0ee..02deb21d914 100644 --- a/packages/framework/src/resources/agent/agent.context.ts +++ b/packages/framework/src/resources/agent/agent.context.ts @@ -5,7 +5,6 @@ import type { AddReactionPayload, AgentAction, AgentBridgeRequest, - AgentContext, AgentConversation, AgentHistoryEntry, AgentMessage, @@ -247,7 +246,7 @@ class ReplyHandleImpl implements ReplyHandle { } } -export class AgentContextImpl implements AgentContext { +export class AgentContextImpl { readonly event: string; readonly action: AgentAction | null; readonly message: AgentMessage | null; diff --git a/packages/framework/src/resources/agent/agent.types.ts b/packages/framework/src/resources/agent/agent.types.ts index 85581d29bd8..ff34a651195 100644 --- a/packages/framework/src/resources/agent/agent.types.ts +++ b/packages/framework/src/resources/agent/agent.types.ts @@ -222,28 +222,7 @@ export interface AgentResolveContext extends AgentContextBase { readonly event: 'onResolve'; } -/** - * @deprecated Use the handler-specific context types instead: - * `AgentMessageContext`, `AgentActionContext`, `AgentReactionContext`, `AgentResolveContext`. - */ -export interface AgentContext { - readonly event: string; - readonly action: AgentAction | null; - readonly message: AgentMessage | null; - readonly reaction: AgentReaction | null; - readonly conversation: AgentConversation; - readonly subscriber: AgentSubscriber | null; - readonly history: AgentHistoryEntry[]; - readonly platform: string; - readonly platformContext: AgentPlatformContext; - reply(content: MessageContent, options?: { files?: FileRef[] }): Promise; - resolve(summary?: string): void; - metadata: { - set(key: string, value: unknown): void; - }; - trigger(workflowId: string, opts?: { to?: TriggerRecipientsPayload; payload?: Record }): void; - addReaction(messageId: string, emojiName: Emoji): void; -} +export type AgentContext = AgentMessageContext | AgentActionContext | AgentReactionContext | AgentResolveContext; export interface AgentHandlers { onMessage: (ctx: AgentMessageContext) => Awaitable; From 30cb9ecfa231cf7ccc1730b7cc21dafb7be4a565 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 4 May 2026 14:50:07 +0000 Subject: [PATCH 5/6] refactor(js): restore simple handler map in runAgentHandler Use `any` for the internal dispatch map instead of per-case type casts. The type safety boundary is at the public AgentHandlers interface. Co-authored-by: Adam Chmara --- packages/framework/src/handler.ts | 50 +++++++++---------------------- 1 file changed, 14 insertions(+), 36 deletions(-) diff --git a/packages/framework/src/handler.ts b/packages/framework/src/handler.ts index 12e78ddee20..e5692528b42 100644 --- a/packages/framework/src/handler.ts +++ b/packages/framework/src/handler.ts @@ -21,15 +21,7 @@ import { SigningKeyNotFoundError, } from './errors'; import { isPlatformError } from './errors/guard.errors'; -import type { - Agent, - AgentActionContext, - AgentBridgeRequest, - AgentMessageContext, - AgentReactionContext, - AgentResolveContext, - MessageContent, -} from './resources/agent'; +import type { Agent, AgentBridgeRequest, MessageContent } from './resources/agent'; import { AgentContextImpl, AgentDeliveryError, AgentEventEnum } from './resources/agent'; import type { Awaitable, EventTriggerParams, Workflow } from './types'; import { createHmacSubtle, initApiClient } from './utils'; @@ -317,38 +309,24 @@ export class NovuRequestHandler { } private async runAgentHandler(registeredAgent: Agent, event: string, ctx: AgentContextImpl): Promise { - if (!Object.values(AgentEventEnum).includes(event as AgentEventEnum)) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const handlerMap: Partial Awaitable>> = { + [AgentEventEnum.ON_MESSAGE]: registeredAgent.handlers.onMessage, + [AgentEventEnum.ON_REACTION]: registeredAgent.handlers.onReaction, + [AgentEventEnum.ON_ACTION]: registeredAgent.handlers.onAction, + [AgentEventEnum.ON_RESOLVE]: registeredAgent.handlers.onResolve, + }; + + if (!Object.prototype.hasOwnProperty.call(handlerMap, event)) { throw new InvalidActionError(event, AgentEventEnum); } - let result: MessageContent | void; - - switch (event as AgentEventEnum) { - case AgentEventEnum.ON_MESSAGE: { - result = await registeredAgent.handlers.onMessage(ctx as unknown as AgentMessageContext); - break; - } - case AgentEventEnum.ON_ACTION: { - result = registeredAgent.handlers.onAction - ? await registeredAgent.handlers.onAction(ctx as unknown as AgentActionContext) - : undefined; - break; - } - case AgentEventEnum.ON_REACTION: { - result = registeredAgent.handlers.onReaction - ? await registeredAgent.handlers.onReaction(ctx as unknown as AgentReactionContext) - : undefined; - break; - } - case AgentEventEnum.ON_RESOLVE: { - result = registeredAgent.handlers.onResolve - ? await registeredAgent.handlers.onResolve(ctx as unknown as AgentResolveContext) - : undefined; - break; - } + const handler = handlerMap[event as AgentEventEnum]; + if (handler) { + const result = await handler(ctx); + if (result != null) await ctx.reply(result); } - if (result != null) await ctx.reply(result); await ctx.flush(); } From e4646b4fc4f9e49db8144fd1eef43a694f399796 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 4 May 2026 14:53:42 +0000 Subject: [PATCH 6/6] refactor(js): use single type assertion instead of any in handler map Co-authored-by: Adam Chmara --- packages/framework/src/handler.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/framework/src/handler.ts b/packages/framework/src/handler.ts index e5692528b42..e6e03976171 100644 --- a/packages/framework/src/handler.ts +++ b/packages/framework/src/handler.ts @@ -309,13 +309,12 @@ export class NovuRequestHandler { } private async runAgentHandler(registeredAgent: Agent, event: string, ctx: AgentContextImpl): Promise { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const handlerMap: Partial Awaitable>> = { + const handlerMap = { [AgentEventEnum.ON_MESSAGE]: registeredAgent.handlers.onMessage, [AgentEventEnum.ON_REACTION]: registeredAgent.handlers.onReaction, [AgentEventEnum.ON_ACTION]: registeredAgent.handlers.onAction, [AgentEventEnum.ON_RESOLVE]: registeredAgent.handlers.onResolve, - }; + } as Partial Awaitable>>; if (!Object.prototype.hasOwnProperty.call(handlerMap, event)) { throw new InvalidActionError(event, AgentEventEnum);