Skip to content

Add onSaveMessages callback#227

Open
ianmacartney wants to merge 3 commits intomainfrom
ian/on-save-messages-callback
Open

Add onSaveMessages callback#227
ianmacartney wants to merge 3 commits intomainfrom
ian/on-save-messages-callback

Conversation

@ianmacartney
Copy link
Member

@ianmacartney ianmacartney commented Feb 19, 2026

This PR adds a new onSaveMessages callback feature that allows developers to execute custom logic whenever messages are saved to a thread. The callback is invoked within the same transaction as the message save, making it transactional.

Key changes:

  • Added SaveMessagesHandler and SaveMessagesCallbackArgs types
  • Updated the Agent configuration to accept an onSaveMessages callback
  • Modified message saving logic to invoke the callback when messages are saved
  • Updated Convex syntax in rules documentation to match latest API patterns
  • Updated Convex dependency to version 1.31.2

Example usage:

const agent = new Agent(components.agent, {
  name: "myAgent",
  languageModel: openai.chat("gpt-4o-mini"),
  onSaveMessages: internal.myModule.onNewMessages,
});

The callback receives the thread ID and the saved messages, allowing for side effects like updating counters, creating notifications, or syncing with external systems.

Summary by CodeRabbit

  • New Features

    • Optional onSaveMessages callback for save operations, with public types and wiring so callers can provide custom handlers when messages are persisted.
    • Message save flow can invoke an optional follow-up mutation after saving messages.
  • Chores

    • Bumped Convex dependency to v1.31.2.
    • Added TypeScript config for projects.
  • Documentation

    • Clarified wording, added examples (including a basic app example) and expanded usage snippets.

@coderabbitai
Copy link

coderabbitai bot commented Feb 19, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 9b3ea21 and 6440fbf.

📒 Files selected for processing (1)
  • src/client/types.ts

📝 Walkthrough

Walkthrough

Adds an optional onSaveMessages callback path: new types and client API wiring (createFunctionHandle), pass-through to an updated addMessages mutation that conditionally invokes a provided callback via ctx.runMutation; also updates docs, Convex dependency, and TypeScript config examples.

Changes

Cohort / File(s) Summary
Client types & exports
src/client/types.ts, src/client/index.ts
Introduce SaveMessagesCallbackArgs and SaveMessagesHandler; add onSaveMessages?: SaveMessagesHandler to Config and Options; export new types and import FunctionReference.
Client callsites
src/client/messages.ts, src/client/start.ts
Add optional onSaveMessages to SaveMessages args; convert provided callback to a serializable handle via createFunctionHandle and forward onSaveMessages to backend save flow.
Server mutation
src/component/messages.ts
addMessages mutation gains optional onSaveMessages: string arg; after persisting messages, conditionally invoke the referenced callback mutation via ctx.runMutation with { userId, threadId, messages }.
Docs & config
.cursor/rules/convex_rules.mdc, package.json, convex/tsconfig.json, App.tsx
Docs updated with examples and table-name usages; Convex dependency bumped to ^1.31.2; added project tsconfig.json files and example App.tsx content.
Miscellaneous usages
(examples in docs: message handling, system storage access)
Examples updated to use table-qualified DB calls (e.g., ctx.db.get("users", id)), system table name ("_storage"), and expanded sample code for message load/save flows.

Sequence Diagram(s)

sequenceDiagram
  participant Client as Client UI
  participant Lib as Client Library
  participant Server as Convex Mutation (addMessages)
  participant DB as Database
  participant Callback as Callback Mutation

  Client->>Lib: startGeneration / saveMessages (onSaveMessages fn)
  Lib->>Lib: createFunctionHandle(onSaveMessages) -> handle
  Lib->>Server: call addMessages(..., onSaveMessages: handle)
  Server->>DB: persist messages
  alt messages saved and onSaveMessages provided
    Server->>Callback: ctx.runMutation(onSaveMessages, { userId, threadId, messages })
    Callback->>DB: perform callback-side writes/logic
    DB-->>Callback: ack
    Callback-->>Server: result
  end
  Server-->>Lib: { messages: [...] }
  Lib-->>Client: returned saved messages
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐰 I hopped a handle, light and keen,
Threaded through layers, seldom seen,
A callback tucked in message seeds,
Sprouting runs where logic needs,
Hooray — messages and gentle deeds!

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Add onSaveMessages callback' directly and clearly describes the main change—introducing a new onSaveMessages callback feature. The changeset confirms this is the primary objective across multiple files.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch ian/on-save-messages-callback

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

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

Copy link
Member Author

This stack of pull requests is managed by Graphite. Learn more about stacking.

@ianmacartney ianmacartney changed the title codegen add onSaveM Feb 19, 2026
@ianmacartney ianmacartney changed the title add onSaveM Add onSaveMessages callback Feb 19, 2026
@pkg-pr-new
Copy link

pkg-pr-new bot commented Feb 19, 2026

Open in StackBlitz

npm i https://pkg.pr.new/get-convex/agent/@convex-dev/agent@227

commit: 9b3ea21

@ianmacartney ianmacartney marked this pull request as ready for review February 19, 2026 08:26
Copy link

