@trigger.dev/sdk 0.0.0-prerelease-20260302145933 → 0.0.0-prerelease-20260305142821
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/commonjs/v3/ai.d.ts +347 -2
- package/dist/commonjs/v3/ai.js +563 -1
- package/dist/commonjs/v3/ai.js.map +1 -1
- package/dist/commonjs/v3/chat-constants.d.ts +10 -0
- package/dist/commonjs/v3/chat-constants.js +14 -0
- package/dist/commonjs/v3/chat-constants.js.map +1 -0
- package/dist/commonjs/v3/chat-react.d.ts +45 -0
- package/dist/commonjs/v3/chat-react.js +71 -0
- package/dist/commonjs/v3/chat-react.js.map +1 -0
- package/dist/commonjs/v3/chat.d.ts +241 -0
- package/dist/commonjs/v3/chat.js +343 -0
- package/dist/commonjs/v3/chat.js.map +1 -0
- package/dist/commonjs/v3/chat.test.d.ts +1 -0
- package/dist/commonjs/v3/chat.test.js +1557 -0
- package/dist/commonjs/v3/chat.test.js.map +1 -0
- package/dist/commonjs/v3/runs.d.ts +3 -3
- package/dist/commonjs/v3/streams.js +27 -17
- package/dist/commonjs/v3/streams.js.map +1 -1
- package/dist/commonjs/version.js +1 -1
- package/dist/esm/v3/ai.d.ts +347 -2
- package/dist/esm/v3/ai.js +564 -2
- package/dist/esm/v3/ai.js.map +1 -1
- package/dist/esm/v3/chat-constants.d.ts +10 -0
- package/dist/esm/v3/chat-constants.js +11 -0
- package/dist/esm/v3/chat-constants.js.map +1 -0
- package/dist/esm/v3/chat-react.d.ts +45 -0
- package/dist/esm/v3/chat-react.js +68 -0
- package/dist/esm/v3/chat-react.js.map +1 -0
- package/dist/esm/v3/chat.d.ts +241 -0
- package/dist/esm/v3/chat.js +338 -0
- package/dist/esm/v3/chat.js.map +1 -0
- package/dist/esm/v3/chat.test.d.ts +1 -0
- package/dist/esm/v3/chat.test.js +1555 -0
- package/dist/esm/v3/chat.test.js.map +1 -0
- package/dist/esm/v3/streams.js +27 -17
- package/dist/esm/v3/streams.js.map +1 -1
- package/dist/esm/version.js +1 -1
- package/package.json +40 -5
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { TriggerChatTransport, type TriggerChatTransportOptions } from "./chat.js";
|
|
2
|
+
import type { AnyTask, TaskIdentifier } from "@trigger.dev/core/v3";
|
|
3
|
+
/**
|
|
4
|
+
* Options for `useTriggerChatTransport`, with a type-safe `task` field.
|
|
5
|
+
*
|
|
6
|
+
* Pass a task type parameter to get compile-time validation of the task ID:
|
|
7
|
+
* ```ts
|
|
8
|
+
* useTriggerChatTransport<typeof myTask>({ task: "my-task", ... })
|
|
9
|
+
* ```
|
|
10
|
+
*/
|
|
11
|
+
export type UseTriggerChatTransportOptions<TTask extends AnyTask = AnyTask> = Omit<TriggerChatTransportOptions, "task"> & {
|
|
12
|
+
/** The task ID. Strongly typed when a task type parameter is provided. */
|
|
13
|
+
task: TaskIdentifier<TTask>;
|
|
14
|
+
};
|
|
15
|
+
/**
|
|
16
|
+
* React hook that creates and memoizes a `TriggerChatTransport` instance.
|
|
17
|
+
*
|
|
18
|
+
* The transport is created once on first render and reused for the lifetime
|
|
19
|
+
* of the component. This avoids the need for `useMemo` and ensures the
|
|
20
|
+
* transport's internal session state (run IDs, lastEventId, etc.)
|
|
21
|
+
* is preserved across re-renders.
|
|
22
|
+
*
|
|
23
|
+
* For dynamic access tokens, pass a function — it will be called on each
|
|
24
|
+
* request without needing to recreate the transport.
|
|
25
|
+
*
|
|
26
|
+
* The `onSessionChange` callback is kept in a ref so the transport always
|
|
27
|
+
* calls the latest version without needing to be recreated.
|
|
28
|
+
*
|
|
29
|
+
* @example
|
|
30
|
+
* ```tsx
|
|
31
|
+
* import { useChat } from "@ai-sdk/react";
|
|
32
|
+
* import { useTriggerChatTransport } from "@trigger.dev/sdk/chat/react";
|
|
33
|
+
* import type { chat } from "@/trigger/chat";
|
|
34
|
+
*
|
|
35
|
+
* function Chat() {
|
|
36
|
+
* const transport = useTriggerChatTransport<typeof chat>({
|
|
37
|
+
* task: "ai-chat",
|
|
38
|
+
* accessToken: () => fetchToken(),
|
|
39
|
+
* });
|
|
40
|
+
*
|
|
41
|
+
* const { messages, sendMessage } = useChat({ transport });
|
|
42
|
+
* }
|
|
43
|
+
* ```
|
|
44
|
+
*/
|
|
45
|
+
export declare function useTriggerChatTransport<TTask extends AnyTask = AnyTask>(options: UseTriggerChatTransportOptions<TTask>): TriggerChatTransport;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
/**
|
|
3
|
+
* @module @trigger.dev/sdk/chat/react
|
|
4
|
+
*
|
|
5
|
+
* React hooks for AI SDK chat transport integration.
|
|
6
|
+
* Use alongside `@trigger.dev/sdk/chat` for a type-safe, ergonomic DX.
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* ```tsx
|
|
10
|
+
* import { useChat } from "@ai-sdk/react";
|
|
11
|
+
* import { useTriggerChatTransport } from "@trigger.dev/sdk/chat/react";
|
|
12
|
+
* import type { chat } from "@/trigger/chat";
|
|
13
|
+
*
|
|
14
|
+
* function Chat() {
|
|
15
|
+
* const transport = useTriggerChatTransport<typeof chat>({
|
|
16
|
+
* task: "ai-chat",
|
|
17
|
+
* accessToken: () => fetchToken(),
|
|
18
|
+
* });
|
|
19
|
+
*
|
|
20
|
+
* const { messages, sendMessage } = useChat({ transport });
|
|
21
|
+
* }
|
|
22
|
+
* ```
|
|
23
|
+
*/
|
|
24
|
+
import { useEffect, useRef } from "react";
|
|
25
|
+
import { TriggerChatTransport, } from "./chat.js";
|
|
26
|
+
/**
|
|
27
|
+
* React hook that creates and memoizes a `TriggerChatTransport` instance.
|
|
28
|
+
*
|
|
29
|
+
* The transport is created once on first render and reused for the lifetime
|
|
30
|
+
* of the component. This avoids the need for `useMemo` and ensures the
|
|
31
|
+
* transport's internal session state (run IDs, lastEventId, etc.)
|
|
32
|
+
* is preserved across re-renders.
|
|
33
|
+
*
|
|
34
|
+
* For dynamic access tokens, pass a function — it will be called on each
|
|
35
|
+
* request without needing to recreate the transport.
|
|
36
|
+
*
|
|
37
|
+
* The `onSessionChange` callback is kept in a ref so the transport always
|
|
38
|
+
* calls the latest version without needing to be recreated.
|
|
39
|
+
*
|
|
40
|
+
* @example
|
|
41
|
+
* ```tsx
|
|
42
|
+
* import { useChat } from "@ai-sdk/react";
|
|
43
|
+
* import { useTriggerChatTransport } from "@trigger.dev/sdk/chat/react";
|
|
44
|
+
* import type { chat } from "@/trigger/chat";
|
|
45
|
+
*
|
|
46
|
+
* function Chat() {
|
|
47
|
+
* const transport = useTriggerChatTransport<typeof chat>({
|
|
48
|
+
* task: "ai-chat",
|
|
49
|
+
* accessToken: () => fetchToken(),
|
|
50
|
+
* });
|
|
51
|
+
*
|
|
52
|
+
* const { messages, sendMessage } = useChat({ transport });
|
|
53
|
+
* }
|
|
54
|
+
* ```
|
|
55
|
+
*/
|
|
56
|
+
export function useTriggerChatTransport(options) {
|
|
57
|
+
const ref = useRef(null);
|
|
58
|
+
if (ref.current === null) {
|
|
59
|
+
ref.current = new TriggerChatTransport(options);
|
|
60
|
+
}
|
|
61
|
+
// Keep onSessionChange up to date without recreating the transport
|
|
62
|
+
const { onSessionChange } = options;
|
|
63
|
+
useEffect(() => {
|
|
64
|
+
ref.current?.setOnSessionChange(onSessionChange);
|
|
65
|
+
}, [onSessionChange]);
|
|
66
|
+
return ref.current;
|
|
67
|
+
}
|
|
68
|
+
//# sourceMappingURL=chat-react.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"chat-react.js","sourceRoot":"","sources":["../../../src/v3/chat-react.ts"],"names":[],"mappings":"AAAA,YAAY,CAAC;AAEb;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH,OAAO,EAAE,SAAS,EAAE,MAAM,EAAE,MAAM,OAAO,CAAC;AAC1C,OAAO,EACL,oBAAoB,GAErB,MAAM,WAAW,CAAC;AAmBnB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,MAAM,UAAU,uBAAuB,CACrC,OAA8C;IAE9C,MAAM,GAAG,GAAG,MAAM,CAA8B,IAAI,CAAC,CAAC;IACtD,IAAI,GAAG,CAAC,OAAO,KAAK,IAAI,EAAE,CAAC;QACzB,GAAG,CAAC,OAAO,GAAG,IAAI,oBAAoB,CAAC,OAAO,CAAC,CAAC;IAClD,CAAC;IAED,mEAAmE;IACnE,MAAM,EAAE,eAAe,EAAE,GAAG,OAAO,CAAC;IACpC,SAAS,CAAC,GAAG,EAAE;QACb,GAAG,CAAC,OAAO,EAAE,kBAAkB,CAAC,eAAe,CAAC,CAAC;IACnD,CAAC,EAAE,CAAC,eAAe,CAAC,CAAC,CAAC;IAEtB,OAAO,GAAG,CAAC,OAAO,CAAC;AACrB,CAAC"}
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module @trigger.dev/sdk/chat
|
|
3
|
+
*
|
|
4
|
+
* Browser-safe module for AI SDK chat transport integration.
|
|
5
|
+
* Use this on the frontend with the AI SDK's `useChat` hook.
|
|
6
|
+
*
|
|
7
|
+
* For backend helpers (`chatTask`, `pipeChat`), use `@trigger.dev/sdk/ai` instead.
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* ```tsx
|
|
11
|
+
* import { useChat } from "@ai-sdk/react";
|
|
12
|
+
* import { TriggerChatTransport } from "@trigger.dev/sdk/chat";
|
|
13
|
+
*
|
|
14
|
+
* function Chat({ accessToken }: { accessToken: string }) {
|
|
15
|
+
* const { messages, sendMessage, status } = useChat({
|
|
16
|
+
* transport: new TriggerChatTransport({
|
|
17
|
+
* task: "my-chat-task",
|
|
18
|
+
* accessToken,
|
|
19
|
+
* }),
|
|
20
|
+
* });
|
|
21
|
+
* }
|
|
22
|
+
* ```
|
|
23
|
+
*/
|
|
24
|
+
import type { ChatTransport, UIMessage, UIMessageChunk, ChatRequestOptions } from "ai";
|
|
25
|
+
/**
|
|
26
|
+
* Options for creating a TriggerChatTransport.
|
|
27
|
+
*/
|
|
28
|
+
export type TriggerChatTransportOptions = {
|
|
29
|
+
/**
|
|
30
|
+
* The Trigger.dev task ID to trigger for chat completions.
|
|
31
|
+
* This task should be defined using `chatTask()` from `@trigger.dev/sdk/ai`,
|
|
32
|
+
* or a regular `task()` that uses `pipeChat()`.
|
|
33
|
+
*/
|
|
34
|
+
task: string;
|
|
35
|
+
/**
|
|
36
|
+
* An access token for authenticating with the Trigger.dev API.
|
|
37
|
+
*
|
|
38
|
+
* This must be a token with permission to trigger the task. You can use:
|
|
39
|
+
* - A **trigger public token** created via `auth.createTriggerPublicToken(taskId)` (recommended for frontend use)
|
|
40
|
+
* - A **secret API key** (for server-side use only — never expose in the browser)
|
|
41
|
+
*
|
|
42
|
+
* Can also be a function that returns a token string (sync or async),
|
|
43
|
+
* useful for dynamic token refresh or passing a Next.js server action directly.
|
|
44
|
+
*/
|
|
45
|
+
accessToken: string | (() => string | Promise<string>);
|
|
46
|
+
/**
|
|
47
|
+
* Base URL for the Trigger.dev API.
|
|
48
|
+
* @default "https://api.trigger.dev"
|
|
49
|
+
*/
|
|
50
|
+
baseURL?: string;
|
|
51
|
+
/**
|
|
52
|
+
* The stream key where the task pipes UIMessageChunk data.
|
|
53
|
+
* When using `chatTask()` or `pipeChat()`, this is handled automatically.
|
|
54
|
+
* Only set this if you're using a custom stream key.
|
|
55
|
+
*
|
|
56
|
+
* @default "chat"
|
|
57
|
+
*/
|
|
58
|
+
streamKey?: string;
|
|
59
|
+
/**
|
|
60
|
+
* Additional headers to include in API requests to Trigger.dev.
|
|
61
|
+
*/
|
|
62
|
+
headers?: Record<string, string>;
|
|
63
|
+
/**
|
|
64
|
+
* The number of seconds to wait for the realtime stream to produce data
|
|
65
|
+
* before timing out.
|
|
66
|
+
*
|
|
67
|
+
* @default 120
|
|
68
|
+
*/
|
|
69
|
+
streamTimeoutSeconds?: number;
|
|
70
|
+
/**
|
|
71
|
+
* Default metadata included in every request payload.
|
|
72
|
+
* Merged with per-call `metadata` from `sendMessage()` — per-call values
|
|
73
|
+
* take precedence over transport-level defaults.
|
|
74
|
+
*
|
|
75
|
+
* Useful for data that should accompany every message, like a user ID.
|
|
76
|
+
*
|
|
77
|
+
* @example
|
|
78
|
+
* ```ts
|
|
79
|
+
* new TriggerChatTransport({
|
|
80
|
+
* task: "my-chat",
|
|
81
|
+
* accessToken,
|
|
82
|
+
* metadata: { userId: currentUser.id },
|
|
83
|
+
* });
|
|
84
|
+
* ```
|
|
85
|
+
*/
|
|
86
|
+
metadata?: Record<string, unknown>;
|
|
87
|
+
/**
|
|
88
|
+
* Restore active chat sessions from external storage (e.g. localStorage).
|
|
89
|
+
*
|
|
90
|
+
* After a page refresh, pass previously persisted sessions here so the
|
|
91
|
+
* transport can reconnect to existing runs instead of starting new ones.
|
|
92
|
+
* Use `getSession()` to retrieve session state for persistence.
|
|
93
|
+
*
|
|
94
|
+
* @example
|
|
95
|
+
* ```ts
|
|
96
|
+
* new TriggerChatTransport({
|
|
97
|
+
* task: "my-chat",
|
|
98
|
+
* accessToken,
|
|
99
|
+
* sessions: {
|
|
100
|
+
* "chat-abc": { runId: "run_123", publicAccessToken: "...", lastEventId: "42" },
|
|
101
|
+
* },
|
|
102
|
+
* });
|
|
103
|
+
* ```
|
|
104
|
+
*/
|
|
105
|
+
sessions?: Record<string, {
|
|
106
|
+
runId: string;
|
|
107
|
+
publicAccessToken: string;
|
|
108
|
+
lastEventId?: string;
|
|
109
|
+
}>;
|
|
110
|
+
/**
|
|
111
|
+
* Called whenever a chat session's state changes.
|
|
112
|
+
*
|
|
113
|
+
* Fires when:
|
|
114
|
+
* - A new session is created (after triggering a task)
|
|
115
|
+
* - A turn completes (lastEventId updated)
|
|
116
|
+
* - A session is removed (run ended or input stream send failed) — `session` will be `null`
|
|
117
|
+
*
|
|
118
|
+
* Use this to persist session state for reconnection after page refreshes,
|
|
119
|
+
* without needing to call `getSession()` manually.
|
|
120
|
+
*
|
|
121
|
+
* @example
|
|
122
|
+
* ```ts
|
|
123
|
+
* new TriggerChatTransport({
|
|
124
|
+
* task: "my-chat",
|
|
125
|
+
* accessToken,
|
|
126
|
+
* onSessionChange: (chatId, session) => {
|
|
127
|
+
* if (session) {
|
|
128
|
+
* localStorage.setItem(`session:${chatId}`, JSON.stringify(session));
|
|
129
|
+
* } else {
|
|
130
|
+
* localStorage.removeItem(`session:${chatId}`);
|
|
131
|
+
* }
|
|
132
|
+
* },
|
|
133
|
+
* });
|
|
134
|
+
* ```
|
|
135
|
+
*/
|
|
136
|
+
onSessionChange?: (chatId: string, session: {
|
|
137
|
+
runId: string;
|
|
138
|
+
publicAccessToken: string;
|
|
139
|
+
lastEventId?: string;
|
|
140
|
+
} | null) => void;
|
|
141
|
+
};
|
|
142
|
+
/**
|
|
143
|
+
* A custom AI SDK `ChatTransport` that runs chat completions as durable Trigger.dev tasks.
|
|
144
|
+
*
|
|
145
|
+
* When `sendMessages` is called, the transport:
|
|
146
|
+
* 1. Triggers a Trigger.dev task (or sends to an existing run via input streams)
|
|
147
|
+
* 2. Subscribes to the task's realtime stream to receive `UIMessageChunk` data
|
|
148
|
+
* 3. Returns a `ReadableStream<UIMessageChunk>` that the AI SDK processes natively
|
|
149
|
+
*
|
|
150
|
+
* Calling `stop()` from `useChat` sends a stop signal via input streams, which
|
|
151
|
+
* aborts the current `streamText` call in the task without ending the run.
|
|
152
|
+
*
|
|
153
|
+
* @example
|
|
154
|
+
* ```tsx
|
|
155
|
+
* import { useChat } from "@ai-sdk/react";
|
|
156
|
+
* import { TriggerChatTransport } from "@trigger.dev/sdk/chat";
|
|
157
|
+
*
|
|
158
|
+
* function Chat({ accessToken }: { accessToken: string }) {
|
|
159
|
+
* const { messages, sendMessage, stop, status } = useChat({
|
|
160
|
+
* transport: new TriggerChatTransport({
|
|
161
|
+
* task: "my-chat-task",
|
|
162
|
+
* accessToken,
|
|
163
|
+
* }),
|
|
164
|
+
* });
|
|
165
|
+
*
|
|
166
|
+
* // stop() sends a stop signal — the task aborts streamText but keeps the run alive
|
|
167
|
+
* }
|
|
168
|
+
* ```
|
|
169
|
+
*/
|
|
170
|
+
export declare class TriggerChatTransport implements ChatTransport<UIMessage> {
|
|
171
|
+
private readonly taskId;
|
|
172
|
+
private readonly resolveAccessToken;
|
|
173
|
+
private readonly baseURL;
|
|
174
|
+
private readonly streamKey;
|
|
175
|
+
private readonly extraHeaders;
|
|
176
|
+
private readonly streamTimeoutSeconds;
|
|
177
|
+
private readonly defaultMetadata;
|
|
178
|
+
private _onSessionChange;
|
|
179
|
+
private sessions;
|
|
180
|
+
constructor(options: TriggerChatTransportOptions);
|
|
181
|
+
sendMessages: (options: {
|
|
182
|
+
trigger: "submit-message" | "regenerate-message";
|
|
183
|
+
chatId: string;
|
|
184
|
+
messageId: string | undefined;
|
|
185
|
+
messages: UIMessage[];
|
|
186
|
+
abortSignal: AbortSignal | undefined;
|
|
187
|
+
} & ChatRequestOptions) => Promise<ReadableStream<UIMessageChunk>>;
|
|
188
|
+
reconnectToStream: (options: {
|
|
189
|
+
chatId: string;
|
|
190
|
+
} & ChatRequestOptions) => Promise<ReadableStream<UIMessageChunk> | null>;
|
|
191
|
+
/**
|
|
192
|
+
* Get the current session state for a chat, suitable for external persistence.
|
|
193
|
+
*
|
|
194
|
+
* Returns `undefined` if no active session exists for this chatId.
|
|
195
|
+
* Persist the returned value to localStorage so it can be restored
|
|
196
|
+
* after a page refresh via `restoreSession()`.
|
|
197
|
+
*
|
|
198
|
+
* @example
|
|
199
|
+
* ```ts
|
|
200
|
+
* const session = transport.getSession(chatId);
|
|
201
|
+
* if (session) {
|
|
202
|
+
* localStorage.setItem(`session:${chatId}`, JSON.stringify(session));
|
|
203
|
+
* }
|
|
204
|
+
* ```
|
|
205
|
+
*/
|
|
206
|
+
getSession: (chatId: string) => {
|
|
207
|
+
runId: string;
|
|
208
|
+
publicAccessToken: string;
|
|
209
|
+
lastEventId?: string;
|
|
210
|
+
} | undefined;
|
|
211
|
+
/**
|
|
212
|
+
* Update the `onSessionChange` callback.
|
|
213
|
+
* Useful for React hooks that need to update the callback without recreating the transport.
|
|
214
|
+
*/
|
|
215
|
+
setOnSessionChange(callback: ((chatId: string, session: {
|
|
216
|
+
runId: string;
|
|
217
|
+
publicAccessToken: string;
|
|
218
|
+
lastEventId?: string;
|
|
219
|
+
} | null) => void) | undefined): void;
|
|
220
|
+
private notifySessionChange;
|
|
221
|
+
private subscribeToStream;
|
|
222
|
+
}
|
|
223
|
+
/**
|
|
224
|
+
* Creates a new `TriggerChatTransport` instance.
|
|
225
|
+
*
|
|
226
|
+
* @example
|
|
227
|
+
* ```tsx
|
|
228
|
+
* import { useChat } from "@ai-sdk/react";
|
|
229
|
+
* import { createChatTransport } from "@trigger.dev/sdk/chat";
|
|
230
|
+
*
|
|
231
|
+
* const transport = createChatTransport({
|
|
232
|
+
* task: "my-chat-task",
|
|
233
|
+
* accessToken: publicAccessToken,
|
|
234
|
+
* });
|
|
235
|
+
*
|
|
236
|
+
* function Chat() {
|
|
237
|
+
* const { messages, sendMessage } = useChat({ transport });
|
|
238
|
+
* }
|
|
239
|
+
* ```
|
|
240
|
+
*/
|
|
241
|
+
export declare function createChatTransport(options: TriggerChatTransportOptions): TriggerChatTransport;
|
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module @trigger.dev/sdk/chat
|
|
3
|
+
*
|
|
4
|
+
* Browser-safe module for AI SDK chat transport integration.
|
|
5
|
+
* Use this on the frontend with the AI SDK's `useChat` hook.
|
|
6
|
+
*
|
|
7
|
+
* For backend helpers (`chatTask`, `pipeChat`), use `@trigger.dev/sdk/ai` instead.
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* ```tsx
|
|
11
|
+
* import { useChat } from "@ai-sdk/react";
|
|
12
|
+
* import { TriggerChatTransport } from "@trigger.dev/sdk/chat";
|
|
13
|
+
*
|
|
14
|
+
* function Chat({ accessToken }: { accessToken: string }) {
|
|
15
|
+
* const { messages, sendMessage, status } = useChat({
|
|
16
|
+
* transport: new TriggerChatTransport({
|
|
17
|
+
* task: "my-chat-task",
|
|
18
|
+
* accessToken,
|
|
19
|
+
* }),
|
|
20
|
+
* });
|
|
21
|
+
* }
|
|
22
|
+
* ```
|
|
23
|
+
*/
|
|
24
|
+
import { ApiClient, SSEStreamSubscription } from "@trigger.dev/core/v3";
|
|
25
|
+
import { CHAT_MESSAGES_STREAM_ID, CHAT_STOP_STREAM_ID } from "./chat-constants.js";
|
|
26
|
+
const DEFAULT_STREAM_KEY = "chat";
|
|
27
|
+
const DEFAULT_BASE_URL = "https://api.trigger.dev";
|
|
28
|
+
const DEFAULT_STREAM_TIMEOUT_SECONDS = 120;
|
|
29
|
+
/**
|
|
30
|
+
* A custom AI SDK `ChatTransport` that runs chat completions as durable Trigger.dev tasks.
|
|
31
|
+
*
|
|
32
|
+
* When `sendMessages` is called, the transport:
|
|
33
|
+
* 1. Triggers a Trigger.dev task (or sends to an existing run via input streams)
|
|
34
|
+
* 2. Subscribes to the task's realtime stream to receive `UIMessageChunk` data
|
|
35
|
+
* 3. Returns a `ReadableStream<UIMessageChunk>` that the AI SDK processes natively
|
|
36
|
+
*
|
|
37
|
+
* Calling `stop()` from `useChat` sends a stop signal via input streams, which
|
|
38
|
+
* aborts the current `streamText` call in the task without ending the run.
|
|
39
|
+
*
|
|
40
|
+
* @example
|
|
41
|
+
* ```tsx
|
|
42
|
+
* import { useChat } from "@ai-sdk/react";
|
|
43
|
+
* import { TriggerChatTransport } from "@trigger.dev/sdk/chat";
|
|
44
|
+
*
|
|
45
|
+
* function Chat({ accessToken }: { accessToken: string }) {
|
|
46
|
+
* const { messages, sendMessage, stop, status } = useChat({
|
|
47
|
+
* transport: new TriggerChatTransport({
|
|
48
|
+
* task: "my-chat-task",
|
|
49
|
+
* accessToken,
|
|
50
|
+
* }),
|
|
51
|
+
* });
|
|
52
|
+
*
|
|
53
|
+
* // stop() sends a stop signal — the task aborts streamText but keeps the run alive
|
|
54
|
+
* }
|
|
55
|
+
* ```
|
|
56
|
+
*/
|
|
57
|
+
export class TriggerChatTransport {
|
|
58
|
+
taskId;
|
|
59
|
+
resolveAccessToken;
|
|
60
|
+
baseURL;
|
|
61
|
+
streamKey;
|
|
62
|
+
extraHeaders;
|
|
63
|
+
streamTimeoutSeconds;
|
|
64
|
+
defaultMetadata;
|
|
65
|
+
_onSessionChange;
|
|
66
|
+
sessions = new Map();
|
|
67
|
+
constructor(options) {
|
|
68
|
+
this.taskId = options.task;
|
|
69
|
+
this.resolveAccessToken =
|
|
70
|
+
typeof options.accessToken === "function"
|
|
71
|
+
? options.accessToken
|
|
72
|
+
: () => options.accessToken;
|
|
73
|
+
this.baseURL = options.baseURL ?? DEFAULT_BASE_URL;
|
|
74
|
+
this.streamKey = options.streamKey ?? DEFAULT_STREAM_KEY;
|
|
75
|
+
this.extraHeaders = options.headers ?? {};
|
|
76
|
+
this.streamTimeoutSeconds = options.streamTimeoutSeconds ?? DEFAULT_STREAM_TIMEOUT_SECONDS;
|
|
77
|
+
this.defaultMetadata = options.metadata;
|
|
78
|
+
this._onSessionChange = options.onSessionChange;
|
|
79
|
+
// Restore sessions from external storage
|
|
80
|
+
if (options.sessions) {
|
|
81
|
+
for (const [chatId, session] of Object.entries(options.sessions)) {
|
|
82
|
+
this.sessions.set(chatId, {
|
|
83
|
+
runId: session.runId,
|
|
84
|
+
publicAccessToken: session.publicAccessToken,
|
|
85
|
+
lastEventId: session.lastEventId,
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
sendMessages = async (options) => {
|
|
91
|
+
const { trigger, chatId, messageId, messages, abortSignal, body, metadata } = options;
|
|
92
|
+
const mergedMetadata = this.defaultMetadata || metadata
|
|
93
|
+
? { ...(this.defaultMetadata ?? {}), ...(metadata ?? {}) }
|
|
94
|
+
: undefined;
|
|
95
|
+
const payload = {
|
|
96
|
+
...(body ?? {}),
|
|
97
|
+
messages,
|
|
98
|
+
chatId,
|
|
99
|
+
trigger,
|
|
100
|
+
messageId,
|
|
101
|
+
metadata: mergedMetadata,
|
|
102
|
+
};
|
|
103
|
+
const session = this.sessions.get(chatId);
|
|
104
|
+
// If we have an existing run, send the message via input stream
|
|
105
|
+
// to resume the conversation in the same run.
|
|
106
|
+
if (session?.runId) {
|
|
107
|
+
try {
|
|
108
|
+
// Keep wire payloads minimal — the backend accumulates the full history.
|
|
109
|
+
// For submit-message: only send the new user message (always the last one).
|
|
110
|
+
// For regenerate-message: send full history so the backend can reset its accumulator.
|
|
111
|
+
const minimalPayload = {
|
|
112
|
+
...payload,
|
|
113
|
+
messages: trigger === "submit-message" ? messages.slice(-1) : messages,
|
|
114
|
+
};
|
|
115
|
+
const apiClient = new ApiClient(this.baseURL, session.publicAccessToken);
|
|
116
|
+
await apiClient.sendInputStream(session.runId, CHAT_MESSAGES_STREAM_ID, minimalPayload);
|
|
117
|
+
return this.subscribeToStream(session.runId, session.publicAccessToken, abortSignal, chatId);
|
|
118
|
+
}
|
|
119
|
+
catch {
|
|
120
|
+
// If sending fails (run died, etc.), fall through to trigger a new run.
|
|
121
|
+
this.sessions.delete(chatId);
|
|
122
|
+
this.notifySessionChange(chatId, null);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
// First message or run has ended — trigger a new run
|
|
126
|
+
const currentToken = await this.resolveAccessToken();
|
|
127
|
+
const apiClient = new ApiClient(this.baseURL, currentToken);
|
|
128
|
+
const triggerResponse = await apiClient.triggerTask(this.taskId, {
|
|
129
|
+
payload,
|
|
130
|
+
options: {
|
|
131
|
+
payloadType: "application/json",
|
|
132
|
+
},
|
|
133
|
+
});
|
|
134
|
+
const runId = triggerResponse.id;
|
|
135
|
+
const publicAccessToken = "publicAccessToken" in triggerResponse
|
|
136
|
+
? triggerResponse.publicAccessToken
|
|
137
|
+
: undefined;
|
|
138
|
+
const newSession = {
|
|
139
|
+
runId,
|
|
140
|
+
publicAccessToken: publicAccessToken ?? currentToken,
|
|
141
|
+
};
|
|
142
|
+
this.sessions.set(chatId, newSession);
|
|
143
|
+
this.notifySessionChange(chatId, newSession);
|
|
144
|
+
return this.subscribeToStream(runId, publicAccessToken ?? currentToken, abortSignal, chatId);
|
|
145
|
+
};
|
|
146
|
+
reconnectToStream = async (options) => {
|
|
147
|
+
const session = this.sessions.get(options.chatId);
|
|
148
|
+
if (!session) {
|
|
149
|
+
return null;
|
|
150
|
+
}
|
|
151
|
+
return this.subscribeToStream(session.runId, session.publicAccessToken, undefined, options.chatId);
|
|
152
|
+
};
|
|
153
|
+
/**
|
|
154
|
+
* Get the current session state for a chat, suitable for external persistence.
|
|
155
|
+
*
|
|
156
|
+
* Returns `undefined` if no active session exists for this chatId.
|
|
157
|
+
* Persist the returned value to localStorage so it can be restored
|
|
158
|
+
* after a page refresh via `restoreSession()`.
|
|
159
|
+
*
|
|
160
|
+
* @example
|
|
161
|
+
* ```ts
|
|
162
|
+
* const session = transport.getSession(chatId);
|
|
163
|
+
* if (session) {
|
|
164
|
+
* localStorage.setItem(`session:${chatId}`, JSON.stringify(session));
|
|
165
|
+
* }
|
|
166
|
+
* ```
|
|
167
|
+
*/
|
|
168
|
+
getSession = (chatId) => {
|
|
169
|
+
const session = this.sessions.get(chatId);
|
|
170
|
+
if (!session)
|
|
171
|
+
return undefined;
|
|
172
|
+
return {
|
|
173
|
+
runId: session.runId,
|
|
174
|
+
publicAccessToken: session.publicAccessToken,
|
|
175
|
+
lastEventId: session.lastEventId,
|
|
176
|
+
};
|
|
177
|
+
};
|
|
178
|
+
/**
|
|
179
|
+
* Update the `onSessionChange` callback.
|
|
180
|
+
* Useful for React hooks that need to update the callback without recreating the transport.
|
|
181
|
+
*/
|
|
182
|
+
setOnSessionChange(callback) {
|
|
183
|
+
this._onSessionChange = callback;
|
|
184
|
+
}
|
|
185
|
+
notifySessionChange(chatId, session) {
|
|
186
|
+
if (!this._onSessionChange)
|
|
187
|
+
return;
|
|
188
|
+
if (session) {
|
|
189
|
+
this._onSessionChange(chatId, {
|
|
190
|
+
runId: session.runId,
|
|
191
|
+
publicAccessToken: session.publicAccessToken,
|
|
192
|
+
lastEventId: session.lastEventId,
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
else {
|
|
196
|
+
this._onSessionChange(chatId, null);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
subscribeToStream(runId, accessToken, abortSignal, chatId) {
|
|
200
|
+
const headers = {
|
|
201
|
+
Authorization: `Bearer ${accessToken}`,
|
|
202
|
+
...this.extraHeaders,
|
|
203
|
+
};
|
|
204
|
+
// When resuming a run, skip past previously-seen events
|
|
205
|
+
// so we only receive the new turn's response.
|
|
206
|
+
const session = chatId ? this.sessions.get(chatId) : undefined;
|
|
207
|
+
// Create an internal AbortController so we can terminate the underlying
|
|
208
|
+
// fetch connection when we're done reading (e.g. after intercepting the
|
|
209
|
+
// control chunk). Without this, the SSE connection stays open and leaks.
|
|
210
|
+
const internalAbort = new AbortController();
|
|
211
|
+
const combinedSignal = abortSignal
|
|
212
|
+
? AbortSignal.any([abortSignal, internalAbort.signal])
|
|
213
|
+
: internalAbort.signal;
|
|
214
|
+
// When the caller aborts (user calls stop()), send a stop signal to the
|
|
215
|
+
// running task via input streams, then close the SSE connection.
|
|
216
|
+
if (abortSignal) {
|
|
217
|
+
abortSignal.addEventListener("abort", () => {
|
|
218
|
+
if (session) {
|
|
219
|
+
session.skipToTurnComplete = true;
|
|
220
|
+
const api = new ApiClient(this.baseURL, session.publicAccessToken);
|
|
221
|
+
api
|
|
222
|
+
.sendInputStream(session.runId, CHAT_STOP_STREAM_ID, { stop: true })
|
|
223
|
+
.catch(() => { }); // Best-effort
|
|
224
|
+
}
|
|
225
|
+
internalAbort.abort();
|
|
226
|
+
}, { once: true });
|
|
227
|
+
}
|
|
228
|
+
const subscription = new SSEStreamSubscription(`${this.baseURL}/realtime/v1/streams/${runId}/${this.streamKey}`, {
|
|
229
|
+
headers,
|
|
230
|
+
signal: combinedSignal,
|
|
231
|
+
timeoutInSeconds: this.streamTimeoutSeconds,
|
|
232
|
+
lastEventId: session?.lastEventId,
|
|
233
|
+
});
|
|
234
|
+
return new ReadableStream({
|
|
235
|
+
start: async (controller) => {
|
|
236
|
+
try {
|
|
237
|
+
const sseStream = await subscription.subscribe();
|
|
238
|
+
const reader = sseStream.getReader();
|
|
239
|
+
let chunkCount = 0;
|
|
240
|
+
try {
|
|
241
|
+
while (true) {
|
|
242
|
+
const { done, value } = await reader.read();
|
|
243
|
+
if (done) {
|
|
244
|
+
// Only delete session if the stream ended naturally (not aborted by stop).
|
|
245
|
+
// When the user clicks stop, the abort closes the SSE reader which
|
|
246
|
+
// returns done=true, but the run is still alive and waiting for
|
|
247
|
+
// the next message via input streams.
|
|
248
|
+
if (chatId && !combinedSignal.aborted) {
|
|
249
|
+
this.sessions.delete(chatId);
|
|
250
|
+
this.notifySessionChange(chatId, null);
|
|
251
|
+
}
|
|
252
|
+
controller.close();
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
if (combinedSignal.aborted) {
|
|
256
|
+
internalAbort.abort();
|
|
257
|
+
await reader.cancel();
|
|
258
|
+
controller.close();
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
// Track the last event ID so we can resume from here
|
|
262
|
+
if (value.id && session) {
|
|
263
|
+
session.lastEventId = value.id;
|
|
264
|
+
}
|
|
265
|
+
// Guard against heartbeat or malformed SSE events
|
|
266
|
+
if (value.chunk != null && typeof value.chunk === "object") {
|
|
267
|
+
const chunk = value.chunk;
|
|
268
|
+
// After a stop, skip leftover chunks from the stopped turn
|
|
269
|
+
// until we see the __trigger_turn_complete marker.
|
|
270
|
+
if (session?.skipToTurnComplete) {
|
|
271
|
+
if (chunk.type === "__trigger_turn_complete") {
|
|
272
|
+
session.skipToTurnComplete = false;
|
|
273
|
+
chunkCount = 0;
|
|
274
|
+
}
|
|
275
|
+
continue;
|
|
276
|
+
}
|
|
277
|
+
if (chunk.type === "__trigger_turn_complete" && chatId) {
|
|
278
|
+
// Notify with updated lastEventId before closing
|
|
279
|
+
if (session) {
|
|
280
|
+
this.notifySessionChange(chatId, session);
|
|
281
|
+
}
|
|
282
|
+
internalAbort.abort();
|
|
283
|
+
try {
|
|
284
|
+
controller.close();
|
|
285
|
+
}
|
|
286
|
+
catch {
|
|
287
|
+
// Controller may already be closed
|
|
288
|
+
}
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
chunkCount++;
|
|
292
|
+
controller.enqueue(chunk);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
catch (readError) {
|
|
297
|
+
reader.releaseLock();
|
|
298
|
+
throw readError;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
catch (error) {
|
|
302
|
+
if (error instanceof Error && error.name === "AbortError") {
|
|
303
|
+
try {
|
|
304
|
+
controller.close();
|
|
305
|
+
}
|
|
306
|
+
catch {
|
|
307
|
+
// Controller may already be closed
|
|
308
|
+
}
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
controller.error(error);
|
|
312
|
+
}
|
|
313
|
+
},
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
/**
|
|
318
|
+
* Creates a new `TriggerChatTransport` instance.
|
|
319
|
+
*
|
|
320
|
+
* @example
|
|
321
|
+
* ```tsx
|
|
322
|
+
* import { useChat } from "@ai-sdk/react";
|
|
323
|
+
* import { createChatTransport } from "@trigger.dev/sdk/chat";
|
|
324
|
+
*
|
|
325
|
+
* const transport = createChatTransport({
|
|
326
|
+
* task: "my-chat-task",
|
|
327
|
+
* accessToken: publicAccessToken,
|
|
328
|
+
* });
|
|
329
|
+
*
|
|
330
|
+
* function Chat() {
|
|
331
|
+
* const { messages, sendMessage } = useChat({ transport });
|
|
332
|
+
* }
|
|
333
|
+
* ```
|
|
334
|
+
*/
|
|
335
|
+
export function createChatTransport(options) {
|
|
336
|
+
return new TriggerChatTransport(options);
|
|
337
|
+
}
|
|
338
|
+
//# sourceMappingURL=chat.js.map
|