Skip to content

feat(js): add discriminated AgentContext types per handler fixes NV-7509#10974

Merged
ChmaraX merged 6 commits intonextfrom
cursor/discriminated-agent-context-types-d10b
May 4, 2026
Merged

feat(js): add discriminated AgentContext types per handler fixes NV-7509#10974
ChmaraX merged 6 commits intonextfrom
cursor/discriminated-agent-context-types-d10b

Conversation

@ChmaraX
Copy link
Copy Markdown
Contributor

@ChmaraX ChmaraX commented May 4, 2026

What changed

Introduces handler-specific discriminated context types for each agent handler callback, replacing the single generic AgentContext that all handlers previously shared.

New types

Handler Context type Guaranteed non-null fields
onMessage AgentMessageContext ctx.message, ctx.event === 'onMessage'
onAction AgentActionContext ctx.action, ctx.event === 'onAction'
onReaction AgentReactionContext ctx.reaction, ctx.event === 'onReaction'
onResolve AgentResolveContext ctx.event === 'onResolve' (minimal context)

Before

onAction: async (ctx) => {
  const actionId = ctx.action!.actionId; // non-null assertion needed
  const text = ctx.message?.text; // always null here, misleading
}

After

onAction: async (ctx) => {
  ctx.action.actionId; // non-null, no assertion needed
  // ctx.message — doesn't exist on type
}

Details

  • Each discriminated type extends a shared AgentContextBase interface (not exported) containing the common fields: conversation, subscriber, history, platform, platformContext, reply(), resolve(), metadata, trigger(), addReaction()
  • AgentHandlers now maps each handler to its specific context type
  • The original AgentContext is preserved with @deprecated for backward compatibility
  • handler.ts dispatches to the correct handler with a type-safe switch instead of a generic map
  • No new tests added — existing tests already cover the runtime behavior and typecheck validates the new types
  • All 49 tests pass with zero type errors
  • Package builds cleanly with all new types exported

Files changed

  • packages/framework/src/resources/agent/agent.types.ts — new discriminated types + deprecated AgentContext
  • packages/framework/src/resources/agent/index.ts — export new types
  • packages/framework/src/index.ts — export new types at package level
  • packages/framework/src/handler.ts — type-safe handler dispatch via switch

Linear Issue: NV-7509

Open in Web Open in Cursor 

What changed

Replaces the single generic AgentContext with four handler-specific discriminated context types (AgentMessageContext, AgentActionContext, AgentReactionContext, AgentResolveContext) built on a shared AgentContextBase, and updates AgentHandlers so each handler receives a narrowed context. Handler dispatch was made type-safe (switch on AgentEventEnum) to allow compile-time guarantees about available fields (e.g., message for onMessage), reducing runtime guards while preserving the original AgentContext as @deprecated for compatibility.

Affected areas

  • js (framework): Type-level refactor of agent APIs: new per-handler context types added and exported, AgentHandlers signatures updated, AgentContext turned into a discriminated union, AgentContextImpl no longer claims to implement AgentContext, and handler.ts now dispatches with a type-safe switch instead of relying on a generic map.

Key technical decisions

  • Introduced AgentContextBase plus literal-tagged per-event interfaces to leverage TypeScript narrowing for handler callbacks.
  • Switched from a dynamic handler-map lookup to an explicit AgentEventEnum switch to enable accurate compile-time context typing.
  • Kept the previous AgentContext exported and marked @deprecated to avoid breaking existing consumers while migrating to the new narrowed types.

Testing

No new tests added; existing framework tests cover runtime behavior and the type changes were validated via typecheck in CI (PR author reports all tests pass and zero type errors). Package builds and new types are exported at package level.

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 <adam.chmara1@gmail.com>
@linear-code
Copy link
Copy Markdown

linear-code Bot commented May 4, 2026

@netlify
Copy link
Copy Markdown

netlify Bot commented May 4, 2026

Deploy Preview for dashboard-v2-novu-staging canceled.

Name Link
🔨 Latest commit e4646b4
🔍 Latest deploy log https://app.netlify.com/projects/dashboard-v2-novu-staging/deploys/69f8b2fc8fce3b000931e115

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 4, 2026

📝 Walkthrough

Walkthrough

Replaces a generic AgentContext with event-discriminated context types (Message/Action/Reaction/Resolve), updates barrels to re-export the new types, removes the implements AgentContext clause from AgentContextImpl, and changes a handlerMap declaration to use a trailing as Partial<Record<...>> assertion.

Changes

Agent handler and types