@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 (2)
src/component/messages.ts (1)

377-429: ⚠️ Potential issue | 🟠 Major

finalizeMessage doesn't propagate onSaveMessages, silently skipping the callback during streaming finalization.

When finalizeMessage is called and the streamed content has accumulated (lines 397–414), it calls addMessagesHandler internally without onSaveMessages. This path runs during streamText completion when the pending message hasn't been filled in yet, meaning messages can be written to the thread through this path without triggering the configured callback. This contradicts the documented guarantee that the callback fires for streamText.

🐛 Proposed fix
 export const finalizeMessage = mutation({
   args: {
     messageId: v.id("messages"),
+    onSaveMessages: v.optional(v.string()),
     result: v.union(
       v.object({ status: v.literal("success") }),
       v.object({ status: v.literal("failed"), error: v.string() }),
     ),
   },
   ...
   handler: async (ctx, { messageId, result }) => {
     ...
     if (!message.message?.content.length) {
       const messages = await getStreamingMessagesWithMetadata(ctx, message, result);
       if (messages.length > 0) {
         await addMessagesHandler(ctx, {
           messages,
           threadId: message.threadId,
           agentName: message.agentName,
           failPendingSteps: false,
           pendingMessageId: messageId,
           userId: message.userId,
           embeddings: undefined,
+          onSaveMessages: args.onSaveMessages,
         });
         return;
       }
     }

The onSaveMessages handle would need to be threaded from the client callers (component.messages.finalizeMessage) in startGeneration's fail closure and any other call sites.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/component/messages.ts` around lines 377 - 429, finalizeMessage currently
calls addMessagesHandler when streaming produced messages (in the block that
checks message.message?.content length) but does not pass the onSaveMessages
callback, so saved messages via this streaming-finalization path skip the
configured onSaveMessages hook; update finalizeMessage to accept and forward an
onSaveMessages parameter to addMessagesHandler (threading the onSaveMessages
argument through the finalizeMessage mutation signature and into the call to
addMessagesHandler), and update all callers (e.g., the startGeneration failure
closure and any other places invoking component.messages.finalizeMessage) to
pass their onSaveMessages handler through so the callback is invoked for
streamed completions as well.
src/client/index.ts (1)

1008-1056: ⚠️ Potential issue | 🟡 Minor

saveStep and saveObject don't propagate onSaveMessages.

Both methods call ctx.runMutation(this.component.messages.addMessages, ...) directly without forwarding this.options.onSaveMessages. If users call agent.saveStep(...) or agent.saveObject(...), the configured callback will silently not fire. The other explicit save methods (saveMessage, saveMessages, asSaveMessagesMutation) all correctly propagate it via the saveMessages helper path.

If these methods are intentionally excluded from the callback contract, a brief comment to that effect would prevent confusion.

Also applies to: 1065-1107

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/client/index.ts` around lines 1008 - 1056, The saveStep (and likewise
saveObject) method currently calls
ctx.runMutation(this.component.messages.addMessages, ...) directly and thus does
not forward the configured onSaveMessages callback; update saveStep and
saveObject to propagate this.options.onSaveMessages into the mutation invocation
(or refactor to call the existing saveMessages helper/asSaveMessagesMutation
path that already forwards onSaveMessages) so the user-provided callback fires
when these APIs are used; reference the saveStep and saveObject methods and
ensure the mutation payload includes the onSaveMessages handler from
this.options or that the helper path is reused; if omission was intentional, add
a short clarifying comment in those methods noting they do not invoke
onSaveMessages.
🧹 Nitpick comments (2)
src/client/messages.ts (1)

221-252: Standalone saveMessage silently drops the onSaveMessages callback.

When saveMessage calls saveMessages (line 238), it doesn't forward an onSaveMessages handler. SaveMessageArgs doesn't include the field either. This means the callback is only triggered via the Agent class path (Agent.saveMessage → this.saveMessages). Callers using the standalone saveMessage export directly cannot attach the callback, even though the type docs promise coverage for saveMessage.

💡 Proposed fix — add `onSaveMessages` to `SaveMessageArgs` and forward it
 export type SaveMessageArgs = {
   threadId: string;
   userId?: string | null;
   promptMessageId?: string;
   metadata?: Omit<MessageWithMetadata, "message">;
   embedding?: { vector: number[]; model: string };
   pendingMessageId?: string;
+  /**
+   * Optional callback mutation to invoke after the message is saved.
+   * Called within the same transaction as the message save.
+   */
+  onSaveMessages?: SaveMessagesHandler;
 } & ( ... );

And in saveMessage:

   const { messages } = await saveMessages(ctx, component, {
     ...
     embeddings,
+    onSaveMessages: args.onSaveMessages,
   });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/client/messages.ts` around lines 221 - 252, The standalone saveMessage
function currently drops the onSaveMessages callback when delegating to
saveMessages; update the SaveMessageArgs type to include an optional
onSaveMessages callback and pass args.onSaveMessages through in the call to
saveMessages (i.e., include onSaveMessages: args.onSaveMessages in the options
object passed to saveMessages) so callers using the exported saveMessage receive
the same callback behavior as Agent.saveMessage → this.saveMessages.
src/client/start.ts (1)

96-107: onSaveMessages is declared redundantly in the startGeneration options type.

onSaveMessages?: SaveMessagesHandler is already part of both Options (via the new field at line 678 of types.ts) and Config (line 137 of types.ts), which are intersected into this parameter. The additional explicit declaration at line 106 is redundant.

♻️ Proposed cleanup
   {
     threadId,
     ...opts
   }: Options &
     Config & {
       userId?: string | null;
       threadId?: string;
       languageModel?: LanguageModel;
       agentName: string;
       agentForToolCtx?: Agent;
-      onSaveMessages?: SaveMessagesHandler;
     },
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/client/start.ts` around lines 96 - 107, The parameter type for the start
generation function currently redundantly re-declares onSaveMessages in the
destructured options object; remove the explicit onSaveMessages?:
SaveMessagesHandler from the parameter intersection so the function relies on
the existing onSaveMessages field defined in Options and Config, keeping the
rest of the destructured props (threadId, userId, languageModel, agentName,
agentForToolCtx, etc.) unchanged and ensuring the signature uses the existing
Options & Config intersection only.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/client/types.ts`:
- Around line 338-391: SaveMessagesCallbackArgs currently defines only threadId
and messages but the code invokes the handler with an extra userId field,
causing runtime validation failures; update SaveMessagesCallbackArgs to include
userId as an optional string (userId?: string) so the declared argument shape
matches what components pass, and ensure SaveMessagesHandler
(FunctionReference<...>) uses the updated SaveMessagesCallbackArgs type so
mutation validators and TypeScript stay in sync with the invocation that passes
{ userId, threadId, messages }.

---

Outside diff comments:
In `@src/client/index.ts`:
- Around line 1008-1056: The saveStep (and likewise saveObject) method currently
calls ctx.runMutation(this.component.messages.addMessages, ...) directly and
thus does not forward the configured onSaveMessages callback; update saveStep
and saveObject to propagate this.options.onSaveMessages into the mutation
invocation (or refactor to call the existing saveMessages
helper/asSaveMessagesMutation path that already forwards onSaveMessages) so the
user-provided callback fires when these APIs are used; reference the saveStep
and saveObject methods and ensure the mutation payload includes the
onSaveMessages handler from this.options or that the helper path is reused; if
omission was intentional, add a short clarifying comment in those methods noting
they do not invoke onSaveMessages.

In `@src/component/messages.ts`:
- Around line 377-429: finalizeMessage currently calls addMessagesHandler when
streaming produced messages (in the block that checks message.message?.content
length) but does not pass the onSaveMessages callback, so saved messages via
this streaming-finalization path skip the configured onSaveMessages hook; update
finalizeMessage to accept and forward an onSaveMessages parameter to
addMessagesHandler (threading the onSaveMessages argument through the
finalizeMessage mutation signature and into the call to addMessagesHandler), and
update all callers (e.g., the startGeneration failure closure and any other
places invoking component.messages.finalizeMessage) to pass their onSaveMessages
handler through so the callback is invoked for streamed completions as well.

---

Duplicate comments:
In `@src/component/messages.ts`:
- Around line 310-325: The onSaveMessages invocation passes userId via
ctx.runMutation (see onSaveMessages and the call in the messages save path), but
the exported type SaveMessagesCallbackArgs lacks userId; update the
SaveMessagesCallbackArgs type (in src/client/types.ts) to include userId?:
string so runtime validation accepts the extra field and the TypeScript type
matches the JSDoc/example; ensure the optional modifier matches the example
(userId optional) so existing callbacks remain compatible.

---

Nitpick comments:
In `@src/client/messages.ts`:
- Around line 221-252: The standalone saveMessage function currently drops the
onSaveMessages callback when delegating to saveMessages; update the
SaveMessageArgs type to include an optional onSaveMessages callback and pass
args.onSaveMessages through in the call to saveMessages (i.e., include
onSaveMessages: args.onSaveMessages in the options object passed to
saveMessages) so callers using the exported saveMessage receive the same
callback behavior as Agent.saveMessage → this.saveMessages.

In `@src/client/start.ts`:
- Around line 96-107: The parameter type for the start generation function
currently redundantly re-declares onSaveMessages in the destructured options
object; remove the explicit onSaveMessages?: SaveMessagesHandler from the
parameter intersection so the function relies on the existing onSaveMessages
field defined in Options and Config, keeping the rest of the destructured props
(threadId, userId, languageModel, agentName, agentForToolCtx, etc.) unchanged
and ensuring the signature uses the existing Options & Config intersection only.

The onSaveMessages callback is invoked with userId, threadId, and messages,
but the SaveMessagesCallbackArgs type was missing the userId field. This
caused a type-runtime mismatch where users adding userId to their validator
would get TypeScript errors even though the runtime passes this field.

Added userId?: string to SaveMessagesCallbackArgs to match the actual
invocation shape in src/component/messages.ts.

Co-authored-by: Ian Macartney <ianmacartney@users.noreply.github.com>
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