Skip to content

Conversation

@jakevollkommer
Copy link

Closes #2634

✅ Checklist

  • I have followed every step in the contributing guide
  • The PR title follows the convention.
  • I ran and tested the code works

Summary

Adds a custom transport implementation for AI SDK's useChat hook that integrates with Trigger.dev background tasks. This enables long-running AI conversations by triggering tasks, subscribing to realtime run updates via Electric SQL, and streaming AI responses via Server-Sent Events (SSE).

Changes

  • New Hook: useTriggerChat - A drop-in replacement for AI SDK's useChat that works with Trigger.dev tasks
  • Transport Class: TriggerChatTransport - Custom transport implementation following AI SDK's transport pattern
  • Dependencies: Added ai (^5.0.82), @ai-sdk/react (^2.0.14), and eventsource-parser (^3.0.0)

Technical Details

Architecture

Developer Requirements

Important: Developers must create their own server action to trigger tasks. Since useTriggerChat is a client-side hook, it cannot directly call tasks.trigger() (which requires server-side execution). The triggerTask option expects a server action that:

  1. Accepts the task identifier and payload
  2. Calls tasks.trigger() on the server
  3. Returns { success: true, runId, publicAccessToken }

Error Handling

  • Gracefully handles stream disconnections and abort signals
  • Warns on unparseable SSE chunks without breaking the stream
  • Only closes controller when run finishes (not when individual streams end)

Testing

Manually tested with a local project using pnpm patch to verify:

  • ✅ Task triggering and run creation
  • ✅ Realtime run status updates
  • ✅ SSE streaming of AI responses
  • ✅ Multiple concurrent streams
  • ✅ Graceful handling of stream completion
  • ✅ TypeScript compilation

Usage Example

1. Define your Trigger.dev task (e.g. src/trigger/chat.ts):

import { metadata, task } from "@trigger.dev/sdk/v3";
import { streamText } from "ai";
import { openai } from "@ai-sdk/openai";

export const chatTask = task({
  id: "chat",
  run: async ({ messages }: { messages: Array<{ role: string; content: string }> }) => {
    const result = streamText({
      model: openai("gpt-5"),
      messages,
    });

    // CRITICAL: Stream the result to the client using metadata.stream()
    // The stream key MUST match the streamKey option in useTriggerChat (default: "chat")
    await metadata.stream("chat", result.toUIMessageStream());

    const text = await result.text;
    return { text };
  },
});

2. Create a server action (e.g. src/actions.ts):

"use server";

import { tasks } from "@trigger.dev/sdk/v3";

export async function triggerChatTask(task: string, payload: unknown) {
  const handle = await tasks.trigger(task, payload);
  return {
    success: true,
    runId: handle.id,
    publicAccessToken: handle.publicAccessToken,
  };
}

3. Use the hook in your component (e.g. src/components/Chat.tsx):

"use client";

import { useTriggerChat } from "@trigger.dev/react-hooks";
import { chatTask } from "../trigger/chat";
import { triggerChatTask } from "../actions";

export function Chat() {
  const { messages, input, handleInputChange, handleSubmit } = useTriggerChat({
    triggerTask: triggerChatTask,
  });

  return (
    <form onSubmit={handleSubmit}>
      <div>
        {messages.map((msg) => (
          <div key={msg.id}>
            <strong>{msg.role}:</strong> {msg.content}
          </div>
        ))}
      </div>
      <input value={input} onChange={handleInputChange} />
      <button type="submit">Send</button>
    </form>
  );
}

Changelog

Added useTriggerChat hook to @trigger.dev/react-hooks that provides AI SDK useChat integration with Trigger.dev background tasks. Enables long-running AI conversations with realtime streaming via custom transport implementation.

New exports:

  • useTriggerChat - Hook for AI chat integration
  • TriggerChatTransport - Custom transport class
  • TriggerChatTransportOptions - Transport configuration type
  • TriggerChatTaskPayload - Task payload type

New dependencies:

  • ai@^5.0.82
  • @ai-sdk/react@^2.0.14
  • eventsource-parser@^3.0.0

Resources


Screenshots

N/A - This is a developer-facing hook with no UI

💯

@changeset-bot
Copy link

changeset-bot bot commented Oct 29, 2025

🦋 Changeset detected

Latest commit: b94f4f2

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 23 packages
Name Type
@trigger.dev/react-hooks Minor
d3-chat Patch
references-d3-openai-agents Patch
references-nextjs-realtime Patch
@trigger.dev/build Minor
@trigger.dev/core Minor
@trigger.dev/python Minor
@trigger.dev/redis-worker Minor
@trigger.dev/rsc Minor
@trigger.dev/schema-to-json Minor
@trigger.dev/sdk Minor
@trigger.dev/database Minor
@trigger.dev/otlp-importer Minor
trigger.dev Minor
@internal/cache Patch
@internal/clickhouse Patch
@internal/redis Patch
@internal/replication Patch
@internal/run-engine Patch
@internal/schedule-engine Patch
@internal/testcontainers Patch
@internal/tracing Patch
@internal/zod-worker Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Oct 29, 2025

Walkthrough

Adds a new TriggerChatTransport class and accompanying useTriggerChat React hook that provide Trigger.dev–backed streaming chat integration for the AI SDK. Introduces types, helpers for SSE/stream subscription and metadata parsing, reconnect logic, access-token handling, and runtime abort/cleanup behavior. Exports the new hook from the package index, updates packages/react-hooks/package.json to declare four optional peer dependencies (@ai-sdk/react, @electric-sql/client, ai, eventsource-parser), and adds a changelog entry for a minor release. No existing public APIs were removed.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

  • Areas to focus on:
    • packages/react-hooks/src/hooks/useTriggerChat.ts (TriggerChatTransport lifecycle, sendMessages/reconnectToStream behavior, SSE parsing, stream enqueueing, abort and cleanup)
    • subscribeToDataStreams / streamDataFromTrigger and metadata parsing correctness and error handling
    • Access token resolution (static vs function) and caching logic
    • package.json peerDependencies and peerDependenciesMeta entries for optionality
    • packages/react-hooks/src/index.ts export addition
    • .changeset entry for correctness and release metadata consistency