Layer / File(s) Summary
Data Shape / Types
packages/framework/src/resources/agent/agent.types.ts
Adds AgentContextBase and four event-specific context interfaces (AgentMessageContext, AgentActionContext, AgentReactionContext, AgentResolveContext); defines AgentContext as their union; updates AgentHandlers to use event-specific context parameter types.
Public Exports
packages/framework/src/index.ts, packages/framework/src/resources/agent/index.ts
Re-exports the four new Agent*Context types from framework and agent barrels.
Core Wiring / Runtime Typing
packages/framework/src/handler.ts
handlerMap object literal now uses a trailing as Partial<Record<AgentEventEnum, ...>> type assertion instead of an explicit annotated const type. No change to handler invocation logic.
Implementation Class
packages/framework/src/resources/agent/agent.context.ts
Removed implements AgentContext from export class AgentContextImpl and removed AgentContext from import list.
Tests / Misc
packages/framework/src/resources/agent/agent.test.ts
Trailing blank line / whitespace termination change only.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Suggested labels

@novu/shared

Suggested reviewers

  • scopsy
🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Title check ⚠️ Warning Title follows Conventional Commits format with valid type and scope, but the Linear ticket reference is positioned incorrectly—it should end the title after the description, not be appended to it. Reformat title as 'feat(framework): add discriminated AgentContext types per handler fixes NV-7509' to comply with Conventional Commits standards and ensure proper Linear ticket reference positioning.
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ 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.

❤️ Share
Review rate limit: 6/8 reviews remaining, refill in 13 minutes and 17 seconds.

Comment @coderabbitai help to get the list of available commands and usage tips.

…est case

Co-authored-by: Adam Chmara <adam.chmara1@gmail.com>
Co-authored-by: Adam Chmara <adam.chmara1@gmail.com>
@ChmaraX ChmaraX marked this pull request as ready for review May 4, 2026 13:11
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 <adam.chmara1@gmail.com>
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 <adam.chmara1@gmail.com>
Co-authored-by: Adam Chmara <adam.chmara1@gmail.com>
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/framework/src/resources/agent/agent.context.ts (1)

250-253: ⚠️ Potential issue | 🟠 Major

The type assertion at line 317 bypasses structural type safety — add event-based validation and null-guards before handler dispatch.

The core concern is valid. AgentContextImpl declares nullable message, action, and reaction fields, but the discriminated handler types require these to be non-null:

  • AgentMessageContext.message: AgentMessage (non-nullable)
  • AgentActionContext.action: AgentAction (non-nullable)
  • AgentReactionContext.reaction: AgentReaction (non-nullable)

In handler.ts, line 317 uses a type assertion that falsely claims all handlers accept AgentContextImpl, bypassing TypeScript's type-safety. No null-checks exist between context creation (line 226) and handler invocation (line 325). If a malformed bridge request sends event='onMessage' with message: null, the handler will receive null and crash when accessing ctx.message.text, despite the type signature promising non-null.

Required fix: validate the event and bridge request data match before dispatch. For example, reject onMessage events when message is null, or narrow the context type based on the event enum before invoking the handler.

🤖 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 250 -
253, The handler dispatch currently asserts types and can pass nulls to
handlers; update AgentContextImpl dispatch logic to validate event + payload
before invoking handlers: inspect ctx.event and ensure required fields are
non-null (for event === 'onMessage' confirm ctx.message is present and
cast/narrow to AgentMessageContext, for 'onAction' confirm ctx.action, for
'onReaction' confirm ctx.reaction), reject or throw a clear error if the payload
is missing/invalid, and only then call the corresponding handler (referencing
AgentContextImpl, AgentMessageContext, AgentActionContext, AgentReactionContext
and the dispatch site where handlers are invoked) so TypeScript narrowing and
runtime null-guards guarantee non-null fields for each handler.
🧹 Nitpick comments (2)
packages/framework/src/resources/agent/agent.types.ts (1)

199-203: ⚡ Quick win

addReaction JSDoc example references ctx.reaction which doesn't exist on non-reaction contexts.

The example at lines 200–201 (ctx.addReaction(ctx.reaction!.messageId, 'check_mark')) lives on AgentContextBase, so it appears in IDE tooling for all four context types. Consumers of AgentMessageContext, AgentActionContext, and AgentResolveContext will see an example that references a property that does not exist on their type.

Consider replacing the example with a standalone messageId:

