@trigger.dev/sdk 4.5.0-rc.5 → 4.5.0-rc.7
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 +178 -5
- package/dist/commonjs/v3/ai.js +603 -119
- package/dist/commonjs/v3/ai.js.map +1 -1
- package/dist/commonjs/v3/chat-client.js +3 -0
- package/dist/commonjs/v3/chat-client.js.map +1 -1
- package/dist/commonjs/v3/chat-react.js +10 -7
- package/dist/commonjs/v3/chat-react.js.map +1 -1
- package/dist/commonjs/v3/chat-server.d.ts +8 -0
- package/dist/commonjs/v3/chat-server.js +32 -10
- package/dist/commonjs/v3/chat-server.js.map +1 -1
- package/dist/commonjs/v3/chat-server.test.js +51 -0
- package/dist/commonjs/v3/chat-server.test.js.map +1 -1
- package/dist/commonjs/v3/chat.js +34 -6
- package/dist/commonjs/v3/chat.js.map +1 -1
- package/dist/commonjs/v3/chat.test.js +53 -0
- package/dist/commonjs/v3/chat.test.js.map +1 -1
- package/dist/commonjs/v3/createStartSessionAction.test.js +30 -0
- package/dist/commonjs/v3/createStartSessionAction.test.js.map +1 -1
- package/dist/commonjs/v3/sessions.d.ts +11 -6
- package/dist/commonjs/v3/sessions.js +10 -5
- package/dist/commonjs/v3/sessions.js.map +1 -1
- package/dist/commonjs/v3/test/mock-chat-agent.d.ts +6 -0
- package/dist/commonjs/v3/test/mock-chat-agent.js +1 -0
- package/dist/commonjs/v3/test/mock-chat-agent.js.map +1 -1
- package/dist/commonjs/version.js +1 -1
- package/dist/esm/v3/ai.d.ts +178 -5
- package/dist/esm/v3/ai.js +603 -120
- package/dist/esm/v3/ai.js.map +1 -1
- package/dist/esm/v3/chat-client.js +3 -0
- package/dist/esm/v3/chat-client.js.map +1 -1
- package/dist/esm/v3/chat-react.js +10 -7
- package/dist/esm/v3/chat-react.js.map +1 -1
- package/dist/esm/v3/chat-server.d.ts +8 -0
- package/dist/esm/v3/chat-server.js +32 -10
- package/dist/esm/v3/chat-server.js.map +1 -1
- package/dist/esm/v3/chat-server.test.js +51 -0
- package/dist/esm/v3/chat-server.test.js.map +1 -1
- package/dist/esm/v3/chat.js +34 -6
- package/dist/esm/v3/chat.js.map +1 -1
- package/dist/esm/v3/chat.test.js +53 -0
- package/dist/esm/v3/chat.test.js.map +1 -1
- package/dist/esm/v3/createStartSessionAction.test.js +30 -0
- package/dist/esm/v3/createStartSessionAction.test.js.map +1 -1
- package/dist/esm/v3/sessions.d.ts +11 -6
- package/dist/esm/v3/sessions.js +10 -5
- package/dist/esm/v3/sessions.js.map +1 -1
- package/dist/esm/v3/test/mock-chat-agent.d.ts +6 -0
- package/dist/esm/v3/test/mock-chat-agent.js +1 -0
- package/dist/esm/v3/test/mock-chat-agent.js.map +1 -1
- package/dist/esm/version.js +1 -1
- package/docs/ai/prompts.mdx +430 -0
- package/docs/ai-chat/actions.mdx +115 -0
- package/docs/ai-chat/anatomy.mdx +71 -0
- package/docs/ai-chat/backend.mdx +817 -0
- package/docs/ai-chat/background-injection.mdx +221 -0
- package/docs/ai-chat/changelog.mdx +850 -0
- package/docs/ai-chat/chat-local.mdx +174 -0
- package/docs/ai-chat/client-protocol.mdx +1081 -0
- package/docs/ai-chat/compaction.mdx +411 -0
- package/docs/ai-chat/custom-agents.mdx +364 -0
- package/docs/ai-chat/error-handling.mdx +415 -0
- package/docs/ai-chat/fast-starts.mdx +672 -0
- package/docs/ai-chat/frontend.mdx +580 -0
- package/docs/ai-chat/how-it-works.mdx +230 -0
- package/docs/ai-chat/lifecycle-hooks.mdx +530 -0
- package/docs/ai-chat/mcp.mdx +101 -0
- package/docs/ai-chat/overview.mdx +90 -0
- package/docs/ai-chat/patterns/branching-conversations.mdx +284 -0
- package/docs/ai-chat/patterns/code-sandbox.mdx +126 -0
- package/docs/ai-chat/patterns/database-persistence.mdx +414 -0
- package/docs/ai-chat/patterns/human-in-the-loop.mdx +275 -0
- package/docs/ai-chat/patterns/large-payloads.mdx +169 -0
- package/docs/ai-chat/patterns/oom-resilience.mdx +120 -0
- package/docs/ai-chat/patterns/persistence-and-replay.mdx +211 -0
- package/docs/ai-chat/patterns/recovery-boot.mdx +230 -0
- package/docs/ai-chat/patterns/skills.mdx +221 -0
- package/docs/ai-chat/patterns/sub-agents.mdx +383 -0
- package/docs/ai-chat/patterns/tool-result-auditing.mdx +148 -0
- package/docs/ai-chat/patterns/trusted-edge-signals.mdx +337 -0
- package/docs/ai-chat/patterns/version-upgrades.mdx +172 -0
- package/docs/ai-chat/pending-messages.mdx +343 -0
- package/docs/ai-chat/prompt-caching.mdx +206 -0
- package/docs/ai-chat/quick-start.mdx +161 -0
- package/docs/ai-chat/reference.mdx +909 -0
- package/docs/ai-chat/server-chat.mdx +263 -0
- package/docs/ai-chat/sessions.mdx +333 -0
- package/docs/ai-chat/testing.mdx +682 -0
- package/docs/ai-chat/tools.mdx +191 -0
- package/docs/ai-chat/types.mdx +242 -0
- package/docs/ai-chat/upgrade-guide.mdx +515 -0
- package/docs/apikeys.mdx +54 -0
- package/docs/building-with-ai.mdx +261 -0
- package/docs/bulk-actions.mdx +49 -0
- package/docs/changelog.mdx +6 -0
- package/docs/cli-deploy-commands.mdx +9 -0
- package/docs/cli-dev-commands.mdx +9 -0
- package/docs/cli-dev.mdx +8 -0
- package/docs/cli-init-commands.mdx +58 -0
- package/docs/cli-introduction.mdx +25 -0
- package/docs/cli-list-profiles-commands.mdx +42 -0
- package/docs/cli-login-commands.mdx +33 -0
- package/docs/cli-logout-commands.mdx +33 -0
- package/docs/cli-preview-archive.mdx +59 -0
- package/docs/cli-promote-commands.mdx +9 -0
- package/docs/cli-switch.mdx +43 -0
- package/docs/cli-update-commands.mdx +42 -0
- package/docs/cli-whoami-commands.mdx +33 -0
- package/docs/community.mdx +6 -0
- package/docs/config/config-file.mdx +602 -0
- package/docs/config/extensions/additionalFiles.mdx +38 -0
- package/docs/config/extensions/additionalPackages.mdx +40 -0
- package/docs/config/extensions/aptGet.mdx +34 -0
- package/docs/config/extensions/audioWaveform.mdx +20 -0
- package/docs/config/extensions/custom.mdx +380 -0
- package/docs/config/extensions/emitDecoratorMetadata.mdx +29 -0
- package/docs/config/extensions/esbuildPlugin.mdx +31 -0
- package/docs/config/extensions/ffmpeg.mdx +45 -0
- package/docs/config/extensions/lightpanda.mdx +56 -0
- package/docs/config/extensions/overview.mdx +67 -0
- package/docs/config/extensions/playwright.mdx +195 -0
- package/docs/config/extensions/prismaExtension.mdx +1014 -0
- package/docs/config/extensions/puppeteer.mdx +30 -0
- package/docs/config/extensions/pythonExtension.mdx +182 -0
- package/docs/config/extensions/syncEnvVars.mdx +291 -0
- package/docs/context.mdx +235 -0
- package/docs/database-connections.mdx +213 -0
- package/docs/deploy-environment-variables.mdx +435 -0
- package/docs/deployment/atomic-deployment.mdx +172 -0
- package/docs/deployment/overview.mdx +257 -0
- package/docs/deployment/preview-branches.mdx +224 -0
- package/docs/errors-retrying.mdx +379 -0
- package/docs/github-actions.mdx +222 -0
- package/docs/github-integration.mdx +136 -0
- package/docs/github-repo.mdx +8 -0
- package/docs/help-email.mdx +6 -0
- package/docs/help-slack.mdx +11 -0
- package/docs/hidden-tasks.mdx +56 -0
- package/docs/how-it-works.mdx +454 -0
- package/docs/how-to-reduce-your-spend.mdx +217 -0
- package/docs/idempotency.mdx +504 -0
- package/docs/introduction.mdx +223 -0
- package/docs/limits.mdx +241 -0
- package/docs/logging.mdx +195 -0
- package/docs/machines.mdx +952 -0
- package/docs/manual-setup.mdx +632 -0
- package/docs/mcp-agent-rules.mdx +41 -0
- package/docs/mcp-introduction.mdx +385 -0
- package/docs/mcp-tools.mdx +273 -0
- package/docs/migrating-from-v3.mdx +334 -0
- package/docs/observability/dashboards.mdx +102 -0
- package/docs/observability/query.mdx +585 -0
- package/docs/open-source-contributing.mdx +16 -0
- package/docs/open-source-self-hosting.mdx +541 -0
- package/docs/private-networking/aws-console-setup.mdx +304 -0
- package/docs/private-networking/overview.mdx +144 -0
- package/docs/private-networking/troubleshooting.mdx +78 -0
- package/docs/queue-concurrency.mdx +354 -0
- package/docs/quick-start.mdx +97 -0
- package/docs/realtime/auth.mdx +208 -0
- package/docs/realtime/backend/overview.mdx +45 -0
- package/docs/realtime/backend/streams.mdx +418 -0
- package/docs/realtime/backend/subscribe.mdx +225 -0
- package/docs/realtime/how-it-works.mdx +94 -0
- package/docs/realtime/overview.mdx +63 -0
- package/docs/realtime/react-hooks/overview.mdx +73 -0
- package/docs/realtime/react-hooks/streams.mdx +449 -0
- package/docs/realtime/react-hooks/subscribe.mdx +674 -0
- package/docs/realtime/react-hooks/swr.mdx +87 -0
- package/docs/realtime/react-hooks/triggering.mdx +194 -0
- package/docs/realtime/react-hooks/use-wait-token.mdx +34 -0
- package/docs/realtime/run-object.mdx +174 -0
- package/docs/replaying.mdx +72 -0
- package/docs/request-feature.mdx +6 -0
- package/docs/roadmap.mdx +6 -0
- package/docs/run-tests.mdx +20 -0
- package/docs/run-usage.mdx +113 -0
- package/docs/runs/heartbeats.mdx +38 -0
- package/docs/runs/max-duration.mdx +139 -0
- package/docs/runs/metadata.mdx +734 -0
- package/docs/runs/priority.mdx +31 -0
- package/docs/runs.mdx +396 -0
- package/docs/self-hosting/docker.mdx +458 -0
- package/docs/self-hosting/env/supervisor.mdx +74 -0
- package/docs/self-hosting/env/webapp.mdx +276 -0
- package/docs/self-hosting/kubernetes.mdx +601 -0
- package/docs/self-hosting/overview.mdx +108 -0
- package/docs/skills.mdx +85 -0
- package/docs/tags.mdx +120 -0
- package/docs/tasks/overview.mdx +697 -0
- package/docs/tasks/scheduled.mdx +382 -0
- package/docs/tasks/schemaTask.mdx +413 -0
- package/docs/tasks/streams.mdx +884 -0
- package/docs/triggering.mdx +1320 -0
- package/docs/troubleshooting-alerts.mdx +385 -0
- package/docs/troubleshooting-debugging-in-vscode.mdx +8 -0
- package/docs/troubleshooting-github-issues.mdx +6 -0
- package/docs/troubleshooting-uptime-status.mdx +6 -0
- package/docs/troubleshooting.mdx +398 -0
- package/docs/upgrading-packages.mdx +80 -0
- package/docs/vercel-integration.mdx +207 -0
- package/docs/versioning.mdx +56 -0
- package/docs/video-walkthrough.mdx +23 -0
- package/docs/wait-for-token.mdx +540 -0
- package/docs/wait-for.mdx +42 -0
- package/docs/wait-until.mdx +53 -0
- package/docs/wait.mdx +18 -0
- package/docs/writing-tasks-introduction.mdx +33 -0
- package/package.json +10 -6
- package/skills/trigger-authoring-chat-agent/SKILL.md +296 -0
- package/skills/trigger-authoring-tasks/SKILL.md +254 -0
- package/skills/trigger-chat-agent-advanced/SKILL.md +368 -0
- package/skills/trigger-cost-savings/SKILL.md +116 -0
- package/skills/trigger-realtime-and-frontend/SKILL.md +276 -0
|
@@ -0,0 +1,884 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "Streaming data from tasks"
|
|
3
|
+
sidebarTitle: "Streams"
|
|
4
|
+
description: "Pipe continuous data from your Trigger.dev tasks to frontend or backend clients in real time. Stream AI completions, file chunks, progress updates, and more."
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
**Streams let you pipe data from a running task to your frontend or backend as it's produced.** Think AI completions token by token, progress updates, or file chunks. You can also **send data into** running tasks with [Input Streams](#input-streams) for bidirectional flows (cancel buttons, approvals).
|
|
8
|
+
|
|
9
|
+
For subscribing to **run state changes** (status, metadata, tags) instead, see [Realtime](/realtime/overview).
|
|
10
|
+
|
|
11
|
+
<Note>
|
|
12
|
+
Streams require SDK version **4.1.0 or later** (`@trigger.dev/sdk` and `@trigger.dev/react-hooks`).
|
|
13
|
+
This doc describes the current streams behavior (v2 is the default). For pre-4.1.0 streams, see
|
|
14
|
+
[Pre-4.1.0 streams (legacy)](#pre-410-streams-legacy) below.
|
|
15
|
+
</Note>
|
|
16
|
+
|
|
17
|
+
## Overview
|
|
18
|
+
|
|
19
|
+
Streams provide:
|
|
20
|
+
|
|
21
|
+
- **Unlimited stream length** (previously capped at 2000 chunks)
|
|
22
|
+
- **Unlimited active streams per run** (previously 5)
|
|
23
|
+
- **Improved reliability** with automatic resumption on connection loss
|
|
24
|
+
- **28-day stream retention** (previously 1 day)
|
|
25
|
+
- **Multiple client streams** can pipe to a single stream
|
|
26
|
+
- **Enhanced dashboard visibility** for viewing stream data in real-time
|
|
27
|
+
|
|
28
|
+
Streams v2 is the **default** when using SDK 4.1.0 or later. If you trigger tasks outside the SDK, set the `x-trigger-realtime-streams-version=v2` header. To opt out, use `auth.configure({ future: { v2RealtimeStreams: false } })` or `TRIGGER_V2_REALTIME_STREAMS=0`.
|
|
29
|
+
|
|
30
|
+
## Limits Comparison
|
|
31
|
+
|
|
32
|
+
| Limit | Legacy (pre-4.1.0) | Current |
|
|
33
|
+
| -------------------------------- | ------------------ | --------- |
|
|
34
|
+
| Maximum stream length | 2000 | Unlimited |
|
|
35
|
+
| Number of active streams per run | 5 | Unlimited |
|
|
36
|
+
| Maximum streams per run | 10 | Unlimited |
|
|
37
|
+
| Maximum stream TTL | 1 day | 28 days |
|
|
38
|
+
| Maximum stream size | 10MB | 300 MiB |
|
|
39
|
+
|
|
40
|
+
## Quick Start
|
|
41
|
+
|
|
42
|
+
The recommended workflow for **output** streams (data from task to client):
|
|
43
|
+
|
|
44
|
+
1. **Define your streams** in a shared location using `streams.define()`
|
|
45
|
+
2. **Use the defined stream** in your tasks with `.pipe()`, `.append()`, or `.writer()`
|
|
46
|
+
3. **Read from the stream** using `.read()` or the `useRealtimeStream` hook in React
|
|
47
|
+
|
|
48
|
+
This approach gives you full type safety, better code organization, and easier maintenance as your application grows. For **input** streams (sending data into a running task), see [Input Streams](#input-streams) below.
|
|
49
|
+
|
|
50
|
+
## Defining Typed Streams (Recommended)
|
|
51
|
+
|
|
52
|
+
The recommended way to work with streams is to define them once with `streams.define()`. This allows you to specify the chunk type and stream ID in one place, and then reuse that definition throughout your codebase with full type safety.
|
|
53
|
+
|
|
54
|
+
### Creating a Defined Stream
|
|
55
|
+
|
|
56
|
+
Define your streams in a shared location (like `app/streams.ts` or `trigger/streams.ts`):
|
|
57
|
+
|
|
58
|
+
```ts
|
|
59
|
+
import { streams, InferStreamType } from "@trigger.dev/sdk";
|
|
60
|
+
|
|
61
|
+
// Define a stream with a specific type
|
|
62
|
+
export const aiStream = streams.define<string>({
|
|
63
|
+
id: "ai-output",
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
// Export the type for use in frontend components
|
|
67
|
+
export type AIStreamPart = InferStreamType<typeof aiStream>;
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
You can define streams for any JSON-serializable type:
|
|
71
|
+
|
|
72
|
+
```ts
|
|
73
|
+
import { streams, InferStreamType } from "@trigger.dev/sdk";
|
|
74
|
+
import { UIMessageChunk } from "ai";
|
|
75
|
+
|
|
76
|
+
// Stream for AI UI message chunks
|
|
77
|
+
export const aiStream = streams.define<UIMessageChunk>({
|
|
78
|
+
id: "ai",
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
// Stream for progress updates
|
|
82
|
+
export const progressStream = streams.define<{ step: string; percent: number }>({
|
|
83
|
+
id: "progress",
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
// Stream for simple text
|
|
87
|
+
export const logStream = streams.define<string>({
|
|
88
|
+
id: "logs",
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
// Export types
|
|
92
|
+
export type AIStreamPart = InferStreamType<typeof aiStream>;
|
|
93
|
+
export type ProgressStreamPart = InferStreamType<typeof progressStream>;
|
|
94
|
+
export type LogStreamPart = InferStreamType<typeof logStream>;
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
### Using Defined Streams in Tasks
|
|
98
|
+
|
|
99
|
+
Once defined, you can use all stream methods on your defined stream:
|
|
100
|
+
|
|
101
|
+
```ts
|
|
102
|
+
import { task } from "@trigger.dev/sdk";
|
|
103
|
+
import { aiStream } from "./streams";
|
|
104
|
+
|
|
105
|
+
export const streamTask = task({
|
|
106
|
+
id: "stream-task",
|
|
107
|
+
run: async (payload: { prompt: string }) => {
|
|
108
|
+
// Get a stream from an AI service, database, etc.
|
|
109
|
+
const stream = await getAIStream(payload.prompt);
|
|
110
|
+
|
|
111
|
+
// Pipe the stream using your defined stream
|
|
112
|
+
const { stream: readableStream, waitUntilComplete } = aiStream.pipe(stream);
|
|
113
|
+
|
|
114
|
+
// Option A: Iterate over the stream locally
|
|
115
|
+
for await (const chunk of readableStream) {
|
|
116
|
+
console.log("Received chunk:", chunk);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Option B: Wait for the stream to complete
|
|
120
|
+
await waitUntilComplete();
|
|
121
|
+
|
|
122
|
+
return { message: "Stream completed" };
|
|
123
|
+
},
|
|
124
|
+
});
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
#### Reading from a Stream
|
|
128
|
+
|
|
129
|
+
Use the defined stream's `read()` method to consume data from anywhere (frontend, backend, or another task):
|
|
130
|
+
|
|
131
|
+
```ts
|
|
132
|
+
import { aiStream } from "./streams";
|
|
133
|
+
|
|
134
|
+
const stream = await aiStream.read(runId);
|
|
135
|
+
|
|
136
|
+
for await (const chunk of stream) {
|
|
137
|
+
console.log(chunk); // chunk is typed as the stream's chunk type
|
|
138
|
+
}
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
With options:
|
|
142
|
+
|
|
143
|
+
```ts
|
|
144
|
+
const stream = await aiStream.read(runId, {
|
|
145
|
+
timeoutInSeconds: 60, // Stop if no data for 60 seconds
|
|
146
|
+
startIndex: 10, // Start from the 10th chunk
|
|
147
|
+
});
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
#### Appending to a Stream
|
|
151
|
+
|
|
152
|
+
Use the defined stream's `append()` method to add a single chunk:
|
|
153
|
+
|
|
154
|
+
```ts
|
|
155
|
+
import { task } from "@trigger.dev/sdk";
|
|
156
|
+
import { aiStream, progressStream, logStream } from "./streams";
|
|
157
|
+
|
|
158
|
+
export const appendTask = task({
|
|
159
|
+
id: "append-task",
|
|
160
|
+
run: async (payload) => {
|
|
161
|
+
// Append to different streams with full type safety
|
|
162
|
+
await logStream.append("Processing started");
|
|
163
|
+
await progressStream.append({ step: "Initialization", percent: 0 });
|
|
164
|
+
|
|
165
|
+
// Do some work...
|
|
166
|
+
|
|
167
|
+
await progressStream.append({ step: "Processing", percent: 50 });
|
|
168
|
+
await logStream.append("Step 1 complete");
|
|
169
|
+
|
|
170
|
+
// Do more work...
|
|
171
|
+
|
|
172
|
+
await progressStream.append({ step: "Complete", percent: 100 });
|
|
173
|
+
await logStream.append("All steps complete");
|
|
174
|
+
},
|
|
175
|
+
});
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
#### Writing Multiple Chunks
|
|
179
|
+
|
|
180
|
+
Use the defined stream's `writer()` method for more complex stream writing:
|
|
181
|
+
|
|
182
|
+
```ts
|
|
183
|
+
import { task } from "@trigger.dev/sdk";
|
|
184
|
+
import { logStream } from "./streams";
|
|
185
|
+
|
|
186
|
+
export const writerTask = task({
|
|
187
|
+
id: "writer-task",
|
|
188
|
+
run: async (payload) => {
|
|
189
|
+
const { waitUntilComplete } = logStream.writer({
|
|
190
|
+
execute: ({ write, merge }) => {
|
|
191
|
+
// Write individual chunks
|
|
192
|
+
write("Chunk 1");
|
|
193
|
+
write("Chunk 2");
|
|
194
|
+
|
|
195
|
+
// Merge another stream
|
|
196
|
+
const additionalStream = ReadableStream.from(["Chunk 3", "Chunk 4", "Chunk 5"]);
|
|
197
|
+
merge(additionalStream);
|
|
198
|
+
},
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
await waitUntilComplete();
|
|
202
|
+
},
|
|
203
|
+
});
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
### Using Defined Streams in React
|
|
207
|
+
|
|
208
|
+
Defined streams work seamlessly with the `useRealtimeStream` hook:
|
|
209
|
+
|
|
210
|
+
```tsx
|
|
211
|
+
"use client";
|
|
212
|
+
|
|
213
|
+
import { useRealtimeStream } from "@trigger.dev/react-hooks";
|
|
214
|
+
import { aiStream } from "@/app/streams";
|
|
215
|
+
|
|
216
|
+
export function StreamViewer({ accessToken, runId }: { accessToken: string; runId: string }) {
|
|
217
|
+
// Pass the defined stream directly - full type safety!
|
|
218
|
+
const { parts, error } = useRealtimeStream(aiStream, runId, {
|
|
219
|
+
accessToken,
|
|
220
|
+
timeoutInSeconds: 600,
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
if (error) return <div>Error: {error.message}</div>;
|
|
224
|
+
if (!parts) return <div>Loading...</div>;
|
|
225
|
+
|
|
226
|
+
return (
|
|
227
|
+
<div>
|
|
228
|
+
{parts.map((part, i) => (
|
|
229
|
+
<span key={i}>{part}</span>
|
|
230
|
+
))}
|
|
231
|
+
</div>
|
|
232
|
+
);
|
|
233
|
+
}
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
## Direct Stream Methods (Without Defining)
|
|
237
|
+
|
|
238
|
+
<Warning>
|
|
239
|
+
We strongly recommend using `streams.define()` instead of direct methods. Defined streams provide
|
|
240
|
+
better organization, full type safety, and make it easier to maintain your codebase as it grows.
|
|
241
|
+
</Warning>
|
|
242
|
+
|
|
243
|
+
If you have a specific reason to avoid defined streams, you can use stream methods directly by specifying the stream key each time.
|
|
244
|
+
|
|
245
|
+
### Direct Piping
|
|
246
|
+
|
|
247
|
+
```ts
|
|
248
|
+
import { streams, task } from "@trigger.dev/sdk";
|
|
249
|
+
|
|
250
|
+
export const directStreamTask = task({
|
|
251
|
+
id: "direct-stream",
|
|
252
|
+
run: async (payload: { prompt: string }) => {
|
|
253
|
+
const stream = await getAIStream(payload.prompt);
|
|
254
|
+
|
|
255
|
+
// Specify the stream key directly
|
|
256
|
+
const { stream: readableStream, waitUntilComplete } = streams.pipe("ai-output", stream);
|
|
257
|
+
|
|
258
|
+
await waitUntilComplete();
|
|
259
|
+
},
|
|
260
|
+
});
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
### Direct Reading
|
|
264
|
+
|
|
265
|
+
```ts
|
|
266
|
+
import { streams } from "@trigger.dev/sdk";
|
|
267
|
+
|
|
268
|
+
// Specify the stream key when reading
|
|
269
|
+
const stream = await streams.read(runId, "ai-output");
|
|
270
|
+
|
|
271
|
+
for await (const chunk of stream) {
|
|
272
|
+
console.log(chunk);
|
|
273
|
+
}
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
### Direct Appending
|
|
277
|
+
|
|
278
|
+
```ts
|
|
279
|
+
import { streams, task } from "@trigger.dev/sdk";
|
|
280
|
+
|
|
281
|
+
export const directAppendTask = task({
|
|
282
|
+
id: "direct-append",
|
|
283
|
+
run: async (payload) => {
|
|
284
|
+
// Specify the stream key each time
|
|
285
|
+
await streams.append("logs", "Processing started");
|
|
286
|
+
await streams.append("progress", "50%");
|
|
287
|
+
await streams.append("logs", "Complete");
|
|
288
|
+
},
|
|
289
|
+
});
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
### Direct Writing
|
|
293
|
+
|
|
294
|
+
```ts
|
|
295
|
+
import { streams, task } from "@trigger.dev/sdk";
|
|
296
|
+
|
|
297
|
+
export const directWriterTask = task({
|
|
298
|
+
id: "direct-writer",
|
|
299
|
+
run: async (payload) => {
|
|
300
|
+
const { waitUntilComplete } = streams.writer("output", {
|
|
301
|
+
execute: ({ write, merge }) => {
|
|
302
|
+
write("Chunk 1");
|
|
303
|
+
write("Chunk 2");
|
|
304
|
+
},
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
await waitUntilComplete();
|
|
308
|
+
},
|
|
309
|
+
});
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
## Default Stream
|
|
313
|
+
|
|
314
|
+
Every run has a "default" stream, allowing you to skip the stream key entirely. This is useful for simple cases where you only need one stream per run.
|
|
315
|
+
|
|
316
|
+
Using direct methods:
|
|
317
|
+
|
|
318
|
+
```ts
|
|
319
|
+
import { streams, task } from "@trigger.dev/sdk";
|
|
320
|
+
|
|
321
|
+
export const defaultStreamTask = task({
|
|
322
|
+
id: "default-stream",
|
|
323
|
+
run: async (payload) => {
|
|
324
|
+
const stream = getDataStream();
|
|
325
|
+
|
|
326
|
+
// No stream key needed - uses "default"
|
|
327
|
+
const { waitUntilComplete } = streams.pipe(stream);
|
|
328
|
+
|
|
329
|
+
await waitUntilComplete();
|
|
330
|
+
},
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
// Reading from the default stream
|
|
334
|
+
const readStream = await streams.read(runId);
|
|
335
|
+
```
|
|
336
|
+
|
|
337
|
+
## Targeting Different Runs
|
|
338
|
+
|
|
339
|
+
You can pipe streams to parent, root, or any other run using the `target` option. This works with both defined streams and direct methods.
|
|
340
|
+
|
|
341
|
+
### With Defined Streams
|
|
342
|
+
|
|
343
|
+
```ts
|
|
344
|
+
import { task } from "@trigger.dev/sdk";
|
|
345
|
+
import { logStream } from "./streams";
|
|
346
|
+
|
|
347
|
+
export const childTask = task({
|
|
348
|
+
id: "child-task",
|
|
349
|
+
run: async (payload, { ctx }) => {
|
|
350
|
+
const stream = getDataStream();
|
|
351
|
+
|
|
352
|
+
// Pipe to parent run
|
|
353
|
+
logStream.pipe(stream, { target: "parent" });
|
|
354
|
+
|
|
355
|
+
// Pipe to root run
|
|
356
|
+
logStream.pipe(stream, { target: "root" });
|
|
357
|
+
|
|
358
|
+
// Pipe to self (default behavior)
|
|
359
|
+
logStream.pipe(stream, { target: "self" });
|
|
360
|
+
|
|
361
|
+
// Pipe to a specific run ID
|
|
362
|
+
logStream.pipe(stream, { target: payload.otherRunId });
|
|
363
|
+
},
|
|
364
|
+
});
|
|
365
|
+
```
|
|
366
|
+
|
|
367
|
+
### With Direct Methods
|
|
368
|
+
|
|
369
|
+
```ts
|
|
370
|
+
import { streams, task } from "@trigger.dev/sdk";
|
|
371
|
+
|
|
372
|
+
export const childTask = task({
|
|
373
|
+
id: "child-task",
|
|
374
|
+
run: async (payload, { ctx }) => {
|
|
375
|
+
const stream = getDataStream();
|
|
376
|
+
|
|
377
|
+
// Pipe to parent run
|
|
378
|
+
streams.pipe("output", stream, { target: "parent" });
|
|
379
|
+
|
|
380
|
+
// Pipe to root run
|
|
381
|
+
streams.pipe("output", stream, { target: "root" });
|
|
382
|
+
|
|
383
|
+
// Pipe to a specific run ID
|
|
384
|
+
streams.pipe("output", stream, { target: payload.otherRunId });
|
|
385
|
+
},
|
|
386
|
+
});
|
|
387
|
+
```
|
|
388
|
+
|
|
389
|
+
## Streaming from Outside a Task
|
|
390
|
+
|
|
391
|
+
If you specify a `target` run ID, you can pipe streams from anywhere (like a Next.js API route):
|
|
392
|
+
|
|
393
|
+
```ts
|
|
394
|
+
import { streams } from "@trigger.dev/sdk";
|
|
395
|
+
import { openai } from "@ai-sdk/openai";
|
|
396
|
+
import { streamText } from "ai";
|
|
397
|
+
|
|
398
|
+
export async function POST(req: Request) {
|
|
399
|
+
const { messages, runId } = await req.json();
|
|
400
|
+
|
|
401
|
+
const result = streamText({
|
|
402
|
+
model: openai("gpt-4o"),
|
|
403
|
+
messages,
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
// Pipe AI stream to a Trigger.dev run
|
|
407
|
+
const { stream } = streams.pipe("ai-stream", result.toUIMessageStream(), {
|
|
408
|
+
target: runId,
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
return new Response(stream as any, {
|
|
412
|
+
headers: { "Content-Type": "text/event-stream" },
|
|
413
|
+
});
|
|
414
|
+
}
|
|
415
|
+
```
|
|
416
|
+
|
|
417
|
+
## React Hook
|
|
418
|
+
|
|
419
|
+
Use the `useRealtimeStream` hook to subscribe to streams in your React components.
|
|
420
|
+
|
|
421
|
+
### With Defined Streams (Recommended)
|
|
422
|
+
|
|
423
|
+
```tsx
|
|
424
|
+
"use client";
|
|
425
|
+
|
|
426
|
+
import { useRealtimeStream } from "@trigger.dev/react-hooks";
|
|
427
|
+
import { aiStream } from "@/app/streams";
|
|
428
|
+
|
|
429
|
+
export function StreamViewer({ accessToken, runId }: { accessToken: string; runId: string }) {
|
|
430
|
+
// Pass the defined stream directly for full type safety
|
|
431
|
+
const { parts, error } = useRealtimeStream(aiStream, runId, {
|
|
432
|
+
accessToken,
|
|
433
|
+
timeoutInSeconds: 600,
|
|
434
|
+
onData: (chunk) => {
|
|
435
|
+
console.log("New chunk:", chunk); // chunk is typed!
|
|
436
|
+
},
|
|
437
|
+
});
|
|
438
|
+
|
|
439
|
+
if (error) return <div>Error: {error.message}</div>;
|
|
440
|
+
if (!parts) return <div>Loading...</div>;
|
|
441
|
+
|
|
442
|
+
return (
|
|
443
|
+
<div>
|
|
444
|
+
{parts.map((part, i) => (
|
|
445
|
+
<span key={i}>{part}</span>
|
|
446
|
+
))}
|
|
447
|
+
</div>
|
|
448
|
+
);
|
|
449
|
+
}
|
|
450
|
+
```
|
|
451
|
+
|
|
452
|
+
### With Direct Stream Keys
|
|
453
|
+
|
|
454
|
+
If you prefer not to use defined streams, you can specify the stream key directly:
|
|
455
|
+
|
|
456
|
+
```tsx
|
|
457
|
+
"use client";
|
|
458
|
+
|
|
459
|
+
import { useRealtimeStream } from "@trigger.dev/react-hooks";
|
|
460
|
+
|
|
461
|
+
export function StreamViewer({ accessToken, runId }: { accessToken: string; runId: string }) {
|
|
462
|
+
const { parts, error } = useRealtimeStream<string>(runId, "ai-output", {
|
|
463
|
+
accessToken,
|
|
464
|
+
timeoutInSeconds: 600,
|
|
465
|
+
});
|
|
466
|
+
|
|
467
|
+
if (error) return <div>Error: {error.message}</div>;
|
|
468
|
+
if (!parts) return <div>Loading...</div>;
|
|
469
|
+
|
|
470
|
+
return (
|
|
471
|
+
<div>
|
|
472
|
+
{parts.map((part, i) => (
|
|
473
|
+
<span key={i}>{part}</span>
|
|
474
|
+
))}
|
|
475
|
+
</div>
|
|
476
|
+
);
|
|
477
|
+
}
|
|
478
|
+
```
|
|
479
|
+
|
|
480
|
+
### Using Default Stream
|
|
481
|
+
|
|
482
|
+
```tsx
|
|
483
|
+
// Omit stream key to use the default stream
|
|
484
|
+
const { parts, error } = useRealtimeStream<string>(runId, {
|
|
485
|
+
accessToken,
|
|
486
|
+
});
|
|
487
|
+
```
|
|
488
|
+
|
|
489
|
+
### Hook Options
|
|
490
|
+
|
|
491
|
+
```tsx
|
|
492
|
+
const { parts, error } = useRealtimeStream(streamDef, runId, {
|
|
493
|
+
accessToken: "pk_...", // Required: Public access token
|
|
494
|
+
baseURL: "https://api.trigger.dev", // Optional: Custom API URL
|
|
495
|
+
timeoutInSeconds: 60, // Optional: Timeout (default: 60)
|
|
496
|
+
startIndex: 0, // Optional: Start from specific chunk
|
|
497
|
+
throttleInMs: 16, // Optional: Throttle updates (default: 16ms)
|
|
498
|
+
onData: (chunk) => {}, // Optional: Callback for each chunk
|
|
499
|
+
});
|
|
500
|
+
```
|
|
501
|
+
|
|
502
|
+
## Input Streams
|
|
503
|
+
|
|
504
|
+
Input Streams let you send data **into** a running task from your backend or frontend. While output streams (above) send data out of tasks, input streams complete the loop — enabling bidirectional communication.
|
|
505
|
+
|
|
506
|
+
<Note>
|
|
507
|
+
Input Streams require SDK version **4.4.2 or later** and use the same streams infrastructure (v2 is the default). If you're on an older SDK, calling `.on()` or `.once()` will throw with instructions to enable v2 streams. See [Pre-4.1.0 streams (legacy)](#pre-410-streams-legacy) for the older metadata-based API.
|
|
508
|
+
</Note>
|
|
509
|
+
|
|
510
|
+
### Input Streams overview
|
|
511
|
+
|
|
512
|
+
Input Streams solve three common problems:
|
|
513
|
+
|
|
514
|
+
- **Cancelling AI streams mid-generation.** When you use AI SDK's `streamText` inside a task, the LLM keeps generating until it's done — even if the user clicked "Stop." With input streams, your frontend sends a cancel signal and the task aborts the LLM call immediately.
|
|
515
|
+
- **Human-in-the-loop workflows.** A task generates a draft, then pauses and waits for the user to approve or edit it before continuing.
|
|
516
|
+
- **Interactive agents.** An AI agent running as a task needs follow-up information from the user mid-execution — clarifying a question, choosing between options, or providing additional context.
|
|
517
|
+
|
|
518
|
+
### Quick Start (Input Streams)
|
|
519
|
+
|
|
520
|
+
1. **Define** input streams in a shared file with `streams.input<T>({ id: "..." })`.
|
|
521
|
+
2. **Receive** in your task with `.wait()`, `.once()`, `.on()`, or `.peek()`.
|
|
522
|
+
3. **Send** from your backend with `.send(runId, data)` or from the frontend with the `useInputStreamSend` hook (see [Realtime React hooks](/realtime/react-hooks/streams#useinputstreamsend)).
|
|
523
|
+
|
|
524
|
+
### Defining Input Streams
|
|
525
|
+
|
|
526
|
+
Use `streams.input()` to define a typed input stream. The generic parameter controls the shape of data that can be sent:
|
|
527
|
+
|
|
528
|
+
```ts
|
|
529
|
+
import { streams } from "@trigger.dev/sdk";
|
|
530
|
+
|
|
531
|
+
export const cancelSignal = streams.input<{ reason?: string }>({
|
|
532
|
+
id: "cancel",
|
|
533
|
+
});
|
|
534
|
+
|
|
535
|
+
export const approval = streams.input<{ approved: boolean; reviewer: string }>({
|
|
536
|
+
id: "approval",
|
|
537
|
+
});
|
|
538
|
+
|
|
539
|
+
export const userResponse = streams.input<{
|
|
540
|
+
action: "approve" | "reject" | "edit";
|
|
541
|
+
message?: string;
|
|
542
|
+
edits?: Record<string, string>;
|
|
543
|
+
}>({
|
|
544
|
+
id: "user-response",
|
|
545
|
+
});
|
|
546
|
+
```
|
|
547
|
+
|
|
548
|
+
Type safety is enforced through the generic — both `.send()` and the receiving methods (`.wait()`, `.once()`, `.on()`, `.peek()`) share the same type.
|
|
549
|
+
|
|
550
|
+
### Receiving data inside a task
|
|
551
|
+
|
|
552
|
+
| Method | Task suspended? | Compute cost while waiting | Best for |
|
|
553
|
+
|--------|-----------------|----------------------------|-----------|
|
|
554
|
+
| `.wait()` | **Yes** | **None** — process freed | Approval gates, human-in-the-loop, long waits |
|
|
555
|
+
| `.once()` | No | Full — process stays alive | Short waits, concurrent work; returns result object with `.unwrap()` |
|
|
556
|
+
| `.on(handler)` | No | Full — process stays alive | Continuous listening (cancel signals, live updates) |
|
|
557
|
+
| `.peek()` | No | None | Non-blocking check for latest buffered value |
|
|
558
|
+
|
|
559
|
+
#### `wait()` — Suspend until data arrives
|
|
560
|
+
|
|
561
|
+
Suspends the task entirely, freeing compute resources. The task resumes when data arrives via `.send()`. Returns a [`ManualWaitpointPromise`](/wait-for-token) — the same type as `wait.forToken()`.
|
|
562
|
+
|
|
563
|
+
```ts
|
|
564
|
+
import { task } from "@trigger.dev/sdk";
|
|
565
|
+
import { approval } from "./streams";
|
|
566
|
+
|
|
567
|
+
export const publishPost = task({
|
|
568
|
+
id: "publish-post",
|
|
569
|
+
run: async (payload: { postId: string }) => {
|
|
570
|
+
const draft = await prepareDraft(payload.postId);
|
|
571
|
+
await notifyReviewer(draft);
|
|
572
|
+
|
|
573
|
+
const result = await approval.wait({ timeout: "7d" });
|
|
574
|
+
|
|
575
|
+
if (result.ok) {
|
|
576
|
+
if (result.output.approved) {
|
|
577
|
+
await publish(draft);
|
|
578
|
+
return { published: true, reviewer: result.output.reviewer };
|
|
579
|
+
}
|
|
580
|
+
return { published: false, reviewer: result.output.reviewer };
|
|
581
|
+
}
|
|
582
|
+
return { published: false, timedOut: true };
|
|
583
|
+
},
|
|
584
|
+
});
|
|
585
|
+
```
|
|
586
|
+
|
|
587
|
+
Use `.unwrap()` to throw on timeout: `const data = await approval.wait({ timeout: "24h" }).unwrap();`
|
|
588
|
+
|
|
589
|
+
**Options:** `timeout` (e.g. `"30s"`, `"5m"`, `"24h"`, `"7d"`), `idempotencyKey`, `idempotencyKeyTTL`, `tags`. Use `idempotencyKey` when your task has retries so the same waitpoint is resumed across retries.
|
|
590
|
+
|
|
591
|
+
#### `once()` — Wait for the next value (non-suspending)
|
|
592
|
+
|
|
593
|
+
Blocks until data arrives but keeps the task process alive. Returns a result object; use `.unwrap()` to get the data or throw on timeout.
|
|
594
|
+
|
|
595
|
+
```ts
|
|
596
|
+
const result = await approval.once({ timeoutMs: 300_000 });
|
|
597
|
+
if (result.ok) {
|
|
598
|
+
console.log(result.output.approved);
|
|
599
|
+
}
|
|
600
|
+
// Or: const data = await approval.once({ timeoutMs: 300_000 }).unwrap();
|
|
601
|
+
```
|
|
602
|
+
|
|
603
|
+
`once()` also accepts a `signal` (e.g. `AbortController.signal`) for cancellation.
|
|
604
|
+
|
|
605
|
+
#### `on()` — Listen for every value
|
|
606
|
+
|
|
607
|
+
Registers a persistent handler that fires on every piece of data. Handlers are automatically cleaned up when the task run completes. Call `.off()` on the returned subscription to stop listening early.
|
|
608
|
+
|
|
609
|
+
```ts
|
|
610
|
+
const controller = new AbortController();
|
|
611
|
+
cancelSignal.on((data) => {
|
|
612
|
+
console.log("Cancelled:", data.reason);
|
|
613
|
+
controller.abort();
|
|
614
|
+
});
|
|
615
|
+
const result = streamText({ ..., abortSignal: controller.signal });
|
|
616
|
+
```
|
|
617
|
+
|
|
618
|
+
#### `peek()` — Non-blocking check
|
|
619
|
+
|
|
620
|
+
Returns the most recent buffered value without waiting, or `undefined` if nothing has been received yet.
|
|
621
|
+
|
|
622
|
+
```ts
|
|
623
|
+
const latest = cancelSignal.peek();
|
|
624
|
+
if (latest) {
|
|
625
|
+
// A cancel was already sent before we checked
|
|
626
|
+
}
|
|
627
|
+
```
|
|
628
|
+
|
|
629
|
+
### Sending data to a running task
|
|
630
|
+
|
|
631
|
+
Use `.send(runId, data)` from your backend to push data into a running task. See the [backend input streams guide](/realtime/backend/input-streams) for API route patterns.
|
|
632
|
+
|
|
633
|
+
```ts
|
|
634
|
+
import { cancelSignal, approval } from "./trigger/streams";
|
|
635
|
+
|
|
636
|
+
await cancelSignal.send(runId, { reason: "User clicked stop" });
|
|
637
|
+
await approval.send(runId, { approved: true, reviewer: "alice@example.com" });
|
|
638
|
+
```
|
|
639
|
+
|
|
640
|
+
### Complete example: Cancellable AI streaming
|
|
641
|
+
|
|
642
|
+
Stream an AI response while allowing the user to cancel mid-generation.
|
|
643
|
+
|
|
644
|
+
**Define the streams:**
|
|
645
|
+
|
|
646
|
+
```ts
|
|
647
|
+
import { streams } from "@trigger.dev/sdk";
|
|
648
|
+
|
|
649
|
+
export const aiOutput = streams.define<string>({ id: "ai" });
|
|
650
|
+
export const cancelStream = streams.input<{ reason?: string }>({ id: "cancel" });
|
|
651
|
+
```
|
|
652
|
+
|
|
653
|
+
**Task:** Register `cancelStream.on()` to abort an `AbortController`, then pipe `streamText(...).textStream` to `aiOutput`. **Backend:** POST to an API route that calls `cancelStream.send(runId, { reason: "User clicked stop" })`. **Frontend:** Use `useRealtimeStream(aiOutput, runId, { accessToken })` and a button that calls your cancel API (or use the `useInputStreamSend` hook; see [Realtime React hooks](/realtime/react-hooks/streams#useinputstreamsend)).
|
|
654
|
+
|
|
655
|
+
**Important notes (input streams):** You cannot send to a completed, failed, or canceled run. Max payload per `.send()` is 1MB. Data sent before a listener is registered is buffered and delivered when a listener attaches; `.wait()` handles the buffering race automatically. Use `.wait()` for long waits to free compute; use `.once()` for short waits or concurrent work. Define input streams in a shared location and combine with output streams for full bidirectional communication.
|
|
656
|
+
|
|
657
|
+
## Complete Example: AI Streaming
|
|
658
|
+
|
|
659
|
+
### Define the stream
|
|
660
|
+
|
|
661
|
+
```ts
|
|
662
|
+
// app/streams.ts
|
|
663
|
+
import { streams, InferStreamType } from "@trigger.dev/sdk";
|
|
664
|
+
import { UIMessageChunk } from "ai";
|
|
665
|
+
|
|
666
|
+
export const aiStream = streams.define<UIMessageChunk>({
|
|
667
|
+
id: "ai",
|
|
668
|
+
});
|
|
669
|
+
|
|
670
|
+
export type AIStreamPart = InferStreamType<typeof aiStream>;
|
|
671
|
+
```
|
|
672
|
+
|
|
673
|
+
### Create the task
|
|
674
|
+
|
|
675
|
+
```ts
|
|
676
|
+
// trigger/ai-task.ts
|
|
677
|
+
import { task } from "@trigger.dev/sdk";
|
|
678
|
+
import { openai } from "@ai-sdk/openai";
|
|
679
|
+
import { streamText } from "ai";
|
|
680
|
+
import { aiStream } from "@/app/streams";
|
|
681
|
+
|
|
682
|
+
export const generateAI = task({
|
|
683
|
+
id: "generate-ai",
|
|
684
|
+
run: async (payload: { prompt: string }) => {
|
|
685
|
+
const result = streamText({
|
|
686
|
+
model: openai("gpt-4o"),
|
|
687
|
+
prompt: payload.prompt,
|
|
688
|
+
});
|
|
689
|
+
|
|
690
|
+
const { waitUntilComplete } = aiStream.pipe(result.toUIMessageStream());
|
|
691
|
+
|
|
692
|
+
await waitUntilComplete();
|
|
693
|
+
|
|
694
|
+
return { success: true };
|
|
695
|
+
},
|
|
696
|
+
});
|
|
697
|
+
```
|
|
698
|
+
|
|
699
|
+
### Frontend component
|
|
700
|
+
|
|
701
|
+
```tsx
|
|
702
|
+
// components/ai-stream.tsx
|
|
703
|
+
"use client";
|
|
704
|
+
|
|
705
|
+
import { useRealtimeStream } from "@trigger.dev/react-hooks";
|
|
706
|
+
import { aiStream } from "@/app/streams";
|
|
707
|
+
|
|
708
|
+
export function AIStream({ accessToken, runId }: { accessToken: string; runId: string }) {
|
|
709
|
+
const { parts, error } = useRealtimeStream(aiStream, runId, {
|
|
710
|
+
accessToken,
|
|
711
|
+
timeoutInSeconds: 300,
|
|
712
|
+
});
|
|
713
|
+
|
|
714
|
+
if (error) return <div>Error: {error.message}</div>;
|
|
715
|
+
if (!parts) return <div>Loading...</div>;
|
|
716
|
+
|
|
717
|
+
return (
|
|
718
|
+
<div className="prose">
|
|
719
|
+
{parts.map((part, i) => (
|
|
720
|
+
<span key={i}>{part}</span>
|
|
721
|
+
))}
|
|
722
|
+
</div>
|
|
723
|
+
);
|
|
724
|
+
}
|
|
725
|
+
```
|
|
726
|
+
|
|
727
|
+
## Migration from v1
|
|
728
|
+
|
|
729
|
+
If you're using the old `metadata.stream()` API, here's how to migrate to the recommended v2 approach:
|
|
730
|
+
|
|
731
|
+
### Step 1: Define Your Streams
|
|
732
|
+
|
|
733
|
+
Create a shared streams definition file:
|
|
734
|
+
|
|
735
|
+
```ts
|
|
736
|
+
// app/streams.ts or trigger/streams.ts
|
|
737
|
+
import { streams, InferStreamType } from "@trigger.dev/sdk";
|
|
738
|
+
|
|
739
|
+
export const myStream = streams.define<string>({
|
|
740
|
+
id: "my-stream",
|
|
741
|
+
});
|
|
742
|
+
|
|
743
|
+
export type MyStreamPart = InferStreamType<typeof myStream>;
|
|
744
|
+
```
|
|
745
|
+
|
|
746
|
+
### Step 2: Update Your Tasks
|
|
747
|
+
|
|
748
|
+
Replace `metadata.stream()` with the defined stream's `pipe()` method:
|
|
749
|
+
|
|
750
|
+
```ts
|
|
751
|
+
// Before (v1)
|
|
752
|
+
import { metadata, task } from "@trigger.dev/sdk";
|
|
753
|
+
|
|
754
|
+
export const myTask = task({
|
|
755
|
+
id: "my-task",
|
|
756
|
+
run: async (payload) => {
|
|
757
|
+
const stream = getDataStream();
|
|
758
|
+
await metadata.stream("my-stream", stream);
|
|
759
|
+
},
|
|
760
|
+
});
|
|
761
|
+
```
|
|
762
|
+
|
|
763
|
+
```ts
|
|
764
|
+
// After (v2 - Recommended)
|
|
765
|
+
import { task } from "@trigger.dev/sdk";
|
|
766
|
+
import { myStream } from "./streams";
|
|
767
|
+
|
|
768
|
+
export const myTask = task({
|
|
769
|
+
id: "my-task",
|
|
770
|
+
run: async (payload) => {
|
|
771
|
+
const stream = getDataStream();
|
|
772
|
+
|
|
773
|
+
// Don't await - returns immediately
|
|
774
|
+
const { waitUntilComplete } = myStream.pipe(stream);
|
|
775
|
+
|
|
776
|
+
// Optionally wait for completion
|
|
777
|
+
await waitUntilComplete();
|
|
778
|
+
},
|
|
779
|
+
});
|
|
780
|
+
```
|
|
781
|
+
|
|
782
|
+
### Step 3: Update Your Frontend
|
|
783
|
+
|
|
784
|
+
Use the defined stream with `useRealtimeStream`:
|
|
785
|
+
|
|
786
|
+
```tsx
|
|
787
|
+
// Before
|
|
788
|
+
const { parts, error } = useRealtimeStream<string>(runId, "my-stream", {
|
|
789
|
+
accessToken,
|
|
790
|
+
});
|
|
791
|
+
```
|
|
792
|
+
|
|
793
|
+
```tsx
|
|
794
|
+
// After
|
|
795
|
+
import { myStream } from "@/app/streams";
|
|
796
|
+
|
|
797
|
+
const { parts, error } = useRealtimeStream(myStream, runId, {
|
|
798
|
+
accessToken,
|
|
799
|
+
});
|
|
800
|
+
```
|
|
801
|
+
|
|
802
|
+
### Alternative: Direct Methods (Not Recommended)
|
|
803
|
+
|
|
804
|
+
If you prefer not to use defined streams, you can use direct methods:
|
|
805
|
+
|
|
806
|
+
```ts
|
|
807
|
+
import { streams, task } from "@trigger.dev/sdk";
|
|
808
|
+
|
|
809
|
+
export const myTask = task({
|
|
810
|
+
id: "my-task",
|
|
811
|
+
run: async (payload) => {
|
|
812
|
+
const stream = getDataStream();
|
|
813
|
+
const { waitUntilComplete } = streams.pipe("my-stream", stream);
|
|
814
|
+
await waitUntilComplete();
|
|
815
|
+
},
|
|
816
|
+
});
|
|
817
|
+
```
|
|
818
|
+
|
|
819
|
+
## Reliability Features
|
|
820
|
+
|
|
821
|
+
Streams v2 includes automatic reliability improvements:
|
|
822
|
+
|
|
823
|
+
- **Automatic resumption**: If a connection is lost, both appending and reading will automatically resume from the last successful chunk
|
|
824
|
+
- **No data loss**: Network issues won't cause stream data to be lost
|
|
825
|
+
- **Idempotent operations**: Duplicate chunks are automatically handled
|
|
826
|
+
|
|
827
|
+
These improvements happen automatically - no code changes needed.
|
|
828
|
+
|
|
829
|
+
## Dashboard Integration
|
|
830
|
+
|
|
831
|
+
Streams are now visible in the Trigger.dev dashboard, allowing you to:
|
|
832
|
+
|
|
833
|
+
- View stream data in real-time as it's generated
|
|
834
|
+
- Inspect historical stream data for completed runs
|
|
835
|
+
- Debug streaming issues with full visibility into chunk delivery
|
|
836
|
+
|
|
837
|
+
<video src="https://content.trigger.dev/streams-v2-dashboard.mp4" controls muted autoPlay loop />
|
|
838
|
+
|
|
839
|
+
## Best Practices
|
|
840
|
+
|
|
841
|
+
1. **Always use `streams.define()`**: Define your streams in a shared location for better organization, type safety, and code reusability. This is the recommended approach for all streams.
|
|
842
|
+
2. **Export stream types**: Use `InferStreamType` to export types for your frontend components
|
|
843
|
+
3. **Handle errors gracefully**: Always check for errors when reading streams in your UI
|
|
844
|
+
4. **Set appropriate timeouts**: Adjust `timeoutInSeconds` based on your use case (AI completions may need longer timeouts)
|
|
845
|
+
5. **Target parent runs**: When orchestrating with child tasks, pipe to parent runs for easier consumption
|
|
846
|
+
6. **Throttle frontend updates**: Use `throttleInMs` in `useRealtimeStream` to prevent excessive re-renders
|
|
847
|
+
7. **Use descriptive stream IDs**: Choose clear, descriptive IDs like `"ai-output"` or `"progress"` instead of generic names
|
|
848
|
+
|
|
849
|
+
## Pre-4.1.0 streams (legacy)
|
|
850
|
+
|
|
851
|
+
Prior to SDK 4.1.0, streams used the older metadata-based API. If you're on an earlier version, see [metadata.stream()](/runs/metadata#stream) for legacy usage. With 4.4.2+, [Input Streams](#input-streams) are available and documented in this page.
|
|
852
|
+
|
|
853
|
+
## Troubleshooting
|
|
854
|
+
|
|
855
|
+
### Stream not appearing in dashboard
|
|
856
|
+
|
|
857
|
+
- Verify your task is actually writing to the stream
|
|
858
|
+
- Check that the stream key matches between writing and reading
|
|
859
|
+
|
|
860
|
+
### Stream timeout errors
|
|
861
|
+
|
|
862
|
+
- Increase `timeoutInSeconds` in your `read()` or `useRealtimeStream()` calls
|
|
863
|
+
- Ensure your stream source is actively producing data
|
|
864
|
+
- Check network connectivity between your application and Trigger.dev
|
|
865
|
+
|
|
866
|
+
### Missing chunks
|
|
867
|
+
|
|
868
|
+
- With the current streams implementation, chunks should not be lost due to automatic resumption
|
|
869
|
+
- Verify you're reading from the correct stream key
|
|
870
|
+
- Check the `startIndex` option if you're not seeing expected chunks
|
|
871
|
+
|
|
872
|
+
### Input streams not working
|
|
873
|
+
|
|
874
|
+
- Input streams require SDK **4.4.2 or later** and the default streams (v2) infrastructure. Ensure you're on a recent SDK and not using the legacy metadata.stream() API.
|
|
875
|
+
- If `.on()` or `.once()` throw, follow the error message to enable v2 streams (they are default in 4.1.0+).
|
|
876
|
+
|
|
877
|
+
### "Stream is being deleted" during long waits
|
|
878
|
+
|
|
879
|
+
If a stream is created but stays empty for ~1 hour (for example, during a long `wait.forToken()` or `wait.for()`), the streams backend may garbage-collect it. When the run resumes and tries to use the stream, you'll see `S2Error: Stream is being deleted` and the task retries from the beginning.
|
|
880
|
+
|
|
881
|
+
Two ways to avoid this:
|
|
882
|
+
|
|
883
|
+
- Close the stream before the wait and open a new one when the run resumes.
|
|
884
|
+
- Write a heartbeat record to the stream every 20–30 minutes during the wait so it's never empty long enough to be deleted.
|