Pre-merge checks and finishing touches

✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed Title clearly describes the main feature: adding TriggerChatTransport for AI SDK useChat integration, directly matching the primary code changes.
Description check ✅ Passed Description is comprehensive and follows the template with completed checklist, testing details, detailed changelog, and technical documentation.
Linked Issues check ✅ Passed All coding requirements from issue #2634 are met: custom transport implemented, task triggering enabled, metadata stream subscription integrated, and realtime features leveraged.
Out of Scope Changes check ✅ Passed All changes are directly aligned with issue #2634 objectives: new hook, transport class, optional peer dependencies, and exports supporting AI SDK integration.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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.

Comment on lines +30 to +43
* @remarks
* **CRITICAL:** Your Trigger.dev task MUST call `metadata.stream()` with the AI SDK stream.
* The stream key used in `metadata.stream()` must match the `streamKey` option (default: "chat").
Copy link
Author

Choose a reason for hiding this comment

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

I'm not sure if there may be a better place to put this piece of documentation

the tricky bit here is there are 2 external files required to enable this hook

  1. the trigger.dev task definition itself, which must call a stream() method from ai and forward the stream to metadata.stream()
  2. the server action which invoked the trigger.dev task server-side

Comment on lines 34 to 64
* @example Trigger.dev task that streams AI responses:
* ```ts
* import { metadata, task } from "@trigger.dev/sdk/v3";
* import { streamText } from "ai";
* import { openai } from "@ai-sdk/openai";
*
* export const chatTask = task({
* id: "chat",
* run: async ({ messages }) => {
* const result = streamText({
* model: openai("gpt-4"),
* messages,
* });
*
* // CRITICAL: Stream to client using metadata.stream()
* await metadata.stream("chat", result.toUIMessageStream());
*
* return { text: await result.text };
* },
* });
Copy link
Author

Choose a reason for hiding this comment

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

similar to the above, it's a bit awkward adding this example here, because the task parameter actually just needs the string which is the id of the task ("chat" in the above example)

still unsure if this is the best way to document this

Comment on lines 331 to 362
function parseMetadata(
metadata: Record<string, unknown> | string | undefined
): ParsedMetadata | undefined {
if (!metadata) return undefined;

if (typeof metadata === "string") {
try {
return JSON.parse(metadata) as ParsedMetadata;
} catch {
return undefined;
}
}

if (typeof metadata === "object") {
return metadata as ParsedMetadata;
}

return undefined;
}
Copy link
Author

Choose a reason for hiding this comment

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

normally I would use zod for this, but I think this matches patterns in other hooks

Comment on lines 481 to 573
* @remarks
* **CRITICAL SETUP REQUIREMENTS:**
*
* 1. Your Trigger.dev task MUST call `metadata.stream()` to stream responses:
* ```ts
* await metadata.stream("chat", result.toUIMessageStream());
* ```
*
* 2. You must provide a server action that calls `tasks.trigger()`:
* ```ts
* "use server";
* export async function triggerChat(task: string, payload: unknown) {
* const handle = await tasks.trigger(task, payload);
* return { success: true, runId: handle.id, publicAccessToken: handle.publicAccessToken };
* }
* ```
*
* @example Complete setup with three files:
*
* **1. Trigger.dev task (src/trigger/chat.ts):**
* ```ts
* import { metadata, task } from "@trigger.dev/sdk/v3";
* import { streamText } from "ai";
* import { openai } from "@ai-sdk/openai";
*
* export const chatTask = task({
* id: "chat",
* run: async ({ messages }) => {
* const result = streamText({ model: openai("gpt-4"), messages });
* // CRITICAL: Stream to client
* await metadata.stream("chat", result.toUIMessageStream());
* return { text: await result.text };
* },
* });
* ```
*
* **2. Server action (src/actions.ts):**
* ```ts
* "use server";
* import { tasks } from "@trigger.dev/sdk/v3";
*
* export async function triggerChat(task: string, payload: unknown) {
* const handle = await tasks.trigger(task, payload);
* return { success: true, runId: handle.id, publicAccessToken: handle.publicAccessToken };
* }
* ```
*
* **3. Client component (src/components/Chat.tsx):**
* ```ts
* "use client";
* import { useTriggerChat } from "@trigger.dev/react-hooks";
* import { triggerChat } from "../actions";
*
* export function Chat() {
* const { messages, input, handleInputChange, handleSubmit } = useTriggerChat({
* transportOptions: { triggerTask: triggerChat }
* });
*
* return (
* <form onSubmit={handleSubmit}>
* {messages.map(m => <div key={m.id}>{m.role}: {m.content}</div>)}
* <input value={input} onChange={handleInputChange} />
* <button type="submit">Send</button>
* </form>
* );
* }
* ```
Copy link
Author

Choose a reason for hiding this comment

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

the above comments apply here as well

…ok with a custom transport for trigger.tdev tasks
@jakevollkommer jakevollkommer marked this pull request as ready for review October 31, 2025 19:18
Copy link
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

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 27376df and ba97cb0.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (4)
  • .changeset/thirty-olives-confess.md (1 hunks)
  • packages/react-hooks/package.json (1 hunks)
  • packages/react-hooks/src/hooks/useTriggerChat.ts (1 hunks)
  • packages/react-hooks/src/index.ts (1 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

**/*.{ts,tsx}: Always prefer using isomorphic code like fetch, ReadableStream, etc. instead of Node.js specific code
For TypeScript, we usually use types over interfaces
Avoid enums
No default exports, use function declarations

Files:

  • packages/react-hooks/src/index.ts
  • packages/react-hooks/src/hooks/useTriggerChat.ts
🧠 Learnings (15)
📓 Common learnings
Learnt from: CR
Repo: triggerdotdev/trigger.dev PR: 0
File: .cursor/rules/writing-tasks.mdc:0-0
Timestamp: 2025-08-18T10:07:17.368Z
Learning: Applies to **/trigger/**/*.{ts,tsx,js,jsx} : Import Trigger.dev APIs from "trigger.dev/sdk/v3" when writing tasks or related utilities
📚 Learning: 2025-08-18T10:07:17.368Z
Learnt from: CR
Repo: triggerdotdev/trigger.dev PR: 0
File: .cursor/rules/writing-tasks.mdc:0-0
Timestamp: 2025-08-18T10:07:17.368Z
Learning: Applies to **/trigger/**/*.{ts,tsx,js,jsx} : Import Trigger.dev APIs from "trigger.dev/sdk/v3" when writing tasks or related utilities

Applied to files:

  • packages/react-hooks/src/index.ts
  • .changeset/thirty-olives-confess.md
  • packages/react-hooks/package.json
  • packages/react-hooks/src/hooks/useTriggerChat.ts
📚 Learning: 2025-08-29T10:06:49.293Z
Learnt from: CR
Repo: triggerdotdev/trigger.dev PR: 0
File: .cursor/rules/webapp.mdc:0-0
Timestamp: 2025-08-29T10:06:49.293Z
Learning: Applies to apps/webapp/**/*.{ts,tsx} : When importing from trigger.dev/core in the webapp, never import the root package path; always use one of the documented subpath exports from trigger.dev/core’s package.json

Applied to files:

  • packages/react-hooks/src/index.ts
📚 Learning: 2025-08-18T10:07:17.368Z
Learnt from: CR
Repo: triggerdotdev/trigger.dev PR: 0
File: .cursor/rules/writing-tasks.mdc:0-0
Timestamp: 2025-08-18T10:07:17.368Z
Learning: Applies to **/trigger/**/*.{ts,tsx,js,jsx} : Use triggerAndWait() only from within a task context (not from generic app code) and handle result.ok or use unwrap() with error handling

Applied to files:

  • packages/react-hooks/src/index.ts
  • .changeset/thirty-olives-confess.md
  • packages/react-hooks/package.json
  • packages/react-hooks/src/hooks/useTriggerChat.ts
📚 Learning: 2025-08-18T10:07:17.368Z
Learnt from: CR
Repo: triggerdotdev/trigger.dev PR: 0
File: .cursor/rules/writing-tasks.mdc:0-0
Timestamp: 2025-08-18T10:07:17.368Z
Learning: Applies to **/trigger/**/*.{ts,tsx,js,jsx} : Export every task (including subtasks) defined with task(), schedules.task(), or schemaTask()

Applied to files:

  • packages/react-hooks/src/index.ts
📚 Learning: 2025-08-18T10:07:17.368Z
Learnt from: CR
Repo: triggerdotdev/trigger.dev PR: 0
File: .cursor/rules/writing-tasks.mdc:0-0
Timestamp: 2025-08-18T10:07:17.368Z
Learning: Applies to **/trigger/**/*.{ts,tsx,js,jsx} : Use schedules.task(...) for scheduled (cron) tasks; do not implement schedules as plain task() with external cron logic

Applied to files:

  • packages/react-hooks/src/index.ts
  • packages/react-hooks/src/hooks/useTriggerChat.ts
📚 Learning: 2025-08-18T10:07:17.368Z
Learnt from: CR
Repo: triggerdotdev/trigger.dev PR: 0
File: .cursor/rules/writing-tasks.mdc:0-0
Timestamp: 2025-08-18T10:07:17.368Z
Learning: Applies to **/trigger/**/*.{ts,tsx,js,jsx} : Define tasks using task({ id, run, ... }) with a unique id per project

Applied to files:

  • packages/react-hooks/src/index.ts
  • .changeset/thirty-olives-confess.md
  • packages/react-hooks/src/hooks/useTriggerChat.ts
📚 Learning: 2025-08-18T10:07:17.368Z
Learnt from: CR
Repo: triggerdotdev/trigger.dev PR: 0
File: .cursor/rules/writing-tasks.mdc:0-0
Timestamp: 2025-08-18T10:07:17.368Z
Learning: Applies to **/trigger/**/*.{ts,tsx,js,jsx} : Use schemaTask({ schema, run, ... }) to validate payloads when input validation is required

Applied to files:

  • packages/react-hooks/src/index.ts
  • packages/react-hooks/src/hooks/useTriggerChat.ts
📚 Learning: 2025-08-18T10:07:17.368Z
Learnt from: CR
Repo: triggerdotdev/trigger.dev PR: 0
File: .cursor/rules/writing-tasks.mdc:0-0
Timestamp: 2025-08-18T10:07:17.368Z
Learning: Applies to **/trigger/**/*.{ts,tsx,js,jsx} : Do not use client.defineJob or any deprecated v2 patterns (e.g., eventTrigger) when defining tasks

Applied to files:

  • packages/react-hooks/src/index.ts
  • .changeset/thirty-olives-confess.md
  • packages/react-hooks/src/hooks/useTriggerChat.ts
📚 Learning: 2025-08-18T10:07:17.368Z
Learnt from: CR
Repo: triggerdotdev/trigger.dev PR: 0
File: .cursor/rules/writing-tasks.mdc:0-0
Timestamp: 2025-08-18T10:07:17.368Z
Learning: Applies to **/trigger/**/*.{ts,tsx,js,jsx} : Use metadata API (metadata.current/get/set/append/stream, etc.) only inside run functions or lifecycle hooks

Applied to files:

  • packages/react-hooks/src/index.ts
  • packages/react-hooks/package.json
  • packages/react-hooks/src/hooks/useTriggerChat.ts
📚 Learning: 2025-08-18T10:07:17.368Z
Learnt from: CR
Repo: triggerdotdev/trigger.dev PR: 0
File: .cursor/rules/writing-tasks.mdc:0-0
Timestamp: 2025-08-18T10:07:17.368Z
Learning: For Realtime subscriptions or React hooks, provide a Public Access Token and scope it appropriately (e.g., via TriggerAuthContext)

Applied to files:

  • packages/react-hooks/src/index.ts
📚 Learning: 2025-08-18T10:07:17.368Z
Learnt from: CR
Repo: triggerdotdev/trigger.dev PR: 0
File: .cursor/rules/writing-tasks.mdc:0-0
Timestamp: 2025-08-18T10:07:17.368Z
Learning: Applies to trigger.config.ts : Configure global task lifecycle hooks (onStart/onSuccess/onFailure) only within trigger.config.ts if needed, not within arbitrary files

Applied to files:

  • packages/react-hooks/src/hooks/useTriggerChat.ts
📚 Learning: 2025-08-18T10:07:17.368Z
Learnt from: CR
Repo: triggerdotdev/trigger.dev PR: 0
File: .cursor/rules/writing-tasks.mdc:0-0
Timestamp: 2025-08-18T10:07:17.368Z
Learning: Applies to **/trigger/**/*.{ts,tsx,js,jsx} : For idempotent child-task invocations, create and pass idempotencyKey (and optional TTL) when calling trigger()/batchTrigger() from tasks

Applied to files:

  • packages/react-hooks/src/hooks/useTriggerChat.ts
📚 Learning: 2025-08-18T10:07:17.368Z
Learnt from: CR
Repo: triggerdotdev/trigger.dev PR: 0
File: .cursor/rules/writing-tasks.mdc:0-0
Timestamp: 2025-08-18T10:07:17.368Z
Learning: Applies to **/trigger/**/*.{ts,tsx,js,jsx} : When triggering a task multiple times in a loop from inside another task, use batchTrigger()/batchTriggerAndWait() instead of per-item trigger() calls

Applied to files:

  • packages/react-hooks/src/hooks/useTriggerChat.ts
📚 Learning: 2025-07-18T17:49:24.468Z
Learnt from: CR
Repo: triggerdotdev/trigger.dev PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-07-18T17:49:24.468Z
Learning: Applies to {packages/core,apps/webapp}/**/*.{ts,tsx} : We use zod a lot in packages/core and in the webapp

Applied to files:

  • packages/react-hooks/src/hooks/useTriggerChat.ts
🧬 Code graph analysis (1)
packages/react-hooks/src/hooks/useTriggerChat.ts (2)
packages/core/src/v3/apiClientManager/index.ts (2)
  • accessToken (37-45)
  • baseURL (32-35)
references/d3-openai-agents/src/components/main-app.tsx (1)
  • useChat (20-86)

Copy link
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

🧹 Nitpick comments (1)
packages/react-hooks/src/hooks/useTriggerChat.ts (1)

268-269: Consider making API versions configurable.

The hardcoded version headers may need updates as the Trigger.dev API evolves. Consider adding optional configuration for these versions in TriggerChatTransportOptions.

 export interface TriggerChatTransportOptions<
   TPayload extends TriggerChatTaskPayload = TriggerChatTaskPayload,
 > {
   // ... existing options ...
   
+  /**
+   * Override Electric SQL version header
+   * @default "1.0.0-beta.1"
+   */
+  electricVersion?: string;
+  
+  /**
+   * Override Trigger.dev API version header
+   * @default "2024-11-28"
+   */
+  apiVersion?: string;
 }

Then use them in the ShapeStream constructor:

 const runStream = new ShapeStream({
   url: runStreamUrl,
   headers: {
     Authorization: `Bearer ${accessToken}`,
-    "x-trigger-electric-version": "1.0.0-beta.1",
-    "x-trigger-api-version": "2024-11-28",
+    "x-trigger-electric-version": this.electricVersion ?? "1.0.0-beta.1",
+    "x-trigger-api-version": this.apiVersion ?? "2024-11-28",
   },
   signal: runAbortController.signal,
 });
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ba97cb0 and 136085f.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (2)
  • packages/react-hooks/package.json (1 hunks)
  • packages/react-hooks/src/hooks/useTriggerChat.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/react-hooks/package.json
🧰 Additional context used
📓 Path-based instructions (1)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

**/*.{ts,tsx}: Always prefer using isomorphic code like fetch, ReadableStream, etc. instead of Node.js specific code
For TypeScript, we usually use types over interfaces
Avoid enums
No default exports, use function declarations

Files:

  • packages/react-hooks/src/hooks/useTriggerChat.ts
🧠 Learnings (13)
📓 Common learnings
Learnt from: CR
Repo: triggerdotdev/trigger.dev PR: 0
File: .cursor/rules/writing-tasks.mdc:0-0
Timestamp: 2025-08-18T10:07:17.368Z
Learning: Applies to **/trigger/**/*.{ts,tsx,js,jsx} : Import Trigger.dev APIs from "trigger.dev/sdk/v3" when writing tasks or related utilities
Learnt from: CR
Repo: triggerdotdev/trigger.dev PR: 0
File: .cursor/rules/writing-tasks.mdc:0-0
Timestamp: 2025-08-18T10:07:17.368Z
Learning: For Realtime subscriptions or React hooks, provide a Public Access Token and scope it appropriately (e.g., via TriggerAuthContext)
Learnt from: CR
Repo: triggerdotdev/trigger.dev PR: 0
File: .cursor/rules/writing-tasks.mdc:0-0
Timestamp: 2025-08-18T10:07:17.368Z
Learning: Applies to **/trigger/**/*.{ts,tsx,js,jsx} : Use metadata API (metadata.current/get/set/append/stream, etc.) only inside run functions or lifecycle hooks
📚 Learning: 2025-08-18T10:07:17.368Z
Learnt from: CR
Repo: triggerdotdev/trigger.dev PR: 0
File: .cursor/rules/writing-tasks.mdc:0-0
Timestamp: 2025-08-18T10:07:17.368Z
Learning: Applies to **/trigger/**/*.{ts,tsx,js,jsx} : Import Trigger.dev APIs from "trigger.dev/sdk/v3" when writing tasks or related utilities

Applied to files:

  • packages/react-hooks/src/hooks/useTriggerChat.ts
📚 Learning: 2025-08-18T10:07:17.368Z
Learnt from: CR
Repo: triggerdotdev/trigger.dev PR: 0
File: .cursor/rules/writing-tasks.mdc:0-0
Timestamp: 2025-08-18T10:07:17.368Z
Learning: Applies to **/trigger/**/*.{ts,tsx,js,jsx} : Use metadata API (metadata.current/get/set/append/stream, etc.) only inside run functions or lifecycle hooks

Applied to files:

  • packages/react-hooks/src/hooks/useTriggerChat.ts
📚 Learning: 2025-08-18T10:07:17.368Z
Learnt from: CR
Repo: triggerdotdev/trigger.dev PR: 0
File: .cursor/rules/writing-tasks.mdc:0-0
Timestamp: 2025-08-18T10:07:17.368Z
Learning: Applies to **/trigger/**/*.{ts,tsx,js,jsx} : Define tasks using task({ id, run, ... }) with a unique id per project

Applied to files:

  • packages/react-hooks/src/hooks/useTriggerChat.ts
📚 Learning: 2025-08-18T10:07:17.368Z
Learnt from: CR
Repo: triggerdotdev/trigger.dev PR: 0
File: .cursor/rules/writing-tasks.mdc:0-0
Timestamp: 2025-08-18T10:07:17.368Z
Learning: Applies to **/trigger/**/*.{ts,tsx,js,jsx} : Use schemaTask({ schema, run, ... }) to validate payloads when input validation is required

Applied to files:

  • packages/react-hooks/src/hooks/useTriggerChat.ts
📚 Learning: 2025-08-18T10:07:17.368Z
Learnt from: CR
Repo: triggerdotdev/trigger.dev PR: 0
File: .cursor/rules/writing-tasks.mdc:0-0
Timestamp: 2025-08-18T10:07:17.368Z
Learning: Applies to **/trigger/**/*.{ts,tsx,js,jsx} : Do not use client.defineJob or any deprecated v2 patterns (e.g., eventTrigger) when defining tasks

Applied to files:

  • packages/react-hooks/src/hooks/useTriggerChat.ts
📚 Learning: 2025-08-18T10:07:17.368Z
Learnt from: CR
Repo: triggerdotdev/trigger.dev PR: 0
File: .cursor/rules/writing-tasks.mdc:0-0
Timestamp: 2025-08-18T10:07:17.368Z
Learning: Applies to trigger.config.ts : Configure global task lifecycle hooks (onStart/onSuccess/onFailure) only within trigger.config.ts if needed, not within arbitrary files

Applied to files:

  • packages/react-hooks/src/hooks/useTriggerChat.ts
📚 Learning: 2025-08-18T10:07:17.368Z
Learnt from: CR
Repo: triggerdotdev/trigger.dev PR: 0
File: .cursor/rules/writing-tasks.mdc:0-0
Timestamp: 2025-08-18T10:07:17.368Z
Learning: Applies to **/trigger/**/*.{ts,tsx,js,jsx} : Use triggerAndWait() only from within a task context (not from generic app code) and handle result.ok or use unwrap() with error handling

Applied to files:

  • packages/react-hooks/src/hooks/useTriggerChat.ts
📚 Learning: 2025-08-18T10:07:17.368Z
Learnt from: CR
Repo: triggerdotdev/trigger.dev PR: 0
File: .cursor/rules/writing-tasks.mdc:0-0
Timestamp: 2025-08-18T10:07:17.368Z
Learning: Applies to **/trigger/**/*.{ts,tsx,js,jsx} : Use schedules.task(...) for scheduled (cron) tasks; do not implement schedules as plain task() with external cron logic

Applied to files:

  • packages/react-hooks/src/hooks/useTriggerChat.ts
📚 Learning: 2025-08-18T10:07:17.368Z
Learnt from: CR
Repo: triggerdotdev/trigger.dev PR: 0
File: .cursor/rules/writing-tasks.mdc:0-0
Timestamp: 2025-08-18T10:07:17.368Z
Learning: Applies to **/trigger/**/*.{ts,tsx,js,jsx} : For idempotent child-task invocations, create and pass idempotencyKey (and optional TTL) when calling trigger()/batchTrigger() from tasks

Applied to files:

  • packages/react-hooks/src/hooks/useTriggerChat.ts
📚 Learning: 2025-08-18T10:07:17.368Z
Learnt from: CR
Repo: triggerdotdev/trigger.dev PR: 0
File: .cursor/rules/writing-tasks.mdc:0-0
Timestamp: 2025-08-18T10:07:17.368Z
Learning: Applies to **/trigger/**/*.{ts,tsx,js,jsx} : When triggering a task multiple times in a loop from inside another task, use batchTrigger()/batchTriggerAndWait() instead of per-item trigger() calls

Applied to files:

  • packages/react-hooks/src/hooks/useTriggerChat.ts
📚 Learning: 2025-07-18T17:49:24.468Z
Learnt from: CR
Repo: triggerdotdev/trigger.dev PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-07-18T17:49:24.468Z
Learning: Applies to {packages/core,apps/webapp}/**/*.{ts,tsx} : We use zod a lot in packages/core and in the webapp

Applied to files:

  • packages/react-hooks/src/hooks/useTriggerChat.ts
📚 Learning: 2025-08-18T10:07:17.368Z
Learnt from: CR
Repo: triggerdotdev/trigger.dev PR: 0
File: .cursor/rules/writing-tasks.mdc:0-0
Timestamp: 2025-08-18T10:07:17.368Z
Learning: For Realtime subscriptions or React hooks, provide a Public Access Token and scope it appropriately (e.g., via TriggerAuthContext)

Applied to files:

  • packages/react-hooks/src/hooks/useTriggerChat.ts
🧬 Code graph analysis (1)
packages/react-hooks/src/hooks/useTriggerChat.ts (1)
packages/core/src/v3/apiClientManager/index.ts (2)
  • accessToken (37-45)
  • baseURL (32-35)
🔇 Additional comments (7)
packages/react-hooks/src/hooks/useTriggerChat.ts (7)

1-20: LGTM: Clean imports and sensible defaults.

The imports are well-organized and the default constants provide good fallback values for common use cases.


33-132: LGTM: Comprehensive interface with excellent documentation.

The inline JSDoc examples are appropriately placed and very helpful for users. Documenting critical setup requirements directly in the interface definition is a best practice, especially given the complexity of the three-file setup (task, server action, client component).


159-337: LGTM: Well-structured transport implementation.

The class properly manages resources with cleanup handlers, handles errors appropriately, and provides both initial connection and reconnection capabilities. The token resolution logic correctly prioritizes stored tokens while providing fallback options.


339-361: LGTM: Pragmatic metadata parsing.

The silent error handling and type casting are acceptable here given that subscribeToDataStreams defensively validates the metadata structure. This approach matches existing patterns in the codebase and avoids adding zod as a dependency.


401-411: LGTM: Promise rejection now properly handled.

The addition of .catch() successfully addresses the previous review concern about unhandled promise rejections. The error is properly propagated to the controller and the run is aborted to ensure clean resource cleanup.


415-491: LGTM: Robust SSE streaming implementation.

The stream handling is well-implemented with appropriate error handling. The decision to continue processing despite individual parse errors adds resilience, and the intentional reliance on run.finishedAt for cleanup (rather than stream end) ensures proper lifecycle management.


530-620: LGTM: Excellent documentation and clean implementation.

The extensive three-file example is exactly what users need to understand the setup requirements. The documentation placement is ideal—comprehensive examples at the public API level guide developers through the entire integration. The hook implementation properly validates dependencies and delegates to useChat.

…check

Remove require.resolve() runtime check for optional peer dependencies.
With optional peerDependencies in package.json, static ES imports provide
better compile-time validation:

- Build fails with clear errors only if useTriggerChat is imported
- TypeScript/LSP shows missing dependencies immediately in IDE
- Tree-shaking eliminates code when hook is unused
- No Node.js-specific APIs in client-side "use client" code

The previous runtime check using require.resolve() was:
1. Not isomorphic (fails in browser/bundler environments)
2. Redundant (static imports already enforce dependencies)
3. Worse UX (runtime errors instead of build-time errors)
Copy link
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

🧹 Nitpick comments (2)
packages/react-hooks/src/hooks/useTriggerChat.ts (2)

210-225: Verify silent error handling in reconnection.

The catch block silently suppresses errors when reconnection fails, which appears intentional (returning null signals failure to the caller). However, this may hide useful debugging information from developers.

Consider logging the error before suppressing it:

    try {
      return this.subscribeToRun(runId, undefined);
-   } catch {
+   } catch (error) {
+     console.warn('Failed to reconnect to stream:', error);
      this.activeRuns.delete(options.chatId);
      return null;
    }

This preserves the reconnection UX while aiding troubleshooting.


276-282: Document the type assertion for clarity.

Line 281 uses a double type assertion (as unknown as AnyRealtimeRun) to work around ShapeStream's generic message typing.

Add a brief comment explaining why the cast is necessary:

            (messages) => {
              for (const message of messages) {
                if (!("value" in message) || !message.value) continue;

+               // ShapeStream returns generic messages; cast to expected run type
                const run = message.value as unknown as AnyRealtimeRun;

This helps future maintainers understand the type safety trade-off.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 136085f and b94f4f2.

📒 Files selected for processing (1)
  • packages/react-hooks/src/hooks/useTriggerChat.ts (1 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

**/*.{ts,tsx}: Always prefer using isomorphic code like fetch, ReadableStream, etc. instead of Node.js specific code
For TypeScript, we usually use types over interfaces
Avoid enums
No default exports, use function declarations

Files:

  • packages/react-hooks/src/hooks/useTriggerChat.ts
🧠 Learnings (15)
📓 Common learnings
Learnt from: CR
Repo: triggerdotdev/trigger.dev PR: 0
File: .cursor/rules/writing-tasks.mdc:0-0
Timestamp: 2025-08-18T10:07:17.368Z
Learning: Applies to **/trigger/**/*.{ts,tsx,js,jsx} : Import Trigger.dev APIs from "trigger.dev/sdk/v3" when writing tasks or related utilities
Learnt from: CR
Repo: triggerdotdev/trigger.dev PR: 0
File: .cursor/rules/writing-tasks.mdc:0-0
Timestamp: 2025-08-18T10:07:17.368Z
Learning: For Realtime subscriptions or React hooks, provide a Public Access Token and scope it appropriately (e.g., via TriggerAuthContext)
📚 Learning: 2025-08-18T10:07:17.368Z
Learnt from: CR
Repo: triggerdotdev/trigger.dev PR: 0
File: .cursor/rules/writing-tasks.mdc:0-0
Timestamp: 2025-08-18T10:07:17.368Z
Learning: Applies to **/trigger/**/*.{ts,tsx,js,jsx} : Import Trigger.dev APIs from "trigger.dev/sdk/v3" when writing tasks or related utilities

Applied to files:

  • packages/react-hooks/src/hooks/useTriggerChat.ts
📚 Learning: 2025-08-18T10:07:17.368Z
Learnt from: CR
Repo: triggerdotdev/trigger.dev PR: 0
File: .cursor/rules/writing-tasks.mdc:0-0
Timestamp: 2025-08-18T10:07:17.368Z
Learning: Applies to **/trigger/**/*.{ts,tsx,js,jsx} : Use metadata API (metadata.current/get/set/append/stream, etc.) only inside run functions or lifecycle hooks

Applied to files:

  • packages/react-hooks/src/hooks/useTriggerChat.ts
📚 Learning: 2025-08-18T10:07:17.368Z
Learnt from: CR
Repo: triggerdotdev/trigger.dev PR: 0
File: .cursor/rules/writing-tasks.mdc:0-0
Timestamp: 2025-08-18T10:07:17.368Z
Learning: Applies to trigger.config.ts : Configure global task lifecycle hooks (onStart/onSuccess/onFailure) only within trigger.config.ts if needed, not within arbitrary files

Applied to files:

  • packages/react-hooks/src/hooks/useTriggerChat.ts
📚 Learning: 2025-08-18T10:07:17.368Z
Learnt from: CR
Repo: triggerdotdev/trigger.dev PR: 0
File: .cursor/rules/writing-tasks.mdc:0-0
Timestamp: 2025-08-18T10:07:17.368Z
Learning: Applies to **/trigger/**/*.{ts,tsx,js,jsx} : Define tasks using task({ id, run, ... }) with a unique id per project

Applied to files:

  • packages/react-hooks/src/hooks/useTriggerChat.ts
📚 Learning: 2025-08-18T10:07:17.368Z
Learnt from: CR
Repo: triggerdotdev/trigger.dev PR: 0
File: .cursor/rules/writing-tasks.mdc:0-0
Timestamp: 2025-08-18T10:07:17.368Z
Learning: Applies to **/trigger/**/*.{ts,tsx,js,jsx} : Use triggerAndWait() only from within a task context (not from generic app code) and handle result.ok or use unwrap() with error handling

Applied to files:

  • packages/react-hooks/src/hooks/useTriggerChat.ts
📚 Learning: 2025-08-18T10:07:17.368Z
Learnt from: CR
Repo: triggerdotdev/trigger.dev PR: 0
File: .cursor/rules/writing-tasks.mdc:0-0
Timestamp: 2025-08-18T10:07:17.368Z
Learning: Applies to **/trigger/**/*.{ts,tsx,js,jsx} : Do not use client.defineJob or any deprecated v2 patterns (e.g., eventTrigger) when defining tasks

Applied to files:

  • packages/react-hooks/src/hooks/useTriggerChat.ts
📚 Learning: 2025-08-18T10:07:17.368Z
Learnt from: CR
Repo: triggerdotdev/trigger.dev PR: 0
File: .cursor/rules/writing-tasks.mdc:0-0
Timestamp: 2025-08-18T10:07:17.368Z
Learning: Applies to **/trigger/**/*.{ts,tsx,js,jsx} : Use schedules.task(...) for scheduled (cron) tasks; do not implement schedules as plain task() with external cron logic

Applied to files:

  • packages/react-hooks/src/hooks/useTriggerChat.ts
📚 Learning: 2025-08-18T10:07:17.368Z
Learnt from: CR
Repo: triggerdotdev/trigger.dev PR: 0
File: .cursor/rules/writing-tasks.mdc:0-0
Timestamp: 2025-08-18T10:07:17.368Z
Learning: Applies to **/trigger/**/*.{ts,tsx,js,jsx} : Use schemaTask({ schema, run, ... }) to validate payloads when input validation is required

Applied to files:

  • packages/react-hooks/src/hooks/useTriggerChat.ts
📚 Learning: 2025-08-18T10:07:17.368Z
Learnt from: CR
Repo: triggerdotdev/trigger.dev PR: 0
File: .cursor/rules/writing-tasks.mdc:0-0
Timestamp: 2025-08-18T10:07:17.368Z
Learning: Applies to **/trigger/**/*.{ts,tsx,js,jsx} : For idempotent child-task invocations, create and pass idempotencyKey (and optional TTL) when calling trigger()/batchTrigger() from tasks

Applied to files:

  • packages/react-hooks/src/hooks/useTriggerChat.ts
📚 Learning: 2025-08-18T10:07:17.368Z
Learnt from: CR
Repo: triggerdotdev/trigger.dev PR: 0
File: .cursor/rules/writing-tasks.mdc:0-0
Timestamp: 2025-08-18T10:07:17.368Z
Learning: Applies to **/trigger/**/*.{ts,tsx,js,jsx} : When triggering a task multiple times in a loop from inside another task, use batchTrigger()/batchTriggerAndWait() instead of per-item trigger() calls

Applied to files:

  • packages/react-hooks/src/hooks/useTriggerChat.ts
📚 Learning: 2025-07-18T17:49:24.468Z
Learnt from: CR
Repo: triggerdotdev/trigger.dev PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-07-18T17:49:24.468Z
Learning: Applies to {packages/core,apps/webapp}/**/*.{ts,tsx} : We use zod a lot in packages/core and in the webapp

Applied to files:

  • packages/react-hooks/src/hooks/useTriggerChat.ts
📚 Learning: 2025-08-18T10:07:17.368Z
Learnt from: CR
Repo: triggerdotdev/trigger.dev PR: 0
File: .cursor/rules/writing-tasks.mdc:0-0
Timestamp: 2025-08-18T10:07:17.368Z
Learning: For Realtime subscriptions or React hooks, provide a Public Access Token and scope it appropriately (e.g., via TriggerAuthContext)

Applied to files:

  • packages/react-hooks/src/hooks/useTriggerChat.ts
📚 Learning: 2025-07-18T17:49:24.468Z
Learnt from: CR
Repo: triggerdotdev/trigger.dev PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-07-18T17:49:24.468Z
Learning: Applies to **/*.{ts,tsx} : Always prefer using isomorphic code like fetch, ReadableStream, etc. instead of Node.js specific code

Applied to files:

  • packages/react-hooks/src/hooks/useTriggerChat.ts
📚 Learning: 2025-08-29T10:06:49.293Z
Learnt from: CR
Repo: triggerdotdev/trigger.dev PR: 0
File: .cursor/rules/webapp.mdc:0-0
Timestamp: 2025-08-29T10:06:49.293Z
Learning: Applies to apps/webapp/**/*.{ts,tsx} : When importing from trigger.dev/core in the webapp, never import the root package path; always use one of the documented subpath exports from trigger.dev/core’s package.json

Applied to files:

  • packages/react-hooks/src/hooks/useTriggerChat.ts
🧬 Code graph analysis (1)
packages/react-hooks/src/hooks/useTriggerChat.ts (2)
packages/core/src/v3/apiClientManager/index.ts (2)
  • accessToken (37-45)
  • baseURL (32-35)
references/d3-openai-agents/src/components/main-app.tsx (1)
  • useChat (20-86)
🔇 Additional comments (9)
packages/react-hooks/src/hooks/useTriggerChat.ts (9)

1-16: LGTM!

The imports follow the isomorphic code guideline, using client-safe libraries and types. The "use client" directive is correctly placed for this React hook.


18-20: LGTM!

The default constants are sensible and align with the documented usage patterns.


22-138: LGTM!

The type definitions are well-structured with comprehensive documentation and helpful examples. The use of generics provides good flexibility while maintaining type safety. Follows the coding guideline to prefer types over interfaces.


159-179: LGTM!

The class structure and constructor are clean, with appropriate use of private fields and sensible defaults. The Map-based state management for activeRuns and runTokens is appropriate for tracking multiple concurrent conversations.


227-336: LGTM!

The buildPayload and resolveAccessToken methods are clean and handle their respective concerns well. The access token resolution follows the learned pattern of supporting both static and dynamic tokens (string or function). Good error messaging when tokens are missing.

Based on learnings


343-361: LGTM!

The metadata parsing handles both string and object formats safely with appropriate error recovery. The simple approach is sufficient for internal parsing of trusted Trigger.dev metadata.


367-413: LGTM!

The stream subscription logic correctly checks for available streams and subscribes to matching keys. The error handling for the async streamDataFromTrigger call properly catches rejections and propagates them to the controller, addressing the previous review concern about unhandled promise rejections.


419-491: LGTM!

The SSE streaming implementation is robust with good error handling:

  • Properly uses EventSourceParserStream for parsing
  • Warns on unparseable chunks without breaking the stream
  • Ignores AbortError to allow graceful cancellation
  • Correctly delegates stream closure to the run lifecycle (line 464 comment)

The fetch-based approach follows the isomorphic code guideline.


493-586: LGTM!

The useTriggerChat hook is clean and well-documented:

  • Simple composition pattern delegating to AI SDK's useChat
  • Comprehensive three-file example covers task definition, server action, and client usage
  • Follows coding guidelines (no default export, function declaration, isomorphic code)
  • Type-safe integration with AI SDK types

The extensive documentation addresses setup complexity effectively.

Comment on lines +263 to +272
const runStreamUrl = `${baseURL}/realtime/v1/runs/${runId}`;
const runStream = new ShapeStream({
url: runStreamUrl,
headers: {
Authorization: `Bearer ${accessToken}`,
"x-trigger-electric-version": "1.0.0-beta.1",
"x-trigger-api-version": "2024-11-28",
},
signal: runAbortController.signal,
});
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Hardcoded API version headers may require maintenance.

Lines 268-269 contain hardcoded version strings that may need updates as the Trigger.dev API evolves. If these versions fall out of sync with the SDK, stream subscriptions could fail.

Consider one of these approaches:

Option 1: Import from a shared constants file

import { ELECTRIC_VERSION, API_VERSION } from '@trigger.dev/core/v3/constants';

Option 2: Make versions configurable via TriggerChatTransportOptions

export interface TriggerChatTransportOptions<...> {
  baseURL?: string;
+ electricVersion?: string;
+ apiVersion?: string;
  // ...
}

Then use:

headers: {
  Authorization: `Bearer ${accessToken}`,
- "x-trigger-electric-version": "1.0.0-beta.1",
- "x-trigger-api-version": "2024-11-28",
+ "x-trigger-electric-version": this.electricVersion ?? "1.0.0-beta.1",
+ "x-trigger-api-version": this.apiVersion ?? "2024-11-28",
},
🤖 Prompt for AI Agents
In packages/react-hooks/src/hooks/useTriggerChat.ts around lines 263 to 272, the
ShapeStream headers currently hardcode "x-trigger-electric-version" and
"x-trigger-api-version", which will break when versions change; replace the
hardcoded strings by sourcing versions from a shared constant or configuration:
either import ELECTRIC_VERSION and API_VERSION from the SDK/shared constants
module and use those variables in the headers, or add them to
TriggerChatTransportOptions (with sensible defaults) and read from options when
constructing ShapeStream; ensure any new import path is added to package
dependencies and update the hook signature/defaults accordingly.

@jakevollkommer jakevollkommer marked this pull request as draft November 4, 2025 19:43
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.

feat: AI SDK ChatTransport

1 participant