- Add onPreload hook and preloaded field to all lifecycle events
- Add transport.preload(chatId) for eagerly starting runs before first message
- Add preloadWarmTimeoutInSeconds and preloadTimeout task options
- Add preload:true run tag and chat.preloaded span attributes
- Add UserTool model for per-user dynamic tools loaded from DB
- Load dynamic tools in onPreload/onChatStart via chat.local
- Build dynamicTool() instances in run and spread into streamText tools
- Reference project: preload on new chat, dynamic company-info and user-preferences tools
- Store chat turn context (chatId, turn, continuation, clientData) in locals for auto-detection
- toolFromTask now auto-detects chat context and passes it to subtask metadata
- Skip serializing messages array (can be large, rarely needed by subtasks)
- Tag subtask runs with toolCallId for dashboard visibility
- Add ai.toolCallId() convenience helper
- Add ai.chatContext<typeof myChat>() with typed clientData inference
- Add ai.chatContextOrThrow<typeof myChat>() that throws if not in a chat context
- Update deepResearch example to use ai.chatContextOrThrow
- Document all helpers in ai-chat guide
- Export chat.stream (typed RealtimeDefinedStream<UIMessageChunk>) for writing custom data to the chat stream
- Add deepResearch subtask using data-* chunks to stream progress back to parent chat via target: root
- Use AI SDK data-research-progress chunk protocol with id-based updates for live progress
- Add ResearchProgress component and generic data-* fallback renderer in frontend
- Persist model per chat in DB (schema + onChatStart), model selector only on new chats
- Add collapsible debug panel showing run ID (with dashboard link), chat ID, model, status, session info
- Document chat.stream API, data-* chunks, and subtask streaming pattern in docs
- Fix onFinish race condition: await onFinishPromise so capturedResponseMessage is set before accumulation
- Add chat.isStopped() helper accessible from anywhere during a turn
- Add chat.cleanupAbortedParts() to remove incomplete tool/reasoning/text parts on stop
- Auto-cleanup aborted parts before passing to onTurnComplete
- Clean incoming messages from frontend to prevent tool_use without tool_result API errors
- Add stopped and rawResponseMessage fields to TurnCompleteEvent
- Add continuation and previousRunId fields to all lifecycle hooks and run payload
- Add span attributes (chat.id, chat.turn, chat.stopped, chat.continuation, chat.previous_run_id, etc.)
- Add webFetch tool and reasoning model support to ai-chat reference project
- Render reasoning parts in frontend chat component
- Document all new fields in ai-chat guide
@ai-sdk/openai v3 and @ai-sdk/react v3 are needed for ai v6 compatibility.
convertToModelMessages is async in newer AI SDK versions.
Co-authored-by: Eric Allam <eric@trigger.dev>
1. Add null/object guard before enqueuing UIMessageChunk from SSE stream
to handle heartbeat or malformed events safely
2. Use incrementing counter instead of Date.now() in test message
factories to avoid duplicate IDs
3. Add test covering publicAccessToken from trigger response being used
for stream subscription auth
Co-authored-by: Eric Allam <eric@trigger.dev>
Move and adapt tests from packages/ai to packages/trigger-sdk.
- Import from ./chat.js instead of ./transport.js
- Use 'task' option instead of 'taskId'
- All 17 tests passing
Co-authored-by: Eric Allam <eric@trigger.dev>
Two new subpath exports:
@trigger.dev/sdk/chat (frontend, browser-safe):
- TriggerChatTransport — ChatTransport implementation for useChat
- createChatTransport() — factory function
- TriggerChatTransportOptions type
@trigger.dev/sdk/ai (backend, adds to existing ai.tool/ai.currentToolOptions):
- chatTask() — pre-typed task wrapper with auto-pipe
- pipeChat() — pipe StreamTextResult to realtime stream
- CHAT_STREAM_KEY constant
- ChatTaskPayload type
- ChatTaskOptions type
- PipeChatOptions type
Co-authored-by: Eric Allam <eric@trigger.dev>
Use the already-resolved token when creating ApiClient instead of
calling resolveAccessToken() again through getApiClient().
Co-authored-by: Eric Allam <eric@trigger.dev>
The accessToken option now accepts either a string or a function
returning a string. This enables dynamic token refresh patterns:
new TriggerChatTransport({
taskId: 'my-task',
accessToken: () => getLatestToken(),
})
The function is called on each sendMessages() call, allowing fresh
tokens to be used for each task trigger.
Co-authored-by: Eric Allam <eric@trigger.dev>
ChatSessionState is an implementation detail of the transport's
session tracking. Users don't need to access it since the sessions
map is private.
Co-authored-by: Eric Allam <eric@trigger.dev>
Adds 3 additional test cases:
- Abort signal gracefully closes the stream
- Multiple independent chat sessions tracked correctly
- ChatRequestOptions.body is merged into task payload
Co-authored-by: Eric Allam <eric@trigger.dev>
Tests cover:
- Constructor with required and optional options
- sendMessages triggering task and returning UIMessageChunk stream
- Correct payload structure sent to trigger API
- Custom streamKey in stream URL
- Extra headers propagation
- reconnectToStream with existing and non-existing sessions
- createChatTransport factory function
- Error handling for API failures
- regenerate-message trigger type
Co-authored-by: Eric Allam <eric@trigger.dev>
New package that provides a custom AI SDK ChatTransport implementation
bridging Vercel AI SDK's useChat hook with Trigger.dev's durable task
execution and realtime streams.
Key exports:
- TriggerChatTransport class implementing ChatTransport<UIMessage>
- createChatTransport() factory function
- ChatTaskPayload type for task-side typing
- TriggerChatTransportOptions type
The transport triggers a Trigger.dev task with chat messages as payload,
then subscribes to the task's realtime stream to receive UIMessageChunk
data, which useChat processes natively.
Co-authored-by: Eric Allam <eric@trigger.dev>
## Summary
- Run-view inspector panel was glitching out on Firefox: visual flicker
on close, locking up at min size, and intermittent `panelHasSpace`
invariant errors. Root cause is the underlying `react-window-splitter`
library's collapse animation, which uses `@react-spring/rafz` and
interacts poorly with Firefox.
- Disabled the library's collapse animation on Firefox only, app-wide
(every consumer of `RESIZABLE_PANEL_ANIMATION`). Chromium and Safari
behaviour is unchanged.
## Changes
- **Firefox animation skip** in `RESIZABLE_PANEL_ANIMATION` —
UA-detected at module load, resolves to `undefined` for Firefox so the
library's animation actor completes in one frame instead of running its
rAF loop.
- **Inspector min raised 50px → 250px** so dragging can't shrink the
panel into a near-useless width.
- **`autosaveId` bumped `v2` → `v3`** to invalidate stale persisted
snapshots (the library has a `// TODO` branch that ignores prop changes
for already-registered panels, so existing users would otherwise still
see the old 50px min).
- **`react-window-splitter` pinned** to exact `0.4.1` to protect the
patch from drifting if line offsets change in a patch release.
- **Two hunks added to the existing `@window-splitter/state` patch:**
- Removed the library's auto-collapse-on-drag block entirely. Every
collapsible panel in the app is parent-controlled, and that block was
triggering state-machine deadlocks when handlers were no-ops.
Drag-to-collapse is now disabled across the app; collapse is only
triggered explicitly (close button, ESC, URL change, etc.).
- In `getDeltaForEvent`, fall back to the panel's `default` before its
`min` when expanding — so the first ever click on a span opens the
inspector at 500px, not 250px.
## Local testing confirmed
- [x] Firefox: open a run, click various spans → panel opens instantly
at 500px, drags freely between 250px and max, closes instantly to 0. No
console errors.
- [x] Chrome/Chromium: same flow, but with smooth open/close animation
as before.
- [x] Safari: same as Chrome.
- [x] Reload mid-session → panel restores cleanly to the dragged size.
- [x] Other resizable panels in the app (logs, deployments, schedules,
batches, bulk-actions, runs index) still animate on Chromium/Safari.
## Notes
- Linear: TRI-8584
- Branch contains intermediate commits exploring an unsuccessful
snapshot-validator approach; they're reverted by the final commit.
Cumulative diff is 6 files. Squash on merge if you'd prefer a clean
history.
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
## Summary
Adds a Redis pub/sub reload path to the webapp's in-memory LLM pricing
registry. When enabled on a process, the registry reloads from the
database whenever a publish lands on the configured channel — instead of
waiting for the existing 5-minute interval. Lets pricing/model changes
propagate to cost enrichment within seconds.
Subscription is **off by default** and opt-in per process. Only
OTel-ingesting services need real-time freshness; dashboard and worker
services run fine on the periodic interval and shouldn't pile onto each
publish with a full-table reload.
## Design
When `LLM_PRICING_RELOAD_PUBSUB_ENABLED=true`, subscribes via
`createRedisClient` against `COMMON_WORKER_REDIS_*` and listens on
`LLM_PRICING_RELOAD_CHANNEL` (default `llm-registry:reload`). The
5-minute periodic reload stays as a backstop, and a SIGTERM/SIGINT
handler closes the subscription cleanly.
The publisher side lives outside this PR — any process running in the
same Redis namespace can trigger a reload by `PUBLISH
llm-registry:reload <anything>`. Includes a `.server-changes/` note for
the changelog.
### Debounced reload
Bursts of publishes are coalesced. The first publish schedules a reload
at T+`LLM_PRICING_RELOAD_DEBOUNCE_MS` (default 1s); subsequent publishes
during that window are no-ops because the trailing reload picks up
everything when it queries the DB. Bounds reload rate to at most 1 per
debounce window regardless of publisher chattiness, so a runaway
upstream publisher can't fan out into a flood of full-table-scan
reloads.
## Test plan
- [ ] With `LLM_PRICING_RELOAD_PUBSUB_ENABLED=false` (default):
`redis-cli PUBSUB NUMSUB llm-registry:reload` returns `0` while the
webapp is up
- [ ] With it set to `true`: returns `>= 1`
- [ ] `redis-cli PUBLISH llm-registry:reload test` returns `1` (one
subscriber received) on a subscribed process
- [ ] Mutate an `LlmModel` row externally, publish on the channel,
observe the registry's match() picks up the change without waiting for
the 5-min tick
- [ ] Publish 100x in rapid succession; confirm only one reload fires
within the debounce window