feat(framework): add agent support to @novu/framework SDK fixes NV-7358#10710
feat(framework): add agent support to @novu/framework SDK fixes NV-7358#10710
Conversation
…tEventEnum onNewMention now fires ON_MESSAGE directly. Mock handler updated accordingly. Ref: NV-7358 Made-with: Cursor
…gents Introduces the agent resource module with user-facing types (AgentMessage, AgentConversation, AgentSubscriber, etc.), the AgentContext interface, and the agent() factory function. Internal bridge protocol types are kept unexported. Ref: NV-7358 Made-with: Cursor
… serve() Phase 2: AgentContextImpl with read-only props, signal batching, reply/update/resolve/flush methods, and ApiKey auth for reply POST. Phase 3: AGENT_EVENT action, AGENT_ID/EVENT query keys, optional workflows on ServeHandlerOptions, agent storage/lookup on Client. Ref: NV-7358 Made-with: Cursor
Add AGENT_EVENT branch to POST action map: looks up agent, builds AgentContextImpl, ACKs immediately, runs handler in background. flush() called after both onMessage and onResolve as safety net. AgentEventEnum added to avoid hardcoded event strings. Ref: NV-7358 Made-with: Cursor
Remove mock-agent-handler.ts from tracking (stays on disk). Fix PostActionEnum exhaustiveness in client.ts logging maps. Add comprehensive agent tests: factory validation, ACK dispatch, signal batching with reply, independent update, onResolve flush, and context property pass-through. Ref: NV-7358 Made-with: Cursor
Replaces hand-crafted express route with agent() + serve() from the framework package, proving the SDK works end-to-end against a real Novu API and Slack integration. Ref: NV-7358 Made-with: Cursor
✅ Deploy Preview for dashboard-v2-novu-staging canceled.
|
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughAdds first-class agent support: new agent types and exports, AgentContextImpl implementation, an Changes
Sequence DiagramsequenceDiagram
participant Client as HTTP Client
participant Handler as NovuRequestHandler
participant Registry as Client.registeredAgents
participant Agent as RegisteredAgent
participant Context as AgentContextImpl
participant NovuAPI as Novu Reply API
Client->>Handler: POST /api/novu?agentId=...&event=...
Handler->>Registry: getAgent(agentId)
Registry-->>Handler: Agent | undefined
alt Agent not found
Handler->>Client: 404 { error: "agent not found" }
else Agent found
Handler->>Client: 200 { status: "ack" } (immediate)
par Async agent execution
Handler->>Context: new AgentContextImpl(request, secret)
Handler->>Agent: handlers.onMessage/onResolve(Context)
Agent->>Context: metadata.set(...), reply(text) / update(text)
Context->>NovuAPI: POST /agents/{id}/reply (Authorization: ApiKey ...)
NovuAPI-->>Context: 200 OK
Agent->>Context: resolve(summary)
Context->>NovuAPI: POST remaining signals + resolve (Authorization: ApiKey ...)
NovuAPI-->>Context: 200 OK
end
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~50 minutes Possibly related PRs
Suggested reviewers
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
🧹 Nitpick comments (4)
packages/framework/src/resources/agent/index.ts (1)
1-1: Consider the public API surface forAgentContextImpl.
AgentContextImplis an implementation class being exported publicly. If this is intended for internal framework use only (constructed by the handler), consider keeping it unexported from the public barrel and exporting only theAgentContextinterface. If users need to extend or instantiate it directly, the current export is appropriate.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/framework/src/resources/agent/index.ts` at line 1, AgentContextImpl is being exported from the public barrel but appears to be an internal implementation; decide whether it should remain public or be hidden: if it should be internal, remove the export of AgentContextImpl from the barrel and export only the AgentContext interface (or type) so consumers use the interface, and ensure any internal places that construct AgentContextImpl import it from its module directly (not the public barrel); if callers must instantiate/extend it, keep the current export but add documentation/comments indicating it is part of the public API (and export AgentContext as well) so intent is explicit.packages/framework/src/handler.ts (2)
29-33: Consider usinginterfacefor backend type definitions.Per coding guidelines, backend TypeScript files should use
interfacefor type definitions rather thantype.♻️ Suggested refactor
-export type ServeHandlerOptions = { +export interface ServeHandlerOptions { client?: Client; workflows?: Array<Workflow>; agents?: Array<Agent>; -}; +}As per coding guidelines: "On the backend: use
interfacefor type definitions"🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/framework/src/handler.ts` around lines 29 - 33, Change the exported type alias ServeHandlerOptions to an exported interface with the same name so backend code follows the interface convention; declare it as interface ServeHandlerOptions { client?: Client; workflows?: Workflow[]; agents?: Agent[]; } preserving optional properties and array types (Client, Workflow, Agent) and update any local references if necessary to continue using ServeHandlerOptions unchanged.
209-223: Add per-action request validation to improve error clarity.The TODO at line 179 acknowledges this gap: "add validation for body per action." Currently, the
bodyis cast toAgentBridgeRequestwithout validation, andAgentContextImplconstructor directly accesses properties without defensive checks. If required fields are missing or malformed, they silently becomeundefined, potentially causing confusing errors downstream. While errors are caught and logged, explicit validation would provide clearer error messages to SDK users.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/framework/src/handler.ts` around lines 209 - 223, PostActionEnum.AGENT_EVENT handler currently casts body to AgentBridgeRequest and constructs AgentContextImpl without validating required fields, causing unclear downstream errors; add explicit per-action validation of the incoming body (e.g., ensure required properties of AgentBridgeRequest are present and of correct types) before creating AgentContextImpl, and if validation fails return this.createResponse(HttpStatusEnum.BAD_REQUEST, { error: 'descriptive message' }); use the same handler symbols (PostActionEnum.AGENT_EVENT, AgentContextImpl, runAgentHandler, createResponse, this.client.getAgent) so the code verifies payload shape/mandatory fields and only constructs AgentContextImpl and calls runAgentHandler when validation passes.packages/framework/src/resources/agent/agent.context.ts (1)
117-131: Consider adding a timeout for fetch requests.The
_post()method has no timeout, which could cause handlers to hang indefinitely if the Novu API is unresponsive. This could lead to resource exhaustion under load.♻️ Suggested fix using AbortController
private async _post(body: AgentReplyPayload): Promise<void> { + const controller = new AbortController(); + const timeoutId = setTimeout(() => controller.abort(), 30000); + - const response = await fetch(this._replyUrl, { + const response = await fetch(this._replyUrl, { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: `ApiKey ${this._secretKey}`, }, body: JSON.stringify(body), + signal: controller.signal, }); + + clearTimeout(timeoutId); if (!response.ok) { const text = await response.text().catch(() => ''); throw new Error(`Agent reply failed (${response.status}): ${text}`); } }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/framework/src/resources/agent/agent.context.ts` around lines 117 - 131, The _post method lacks a network timeout; update AgentContext._post to use an AbortController: create an AbortController, start a setTimeout that calls controller.abort() after a configurable ms (e.g., replyTimeout), pass controller.signal into fetch, and clear the timeout when the fetch completes; ensure you catch an abort error and rethrow a clear Error indicating a timeout (while preserving other errors), and reference the AgentReplyPayload parameter and the _replyUrl/_secretKey fields when locating the method to modify.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Nitpick comments:
In `@packages/framework/src/handler.ts`:
- Around line 29-33: Change the exported type alias ServeHandlerOptions to an
exported interface with the same name so backend code follows the interface
convention; declare it as interface ServeHandlerOptions { client?: Client;
workflows?: Workflow[]; agents?: Agent[]; } preserving optional properties and
array types (Client, Workflow, Agent) and update any local references if
necessary to continue using ServeHandlerOptions unchanged.
- Around line 209-223: PostActionEnum.AGENT_EVENT handler currently casts body
to AgentBridgeRequest and constructs AgentContextImpl without validating
required fields, causing unclear downstream errors; add explicit per-action
validation of the incoming body (e.g., ensure required properties of
AgentBridgeRequest are present and of correct types) before creating
AgentContextImpl, and if validation fails return
this.createResponse(HttpStatusEnum.BAD_REQUEST, { error: 'descriptive message'
}); use the same handler symbols (PostActionEnum.AGENT_EVENT, AgentContextImpl,
runAgentHandler, createResponse, this.client.getAgent) so the code verifies
payload shape/mandatory fields and only constructs AgentContextImpl and calls
runAgentHandler when validation passes.
In `@packages/framework/src/resources/agent/agent.context.ts`:
- Around line 117-131: The _post method lacks a network timeout; update
AgentContext._post to use an AbortController: create an AbortController, start a
setTimeout that calls controller.abort() after a configurable ms (e.g.,
replyTimeout), pass controller.signal into fetch, and clear the timeout when the
fetch completes; ensure you catch an abort error and rethrow a clear Error
indicating a timeout (while preserving other errors), and reference the
AgentReplyPayload parameter and the _replyUrl/_secretKey fields when locating
the method to modify.
In `@packages/framework/src/resources/agent/index.ts`:
- Line 1: AgentContextImpl is being exported from the public barrel but appears
to be an internal implementation; decide whether it should remain public or be
hidden: if it should be internal, remove the export of AgentContextImpl from the
barrel and export only the AgentContext interface (or type) so consumers use the
interface, and ensure any internal places that construct AgentContextImpl import
it from its module directly (not the public barrel); if callers must
instantiate/extend it, keep the current export but add documentation/comments
indicating it is part of the public API (and export AgentContext as well) so
intent is explicit.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 0bbaa778-2df9-4b24-9904-6f539f6a099c
📒 Files selected for processing (14)
apps/api/src/app/agents/dtos/agent-event.enum.tsapps/api/src/app/agents/e2e/mock-agent-handler.tsapps/api/src/app/agents/services/chat-sdk.service.tspackages/framework/src/client.tspackages/framework/src/constants/action.constants.tspackages/framework/src/constants/http-query.constants.tspackages/framework/src/handler.tspackages/framework/src/index.tspackages/framework/src/resources/agent/agent.context.tspackages/framework/src/resources/agent/agent.resource.tspackages/framework/src/resources/agent/agent.test.tspackages/framework/src/resources/agent/agent.types.tspackages/framework/src/resources/agent/index.tspackages/framework/src/resources/index.ts
💤 Files with no reviewable changes (1)
- apps/api/src/app/agents/dtos/agent-event.enum.ts
- Fix type error in agent.test.ts: cast fetchMock to typeof fetch - Fix E2E credential mismatch: use signingSecret instead of apiKey - Fix E2E reference to removed ON_START enum, use ON_MESSAGE - Remove clear-text logging of secret key (CodeQL finding) Made-with: Cursor
commit: |
There was a problem hiding this comment.
Actionable comments posted: 3
🧹 Nitpick comments (3)
apps/api/src/app/agents/e2e/mock-agent-handler.ts (2)
39-39: Avoid relying on type assertion for numeric metadata.At Line 39,
as numberdoes not coerce at runtime. IfturnCountis persisted as a string, increment logic can become incorrect.Proposed hardening
- const turnCount = (ctx.conversation.metadata?.turnCount as number) ?? 0; + const turnCount = Number(ctx.conversation.metadata?.turnCount ?? 0);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/api/src/app/agents/e2e/mock-agent-handler.ts` at line 39, The code uses a type assertion "(ctx.conversation.metadata?.turnCount as number) ?? 0" which doesn't coerce runtime strings; change the logic that reads turnCount (the use of ctx.conversation.metadata?.turnCount in mock-agent-handler.ts) to explicitly coerce and validate the value (e.g., use Number(...) or parseInt(...,10) and guard against NaN/Infinity) and then default to 0, so increments operate on a real numeric value; update any increment logic that relies on turnCount to use the validated numeric variable.
66-69: Align strictAuthentication configuration with codebase pattern for test utilities.This E2E handler is a test utility run manually during development, but it should follow the same environment-based pattern used in
apps/api/src/app.module.ts:strictAuthentication: process.env.NODE_ENV === 'production' || process.env.NODE_ENV === 'dev'. This maintains consistency across the codebase and prevents accidental exposure if the utility is repurposed. (The main API already enforces signature verification in production; this is a refactor for parity in test tooling.)🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/api/src/app/agents/e2e/mock-agent-handler.ts` around lines 66 - 69, Update the Client instantiation in mock-agent-handler to use the same environment-based strictAuthentication logic as the rest of the codebase: replace the hardcoded strictAuthentication: false with strictAuthentication: process.env.NODE_ENV === 'production' || process.env.NODE_ENV === 'dev' while keeping secretKey: NOVU_SECRET_KEY and the Client(...) construct intact so the test utility aligns with app.module.ts behavior.packages/framework/src/resources/agent/agent.test.ts (1)
9-43: Use shared test helpers instead of a custom local harness.
createMockBridgeRequestplus repeated inline request-adapter stubs are effectively a custom harness. Please switch tolibs/testinghelpers to keep setup consistent and reduce duplication.As per coding guidelines: "Shared test utilities and setup helpers live in
libs/testing— use them instead of rolling custom harnesses."Also applies to: 79-95, 120-135, 152-168, 194-210, 238-254, 285-301
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/framework/src/resources/agent/agent.test.ts` around lines 9 - 43, Replace the local test harness by removing the createMockBridgeRequest function and the duplicated inline request-adapter stubs and instead import and use the shared helpers from libs/testing (e.g., the shared bridge/request factory and adapter stubs); update tests in agent.test.ts to call the shared helper (matching the shape of AgentBridgeRequest) and to reuse the common request-adapter stub utilities rather than constructing message/conversation/subscriber objects inline in the blocks referenced (including the other occurrences at the listed ranges), ensuring you adjust any overrides to the helper’s API and remove the local createMockBridgeRequest symbol and duplicated stubs so tests use the centralized libs/testing utilities.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@apps/api/src/app/agents/e2e/agent-webhook.e2e.ts`:
- Line 70: The test default for invokeInbound was changed to
AgentEventEnum.ON_MESSAGE but the downstream bridge verification still expects
AgentEventEnum.ON_START; update the bridge assertion to expect
AgentEventEnum.ON_MESSAGE (or alternatively restore invokeInbound's default to
AgentEventEnum.ON_START or pass AgentEventEnum.ON_START explicitly from
callers). Locate the invokeInbound function and the bridge
verification/assertion that references AgentEventEnum.ON_START and make the
expectation match the default AgentEventEnum.ON_MESSAGE so the test intent is
consistent.
In `@packages/framework/src/resources/agent/agent.test.ts`:
- Around line 66-70: The test setup replaces global.fetch in the beforeEach
(creating fetchMock and assigning it to global.fetch) but never restores it; add
an afterEach that saves the original fetch (e.g. const originalFetch =
global.fetch in the top-level scope) and restores global.fetch = originalFetch
after each test and also clear/reset the fetchMock (or call
vi.restoreAllMocks()/vi.resetAllMocks()) so the mocked fetch does not leak into
other suites; update the test file's setup/teardown around the
beforeEach/fetchMock and reference the existing beforeEach, fetchMock and client
symbols when adding the afterEach teardown.
- Around line 216-226: The test currently assumes ordering in
fetchMock.mock.calls by indexing replyCalls[0] and replyCalls[1]; instead parse
each call body in replyCalls and identify the update vs reply payloads by
checking for the presence of "update" or "reply" keys, then assert against the
matched payloads (assert update.text === 'Thinking...' and that update has no
signals, and assert reply.text === 'Done thinking' and reply.signals.length ===
1). Locate the logic around replyCalls, updateBody, and replyBody in the test
and replace the index-based assertions with a loop or find() that parses each
call[1].body and branches based on payload shape before making the expects.
---
Nitpick comments:
In `@apps/api/src/app/agents/e2e/mock-agent-handler.ts`:
- Line 39: The code uses a type assertion "(ctx.conversation.metadata?.turnCount
as number) ?? 0" which doesn't coerce runtime strings; change the logic that
reads turnCount (the use of ctx.conversation.metadata?.turnCount in
mock-agent-handler.ts) to explicitly coerce and validate the value (e.g., use
Number(...) or parseInt(...,10) and guard against NaN/Infinity) and then default
to 0, so increments operate on a real numeric value; update any increment logic
that relies on turnCount to use the validated numeric variable.
- Around line 66-69: Update the Client instantiation in mock-agent-handler to
use the same environment-based strictAuthentication logic as the rest of the
codebase: replace the hardcoded strictAuthentication: false with
strictAuthentication: process.env.NODE_ENV === 'production' ||
process.env.NODE_ENV === 'dev' while keeping secretKey: NOVU_SECRET_KEY and the
Client(...) construct intact so the test utility aligns with app.module.ts
behavior.
In `@packages/framework/src/resources/agent/agent.test.ts`:
- Around line 9-43: Replace the local test harness by removing the
createMockBridgeRequest function and the duplicated inline request-adapter stubs
and instead import and use the shared helpers from libs/testing (e.g., the
shared bridge/request factory and adapter stubs); update tests in agent.test.ts
to call the shared helper (matching the shape of AgentBridgeRequest) and to
reuse the common request-adapter stub utilities rather than constructing
message/conversation/subscriber objects inline in the blocks referenced
(including the other occurrences at the listed ranges), ensuring you adjust any
overrides to the helper’s API and remove the local createMockBridgeRequest
symbol and duplicated stubs so tests use the centralized libs/testing utilities.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 62f87172-52e5-429f-ab6a-d7cd6a8d640c
📒 Files selected for processing (4)
apps/api/src/app/agents/e2e/agent-webhook.e2e.tsapps/api/src/app/agents/e2e/helpers/agent-test-setup.tsapps/api/src/app/agents/e2e/mock-agent-handler.tspackages/framework/src/resources/agent/agent.test.ts
The signingSecret field was added to the ICredentials interface (PR #10707) but not to the Mongoose schema, causing it to be silently stripped on save. Made-with: Cursor
The test asserted ON_START which was removed from the enum. Made-with: Cursor
- Restore global.fetch in afterEach to prevent cross-suite mock leakage - Use payload-shape matching instead of index-based assertions for async calls - Convert ServeHandlerOptions from type to interface per backend convention Made-with: Cursor
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (1)
packages/framework/src/handler.ts (1)
35-40: MakeINovuRequestHandlerOptionsan interface extendingServeHandlerOptions.This exported options shape still uses a type alias and re-declares
client/workflows/agentsthat already exist onServeHandlerOptions. Converting it to an interface keeps the public API smaller and avoids the duplicated fields drifting apart again.♻️ Suggested cleanup
-export type INovuRequestHandlerOptions<Input extends any[] = any[], Output = any> = ServeHandlerOptions & { +export interface INovuRequestHandlerOptions<Input extends any[] = any[], Output = any> + extends ServeHandlerOptions { frameworkName: string; - client?: Client; - workflows?: Array<Workflow>; - agents?: Array<Agent>; handler: Handler<Input, Output>; -}; +}As per coding guidelines,
**/*.{ts,tsx}: On the backend: useinterfacefor type definitions.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/framework/src/handler.ts` around lines 35 - 40, Convert the exported INovuRequestHandlerOptions type alias into an interface that extends ServeHandlerOptions so you don't redeclare existing fields; remove duplicated properties client, workflows, and agents from the declaration and keep only the additional members (frameworkName and handler) while preserving the generic parameters, i.e., declare interface INovuRequestHandlerOptions<Input extends any[] = any[], Output = any> extends ServeHandlerOptions { frameworkName: string; handler: Handler<Input, Output>; } ensuring exports and imports remain unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@packages/framework/src/handler.ts`:
- Around line 293-300: runAgentHandler currently treats any event other than
AgentEventEnum.ON_RESOLVE as valid and calls registeredAgent.handlers.onMessage,
which wrongly routes unknown or misspelled events; update runAgentHandler to
explicitly validate the event against AgentEventEnum (e.g., check for ON_RESOLVE
and the expected ON_MESSAGE or other supported enum members) and if the event is
not one of the supported values return a 400 error (or throw a specific
BadRequest) instead of falling through to onMessage; ensure you reference
AgentEventEnum, runAgentHandler, registeredAgent.handlers.onResolve and
registeredAgent.handlers.onMessage when adding the guard so the code rejects
unsupported events before dispatching handlers.
- Around line 218-222: The ACK-first pattern can lose background work on edge
runtimes; modify the call to runAgentHandler so it uses runtime-aware background
scheduling when available: if the request context (ctx) exposes waitUntil/use
(check for ctx.waitUntil or similar), call
ctx.waitUntil(this.runAgentHandler(registeredAgent, agentEvent, ctx).catch(err
=> process/log...)) and then return createResponse(HttpStatusEnum.OK, { status:
'ack' }); otherwise await this.runAgentHandler(registeredAgent, agentEvent,
ctx).catch(err => process/log...) before returning; ensure you still call
ctx.flush() inside runAgentHandler or via the scheduled promise and keep error
logging consistent.
---
Nitpick comments:
In `@packages/framework/src/handler.ts`:
- Around line 35-40: Convert the exported INovuRequestHandlerOptions type alias
into an interface that extends ServeHandlerOptions so you don't redeclare
existing fields; remove duplicated properties client, workflows, and agents from
the declaration and keep only the additional members (frameworkName and handler)
while preserving the generic parameters, i.e., declare interface
INovuRequestHandlerOptions<Input extends any[] = any[], Output = any> extends
ServeHandlerOptions { frameworkName: string; handler: Handler<Input, Output>; }
ensuring exports and imports remain unchanged.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 46b63cee-a6c3-42f1-b037-b7285139d9fb
📒 Files selected for processing (2)
packages/framework/src/handler.tspackages/framework/src/resources/agent/agent.test.ts
🚧 Files skipped from review as they are similar to previous changes (1)
- packages/framework/src/resources/agent/agent.test.ts
…h to onMessage Made-with: Cursor
Server adapters can now provide an optional waitUntil callback to ensure agent handler completion on edge runtimes (Next.js Edge, Cloudflare Workers) that terminate after the response is sent. Made-with: Cursor
Summary
Adds conversational agent support to
@novu/framework, enabling developers to define agents withagent()and serve them alongside workflows viaserve(). The SDK handles the full bridge protocol: receiving events from the Novu API, providing a typedctxobject to handlers, and POSTing replies back.agent()primitive — factory function returning{ id, handlers }withonMessage(required) andonResolve(optional) handlersAgentContextImpl— thectxobject with read-only properties from the bridge payload,ctx.reply()(batches signals),ctx.update()(sends immediately),ctx.metadata.set(),ctx.trigger(),ctx.resolve(), and auto-flush()after handlers completeNovuRequestHandlerroutesPOST ?action=agent-eventto the registered agent, ACKs immediately, runs handler in backgroundON_START/ON_ACTIONfromAgentEventEnumctx.reply()→ API → Slack delivery, including resolve flowArchitecture
sequenceDiagram participant Slack participant API as Novu API participant SDK as serve() endpoint Slack->>API: @mention bot API->>API: Resolve subscriber, create conversation API->>SDK: POST ?action=agent-event&agentId=X&event=onMessage SDK-->>API: 200 { status: "ack" } Note over SDK: Run onMessage(ctx) in background SDK->>API: POST /agents/:id/reply (ApiKey auth) API->>Slack: Deliver reply messageFiles changed
resources/agent/agent.types.tsAgentMessage,AgentContext,AgentHandlers,Agent, bridge protocol types,AgentEventEnumresources/agent/agent.resource.tsagent()function with validationresources/agent/agent.context.tsAgentContextImpl— signal batching, reply/update POST, flushhandler.ts,client.ts,constants/AGENT_EVENTaction, agent storage/lookup, dispatch + background executionindex.ts,resources/index.tsagent,Agent,AgentContext,AgentHandlersresources/agent/agent.test.tsagent-event.enum.ts,chat-sdk.service.tse2e/mock-agent-handler.tsagent()+serve()from the SDKTest plan
vitest run src/resources/agent/agent.test.ts)Made with Cursor
Note
Medium Risk
Adds a new request action (
agent-event) that executes user-provided agent handlers and performs outboundfetchcalls to reply/flush signals; mistakes could impact reliability and security (auth headers/HMAC expectations). Also changes API-side event mapping for new mentions, which could alter conversation flows.Overview
Adds conversational agent support to
@novu/framework: a newagent()factory plus typed agent context (AgentContextImpl) that canreply,update,resolve, set metadata, and enqueue trigger signals.Extends
serve()/NovuRequestHandlerwith a newPOST ?action=agent-event&agentId=...&event=...route that looks up registered agents fromClient, ACKs immediately, runs handlers in the background, and thenflush()es any remaining signals back to the API.Cleans up API-side agent events by removing unused
ON_START/ON_ACTIONand dispatchingonNewMentionasON_MESSAGE, and adds an E2Emock-agent-handler.tsthat uses the new SDK agent API.Reviewed by Cursor Bugbot for commit 8c29d92. Configure here.
What changed
Added conversational agent support to @novu/framework: a new agent() factory, typed AgentContextImpl (reply/update/resolve/trigger/metadata), client-side agent registry, and handler routing to accept POST?action=agent-event (agentId + event). The request handler ACKs immediately, runs user agent handlers in the background, and flushes batched signals back to the Novu API. API enums were simplified (removed unused events) and integration credential persistence was extended to store signingSecret.
Affected areas
Key technical decisions
Testing
Added Vitest unit/integration tests covering agent factory, routing, metadata batching, update/reply/resolve flows (tests pass), and manual E2E Slack round-trip validation using the mock-agent-handler.