-
-
Notifications
You must be signed in to change notification settings - Fork 1.1k
feat: Input Streams - Bidirectional task communication #3146
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
ericallam
wants to merge
27
commits into
main
Choose a base branch
from
feature/tri-7538-input-streams-bidirectional-communication-with-running-tasks
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+2,875
−155
Open
Changes from 26 commits
Commits
Show all changes
27 commits
Select commit
Hold shift + click to select a range
1fe1e02
feat: add streams.input() for inbound realtime data to running tasks
claude 05139d0
refactor: connect input stream tail lazily from listener side
claude 2cbdb9c
fix: gate input stream tail connection on v2 realtime streams
claude 66f7d3d
fix: throw error when input streams are used without v2 realtime streams
claude e17bf77
docs: add input streams documentation to realtime guides
claude 19c77f4
docs: add API/SDK design proposal for temporal signals versioning
claude ec26e2c
chore: remove temporal signals versioning design doc
claude f9273e1
docs: add SDK design proposal for input stream .wait() method
claude 04a1d6f
input stream waitpoints and tests in the hello world reference project
ericallam 6a83fa4
No more input stream multiplexing, no naming collision risk, removed …
ericallam 024426f
Better span experience, show input streams on dashboard, cleanup the …
ericallam 3776d2f
adopt s2-lite, upgrade s2 package with support
ericallam 732ead3
add input streams example
ericallam a5d104c
better changesets
ericallam 0c9db46
Upgrade the deployment s2 client to 0.22.5 as well and fix some typec…
ericallam d768007
fix: address CodeRabbit review feedback on input streams PR
ericallam a08f44f
fix typecheck issue
ericallam 4f379f6
Add result type and .unwrap() helper to inputStream.once().
ericallam 29db992
small jsdocs tweak
ericallam bcbf874
Reconnect input stream tail after SSE timeout
ericallam 4846fe0
automatically cleanup input stream on handlers and tail SSE requests …
ericallam 6d1a29f
remove realtime rules update, we'll do this in a separate PR
ericallam e3cbe7d
undo changes to local claude realtime skill, we'll do that in another PR
ericallam 1ba5cd7
delete design doc
ericallam 480f967
Add mock agentRelay example
ericallam eb70542
remove unused function
ericallam 9d9970d
prevent duplicate messages by reconnecting and passing in the last se…
ericallam File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,8 @@ | ||
| --- | ||
| "@trigger.dev/sdk": patch | ||
| "@trigger.dev/react-hooks": patch | ||
| --- | ||
|
|
||
| Add input streams for bidirectional communication with running tasks. Define typed input streams with `streams.input<T>({ id })`, then consume inside tasks via `.wait()` (suspends the process), `.once()` (waits for next message), or `.on()` (subscribes to a continuous stream). Send data from backends with `.send(runId, data)` or from frontends with the new `useInputStreamSend` React hook. | ||
|
|
||
| Upgrade S2 SDK from 0.17 to 0.22 with support for custom endpoints (s2-lite) via the new `endpoints` configuration, `AppendRecord.string()` API, and `maxInflightBytes` session option. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| --- | ||
| area: webapp | ||
| type: feature | ||
| --- | ||
|
|
||
| Add input streams with API routes for sending data to running tasks, SSE reading, and waitpoint creation. Includes Redis cache for fast `.send()` to `.wait()` bridging, dashboard span support for input stream operations, and s2-lite support with configurable S2 endpoint, access token skipping, and S2-Basin headers for self-hosted deployments. Adds s2-lite to Docker Compose for local development. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
148 changes: 148 additions & 0 deletions
148
apps/webapp/app/routes/api.v1.runs.$runFriendlyId.input-streams.wait.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,148 @@ | ||
| import { json } from "@remix-run/server-runtime"; | ||
| import { z } from "zod"; | ||
| import { | ||
| CreateInputStreamWaitpointRequestBody, | ||
| type CreateInputStreamWaitpointResponseBody, | ||
| } from "@trigger.dev/core/v3"; | ||
| import { WaitpointId } from "@trigger.dev/core/v3/isomorphic"; | ||
| import { $replica } from "~/db.server"; | ||
| import { createWaitpointTag, MAX_TAGS_PER_WAITPOINT } from "~/models/waitpointTag.server"; | ||
| import { | ||
| deleteInputStreamWaitpoint, | ||
| setInputStreamWaitpoint, | ||
| } from "~/services/inputStreamWaitpointCache.server"; | ||
| import { getRealtimeStreamInstance } from "~/services/realtime/v1StreamsGlobal.server"; | ||
| import { createActionApiRoute } from "~/services/routeBuilders/apiBuilder.server"; | ||
| import { parseDelay } from "~/utils/delays"; | ||
| import { resolveIdempotencyKeyTTL } from "~/utils/idempotencyKeys.server"; | ||
| import { engine } from "~/v3/runEngine.server"; | ||
| import { ServiceValidationError } from "~/v3/services/baseService.server"; | ||
|
|
||
| const ParamsSchema = z.object({ | ||
| runFriendlyId: z.string(), | ||
| }); | ||
|
|
||
| const { action, loader } = createActionApiRoute( | ||
| { | ||
| params: ParamsSchema, | ||
| body: CreateInputStreamWaitpointRequestBody, | ||
| maxContentLength: 1024 * 10, // 10KB | ||
| method: "POST", | ||
| }, | ||
| async ({ authentication, body, params }) => { | ||
| try { | ||
| const run = await $replica.taskRun.findFirst({ | ||
| where: { | ||
| friendlyId: params.runFriendlyId, | ||
| runtimeEnvironmentId: authentication.environment.id, | ||
| }, | ||
| select: { | ||
| id: true, | ||
| friendlyId: true, | ||
| realtimeStreamsVersion: true, | ||
| }, | ||
| }); | ||
|
|
||
| if (!run) { | ||
| return json({ error: "Run not found" }, { status: 404 }); | ||
| } | ||
|
|
||
| const idempotencyKeyExpiresAt = body.idempotencyKeyTTL | ||
| ? resolveIdempotencyKeyTTL(body.idempotencyKeyTTL) | ||
| : undefined; | ||
|
|
||
| const timeout = await parseDelay(body.timeout); | ||
|
|
||
| // Process tags (same pattern as api.v1.waitpoints.tokens.ts) | ||
| const bodyTags = typeof body.tags === "string" ? [body.tags] : body.tags; | ||
|
|
||
| if (bodyTags && bodyTags.length > MAX_TAGS_PER_WAITPOINT) { | ||
| throw new ServiceValidationError( | ||
| `Waitpoints can only have ${MAX_TAGS_PER_WAITPOINT} tags, you're trying to set ${bodyTags.length}.` | ||
| ); | ||
| } | ||
|
|
||
| if (bodyTags && bodyTags.length > 0) { | ||
| for (const tag of bodyTags) { | ||
| await createWaitpointTag({ | ||
| tag, | ||
| environmentId: authentication.environment.id, | ||
| projectId: authentication.environment.projectId, | ||
| }); | ||
| } | ||
| } | ||
|
|
||
| // Step 1: Create the waitpoint | ||
| const result = await engine.createManualWaitpoint({ | ||
| environmentId: authentication.environment.id, | ||
| projectId: authentication.environment.projectId, | ||
| idempotencyKey: body.idempotencyKey, | ||
| idempotencyKeyExpiresAt, | ||
| timeout, | ||
| tags: bodyTags, | ||
| }); | ||
|
|
||
| // Step 2: Cache the mapping in Redis for fast lookup from .send() | ||
| const ttlMs = timeout ? timeout.getTime() - Date.now() : undefined; | ||
| await setInputStreamWaitpoint( | ||
| run.friendlyId, | ||
| body.streamId, | ||
| result.waitpoint.id, | ||
| ttlMs && ttlMs > 0 ? ttlMs : undefined | ||
| ); | ||
|
|
||
| // Step 3: Check if data was already sent to this input stream (race condition handling). | ||
| // If .send() landed before .wait(), the data is in the S2 stream but no waitpoint | ||
| // existed to complete. We check from the client's last known position. | ||
| if (!result.isCached) { | ||
| try { | ||
| const realtimeStream = getRealtimeStreamInstance( | ||
| authentication.environment, | ||
| run.realtimeStreamsVersion | ||
| ); | ||
|
|
||
| const records = await realtimeStream.readRecords( | ||
| run.friendlyId, | ||
| `$trigger.input:${body.streamId}`, | ||
| body.lastSeqNum | ||
| ); | ||
|
|
||
| if (records.length > 0) { | ||
| const record = records[0]!; | ||
|
|
||
| // Record data is the raw user payload — no wrapper to unwrap | ||
| await engine.completeWaitpoint({ | ||
| id: result.waitpoint.id, | ||
| output: { | ||
| value: record.data, | ||
| type: "application/json", | ||
| isError: false, | ||
| }, | ||
| }); | ||
|
|
||
| // Clean up the Redis cache since we completed it ourselves | ||
| await deleteInputStreamWaitpoint(run.friendlyId, body.streamId); | ||
| } | ||
| } catch { | ||
| // Non-fatal: if the S2 check fails, the waitpoint is still PENDING. | ||
| // The next .send() will complete it via the Redis cache path. | ||
| } | ||
| } | ||
|
|
||
| return json<CreateInputStreamWaitpointResponseBody>({ | ||
| waitpointId: WaitpointId.toFriendlyId(result.waitpoint.id), | ||
| isCached: result.isCached, | ||
| }); | ||
| } catch (error) { | ||
| if (error instanceof ServiceValidationError) { | ||
| return json({ error: error.message }, { status: 422 }); | ||
| } else if (error instanceof Error) { | ||
| return json({ error: error.message }, { status: 500 }); | ||
| } | ||
|
|
||
| return json({ error: "Something went wrong" }, { status: 500 }); | ||
| } | ||
| } | ||
| ); | ||
|
|
||
| export { action, loader }; | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.