✏️ Proposed fix
-   * `@example`
-   *   ctx.addReaction(ctx.reaction!.messageId, 'check_mark');
-   *   await ctx.reply('Done!');
+   * `@example`
+   *   ctx.addReaction('<platform-message-id>', 'check_mark');
+   *   await ctx.reply('Done!');
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/framework/src/resources/agent/agent.types.ts` around lines 199 -
203, The JSDoc example for addReaction on AgentContextBase references
ctx.reaction which isn't present on
AgentMessageContext/AgentActionContext/AgentResolveContext; update the
addReaction JSDoc (method addReaction in AgentContextBase) to use a standalone
messageId variable (e.g., const messageId = '...'; ctx.addReaction(messageId,
'check_mark'); await ctx.reply('Done!')) so the example is valid for all context
types and remove any usage of ctx.reaction from the example.
packages/framework/src/resources/agent/agent.context.ts (1)

249-249: ⚡ Quick win

AgentContextImpl has no implements clause — type contract is unverified.

implements AgentContext was correctly removed since TypeScript forbids implementing a union type. However, AgentContextImpl structurally satisfies AgentContextBase but has no compile-time enforcement of that contract. If a required method (trigger, addReaction, resolve, etc.) is accidentally removed or its signature changed, TypeScript won't catch it until the cast in handler.ts is exercised.

Exporting AgentContextBase from agent.types.ts and adding implements AgentContextBase to this class would restore the safety net:

✏️ Proposed fix

In agent.types.ts:

-interface AgentContextBase {
+export interface AgentContextBase {

In agent.context.ts:

+import type { ..., AgentContextBase } from './agent.types';

-export class AgentContextImpl {
+export class AgentContextImpl implements AgentContextBase {
🤖 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` at line 249, Export
the AgentContextBase type from agent.types.ts (if not already exported) and add
an implements clause to the class declaration: change AgentContextImpl to
implement AgentContextBase; then adjust any method signatures in
AgentContextImpl (e.g., trigger, addReaction, resolve, etc.) to match the
AgentContextBase interface so the compiler enforces the contract and surface
mismatches during build.
🤖 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/resources/agent/agent.types.ts`:
- Line 225: Add a JSDoc deprecation tag to the exported AgentContext union:
update the declaration for AgentContext (the exported type named "AgentContext")
to include a top-line JSDoc comment with "@deprecated" and a short migration
hint (e.g., use AgentMessageContext | AgentActionContext | AgentReactionContext
| AgentResolveContext directly), so consumers are warned that the previous
unified access pattern is deprecated and should narrow to the specific context
types.

---

Outside diff comments:
In `@packages/framework/src/resources/agent/agent.context.ts`:
- Around line 250-253: The handler dispatch currently asserts types and can pass
nulls to handlers; update AgentContextImpl dispatch logic to validate event +
payload before invoking handlers: inspect ctx.event and ensure required fields
are non-null (for event === 'onMessage' confirm ctx.message is present and
cast/narrow to AgentMessageContext, for 'onAction' confirm ctx.action, for
'onReaction' confirm ctx.reaction), reject or throw a clear error if the payload
is missing/invalid, and only then call the corresponding handler (referencing
AgentContextImpl, AgentMessageContext, AgentActionContext, AgentReactionContext
and the dispatch site where handlers are invoked) so TypeScript narrowing and
runtime null-guards guarantee non-null fields for each handler.

---

Nitpick comments:
In `@packages/framework/src/resources/agent/agent.context.ts`:
- Line 249: Export the AgentContextBase type from agent.types.ts (if not already
exported) and add an implements clause to the class declaration: change
AgentContextImpl to implement AgentContextBase; then adjust any method
signatures in AgentContextImpl (e.g., trigger, addReaction, resolve, etc.) to
match the AgentContextBase interface so the compiler enforces the contract and
surface mismatches during build.

In `@packages/framework/src/resources/agent/agent.types.ts`:
- Around line 199-203: The JSDoc example for addReaction on AgentContextBase
references ctx.reaction which isn't present on
AgentMessageContext/AgentActionContext/AgentResolveContext; update the
addReaction JSDoc (method addReaction in AgentContextBase) to use a standalone
messageId variable (e.g., const messageId = '...'; ctx.addReaction(messageId,
'check_mark'); await ctx.reply('Done!')) so the example is valid for all context
types and remove any usage of ctx.reaction from the example.
🪄 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: b56f2d45-1855-4658-9485-c769ec3979b6

📥 Commits

Reviewing files that changed from the base of the PR and between dd42164 and e4646b4.

📒 Files selected for processing (3)
  • packages/framework/src/handler.ts
  • packages/framework/src/resources/agent/agent.context.ts
  • packages/framework/src/resources/agent/agent.types.ts
✅ Files skipped from review due to trivial changes (1)
  • packages/framework/src/handler.ts

Comment thread packages/framework/src/resources/agent/agent.types.ts
@ChmaraX ChmaraX merged commit e1d17db into next May 4, 2026
37 checks passed
@ChmaraX ChmaraX deleted the cursor/discriminated-agent-context-types-d10b branch May 4, 2026 15:08
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants