Commit Graph

7272 Commits

Author SHA1 Message Date
Eric Allam c0986f8bd5 feat(sdk): server-side ChatStream / AgentChat -> Sessions (phase D)
Rewires the server-side AgentChat class in chat-client.ts onto the
Session primitive, matching the browser transport's shape.

- ChatSession persistence and SessionState internal state now key on
  sessionId (friendlyId). runId is optional, just a 'live run' hint.
- triggerNewRun upserts the backing Session via sessions.create
  (idempotent on externalId = chatId) before triggering so sessionId
  rides along in payload.
- sendRaw, steer, sendAction, close, stop all go through
  appendToSessionStream(sessionId, 'in',
  serializeInputChunk({kind: …})). Stop becomes {kind: 'stop'};
  messages become {kind: 'message', payload}; actions and close go
  through the message payload with trigger='action' / 'close'.
- Subscribe moves from /realtime/v1/streams/{runId}/chat to
  /realtime/v1/sessions/{sessionId}/out. Records arrive as JSON
  strings; the loop parses them back into objects before the
  trigger:turn-complete / trigger:upgrade-required dispatch.
- Upgrade-required path keeps the same Session, swaps runId only.
- Drops the CHAT_STREAM_KEY / CHAT_MESSAGES_STREAM_ID /
  CHAT_STOP_STREAM_ID imports from chat-constants.js — chat-client.ts
  no longer references the legacy stream keys (the constants file
  itself will be deleted in Phase F along with the three references
  still in ai.ts's re-exports and chat-constants.ts itself).

Server-side auth uses apiClientManager.accessToken (the env secret
key), which has full scopes — no token-scoping changes needed here.
The browser transport's token-scope updates (Phase E) already cover
the client side.
2026-05-05 11:06:25 +01:00
Eric Allam 08da57dd49 feat(sdk): chat.agent → Sessions migration (phases B + C + min E)
Rewires chat.agent's internal I/O and TriggerChatTransport's send +
subscribe paths onto the Session primitive. Minimum token-scope work
included so the transport's session endpoints actually authenticate.

Phase B — chat.agent internals (ai.ts)

- New ChatInputChunk tagged union (`kind: "message" | "stop"`) —
  replaces the two-stream split (chat-messages + chat-stop) with a
  single Session `.in` channel.
- New chatSessionHandleKey locals slot populated at run start from
  `payload.sessionId ?? payload.chatId`. Every module-level helper
  now resolves to the per-run session handle.
- Module-level `chatStream`, `messagesInput`, `stopInput` become thin
  facades over the session. `chatStream` mirrors
  `RealtimeDefinedStream<UIMessageChunk>` and delegates to
  `handle.out`. `messagesInput` / `stopInput` mirror
  `RealtimeDefinedInputStream<…>` and filter `.in` by kind — the two
  internal `.on()`/`.waitWithIdleTimeout()` callers and the
  `chat.messages` / `chat.createStopSignal` public exposures keep
  their existing shapes.
- Every `streams.writer(CHAT_STREAM_KEY, …)` callsite swaps to
  `chatStream.writer(…)` so all chat output flows through
  `session.out` → `SessionStreamInstance` → direct-to-S2.
- Threaded `sessionId` through `ChatTaskWirePayload` /
  `ChatTaskPayload` / `ChatTaskRunPayload` so advanced users can
  `sessions.open(sessionId)` directly from `run()`.

Phase C — TriggerChatTransport (chat.ts)

- `ChatSessionState` keys durable identity on `sessionId` (friendlyId);
  `runId` becomes an optional hint about whether a run is live.
- `ensureSession(chatId)` lazily upserts the Session via
  `apiClient.createSession({type: "chat.agent", externalId: chatId})`
  on the direct `accessToken` path. Idempotent — two tabs on the
  same chat converge.
- `sendMessages`, `sendPendingMessage`, `stopGeneration`,
  `sendAction` all go through `appendToSessionStream(sessionId, "in",
  serializeInputChunk({kind: …}))` — one endpoint, one tag per
  record.
- SSE subscribe URL moves from `/realtime/v1/streams/{runId}/chat` to
  `/realtime/v1/sessions/{sessionId}/out`. The old run-scoped
  `subscribeToStream` is replaced by `subscribeToSessionStream`.
  Incoming chunks come back as JSON strings on the session channel
  (server wraps records as `{data, id}` on S2), so the subscribe
  loop parses them back into objects to keep the rest of the control
  flow (turn-complete / upgrade-required / skipToTurnComplete)
  unchanged.
- Upgrade-required re-trigger keeps the same Session and swaps only
  the runId + token.
- `getSession` / `setSession` / `setOnSessionChange` / persistence
  shape all grow a `sessionId` field (runId now optional).

Phase E — minimum token scopes

- `chat.createTriggerAction` (server side) now creates the Session
  before triggering so it can (a) thread `sessionId` into the run
  payload and (b) mint a token with both run and session scopes.
  Returns `sessionId` in its result so the transport can skip its
  own `sessions.create` call on the server-side-trigger path.
- `TriggerChatTaskResult` gains optional `sessionId`.
- The two in-run PAT refresh sites (preloadAccessToken,
  turnAccessToken) add `read:sessions:{sessionId}` +
  `write:sessions:{sessionId}` alongside the existing run scopes.

Known follow-ups (deferred to later passes)

- Phase D: `AgentChat` / `ChatStream` in chat-client.ts still uses
  the old `/realtime/v1/streams/{runId}/chat` path. Used by server-
  side task-to-task compositions, not the browser transport.
- Phase F: delete CHAT_STREAM_KEY, CHAT_MESSAGES_STREAM_ID,
  CHAT_STOP_STREAM_ID from chat-constants.ts + ai-chat smoke verify.
2026-05-05 11:06:25 +01:00
Eric Allam 5c66161ca7 feat(sdk,core): Session channel SDK toolkits + waitpoints — client side
Build the client-side half of the Session channel extensions that the
sessions PR shipped on the server. Pairs with POST
/api/v1/runs/:runFriendlyId/session-streams/wait and the
append-fires-waitpoints wiring on the session append handler.

Extend SessionHandle with two asymmetric channels mirroring the
run-scoped streams primitives:

- .in (SessionInputChannel) mirrors streams.input. on / once / peek /
  wait / waitWithIdleTimeout for the task to consume; send for
  external clients to produce. .wait / .waitWithIdleTimeout suspend
  the run on a session-stream waitpoint; it resumes when a record
  lands on .in, same semantics as streams.input.wait on a run-scoped
  input stream.
- .out (SessionOutputChannel) mirrors streams.define. append / pipe /
  writer for the task to produce records — all three route through
  SessionStreamInstance -> StreamsWriterV2 for uniform parsed-object
  serialization on the subscribe side. read returns an SSE subscription
  for external consumers.

The two channels are disjoint classes with zero overlapping methods.
SessionHandle is { id, in, out } so directional tags stay at every
call site. No public initialize() — S2 credentials are an internal
detail of pipe / writer.

Core
- StandardSessionStreamManager + sessionStreams global: SSE-backed
  tail with once/on/peek buffering, auto-reconnect, lastSeqNum
  resume. Keyed on {sessionId, io}. Registered in dev- and managed-
  run workers; taskExecutor clears handlers at run end alongside
  input streams.
- SessionStreamInstance: S2-only parallel of StreamInstance. Fetches
  session S2 creds via initializeSessionStream and pipes through
  StreamsWriterV2.
- ApiClient.createSessionStreamWaitpoint — calls the new server route.

Reference
- references/hello-world/src/trigger/sessionsSmoke.ts now exercises
  .out.writer alongside .out.append.
- references/hello-world/src/trigger/sessionsWaitSmoke.ts (new) —
  end-to-end waitpoint validation. Orchestrator suspends on
  .in.waitWithIdleTimeout; a delayed sender task fires the waitpoint
  via .in.send; orchestrator resumes with the payload. match: true.
2026-05-05 11:06:25 +01:00
Eric Allam 0eb750fa34 feat(sdk,core): Session client SDK + hello-world smoke test
Client-side pair to the Session primitive server PR (TRI-8627).
Run-scoped streams.pipe / streams.input are untouched.

@trigger.dev/core ApiClient
- createSession / retrieveSession / updateSession / closeSession —
  zodfetch against /api/v1/sessions control plane
- listSessions — CursorPagePromise<SessionItem>, follows the runs/waitpoints
  convention (page[size], page[after], page[before] + filter[*])
- initializeSessionStream — PUT /realtime/v1/sessions/:session/:io,
  returns S2 creds in headers (feeds StreamsWriterV2 directly)
- appendToSessionStream — POST …/append
- subscribeToSessionStream — reuses SSEStreamSubscription for SSE
  subscribes (auto-retry, Last-Event-ID resume, abort propagation), so
  session subscribers get the exact same semantics as runs.fetchStream.
  Returns AsyncIterableStream<T>.

@trigger.dev/sdk sessions namespace
- sessions.create / retrieve / update / close / list — wraps the ApiClient
  with the standard tracer + accessoryAttributes + mergeRequestOptions.
  Returns ApiPromise / CursorPagePromise.
- sessions.open(id) returns a SessionHandle with .out and .in
  SessionChannels. Each channel exposes append / send / subscribe /
  initialize. The handle is polymorphic on friendlyId or externalId.
- auth.ts adds the `sessions` permission on PublicTokenPermissionProperties
  so auth.createPublicToken({ read: { sessions: ["session_abc"] } }) works.

Reference
- references/hello-world/src/trigger/sessionsSmoke.ts — idempotent
  Trigger.dev task that exercises every code path (control-plane CRUD,
  polymorphic lookup, list with tag/type/status/externalId filters, cursor
  pagination, out.initialize + append + subscribe SSE round-trip, in.send,
  close + idempotent re-close). Trigger via
  mcp__trigger__trigger_task(taskId: "sessions-smoke").

Verified live against the local webapp (project hello-world): 10/10
steps pass end-to-end, S2 round-trip returns appended chunks through the
shared SSEStreamSubscription pipeline.
2026-05-05 11:06:25 +01:00
Eric Allam 0c4b4b0da3 WIP chat.store primitive 2026-05-05 11:06:25 +01:00
Eric Allam 665b81b83c fix(cli): skills bundler resolves caller-relative paths + correct dev layout
Two correctness fixes caught during end-to-end validation:

1. Resolve skill.path relative to the file that called skills.define(),
   not the project root. The resource catalog already captures filePath
   on each SkillManifest — use it to compute the source folder.

2. In dev, copy skill bundles to {workingDir}/.trigger/skills/ (where
   the worker's cwd resolves). In deploy, keep copying to
   {outputPath}/.trigger/skills/ so the Dockerfile COPY . /app lands
   them at /app/.trigger/skills/.

Also upgrade the skill-discovery failure log from debug to warn so
config mistakes surface in the dev console instead of disappearing
silently.

Finally, update the ai-chat reference's aiChat.run() to pass chatTools
through chat.toStreamTextOptions({ tools: chatTools }) instead of
spreading them after — the auto-injected loadSkill/readFile/bash tools
would otherwise get overwritten by the explicit tools: chatTools key.
2026-05-05 11:06:25 +01:00
Eric Allam 9d5f3c2aa7 feat(sdk,cli,core,build): phase 1 of agent skills
Full Phase 1 for the new ai.skills primitive — SDK + CLI, no
backend. Developer-authored folders (SKILL.md + scripts/references/
assets) discovered at build time, bundled into the deploy image at
/app/.trigger/skills/{id}/, and auto-wired into streamText at
runtime via the loadSkill/readFile/bash tools.

Adds a time-utils example skill to the ai-chat reference: two bash
scripts (now.sh, add.sh) plus a timezones.txt cheat-sheet, wired
into the aiChat agent via chat.skills.set() in onChatStart and
onPreload. Exercises the full pipeline end-to-end.

Changeset: patch bump for @trigger.dev/sdk, @trigger.dev/core,
@trigger.dev/build, trigger.dev.
2026-05-05 11:06:25 +01:00
Eric Allam 9ef910b107 feat(cli): built-in skill bundler for trigger dev + deploy
New first-class bundling step (not a build extension): after esbuild
produces the worker bundle, fork the indexer locally to discover
skills registered via ai.skills.define(), validate each skill's
SKILL.md, and copy the folder into {outputPath}/.trigger/skills/{id}/.

Hooks into both buildWorker() (deploy) and devSession's updateBundle
(dev) right after createBuildManifestFromBundle and before the
extension onBuildComplete hook, so extensions can observe the
annotated manifest.skills. The existing Dockerfile COPY picks up the
new .trigger/skills/ subdirectory without changes.

Also: managed-index-worker and dev-index-worker now emit
resourceCatalog.listSkillManifests() in the INDEX_COMPLETE message
so downstream stages can see the skill list.

Part 3/3 of Phase 1 for the new ai.skills primitive.
2026-05-05 11:06:25 +01:00
Eric Allam eea6676128 feat(sdk): add skills.define + chat.skills runtime wiring
skills.define({ id, path }) registers a skill with the resource
catalog and returns a SkillHandle. SkillHandle.local() reads the
bundled SKILL.md from ./.trigger/skills/{id}/ at runtime, parses
frontmatter, and returns a ResolvedSkill ready for chat.skills.set().

chat.skills.set([...]) stores resolved skills for the current run.
chat.toStreamTextOptions() auto-injects the skills preamble into the
system prompt and merges three tools — loadSkill, readFile, bash —
scoped per-skill with path-traversal guards and output caps (64 KB
stdout/stderr, 1 MB readFile). Bash executes in the worker container
with the turn's abort signal, no sandbox — skills are developer code.

Shared packages/build/src/internal/copyFiles.ts extracted from the
additionalFiles extension so the CLI's built-in skill bundler and the
existing extension share one glob + copy implementation.

Part 2/3 of Phase 1 for the new ai.skills primitive.
2026-05-05 11:06:25 +01:00
Eric Allam bf8e936774 feat(core): add skill resource catalog + SkillManifest schemas
SkillMetadata + SkillManifest zod schemas alongside the existing
task/prompt ones. registerSkillMetadata / listSkillManifests /
getSkillManifest on ResourceCatalog (both Standard and Noop), wired
through the ResourceCatalogAPI facade. BuildManifest + WorkerManifest
gain an optional `skills` array so the built-in CLI bundler can
annotate discoveries and the indexer can report them.

Part 1/3 of Phase 1 for the new ai.skills primitive.
2026-05-05 11:06:25 +01:00
Eric Allam c146ef1c30 fix(chat): include "action" in ChatTaskPayload.trigger type
run() is invoked with trigger: "action" after onAction processes a
typed action, but the type previously omitted it. Adding it lets
users cleanly short-circuit the LLM call for actions that don't
need a response (e.g. user-initiated compaction):
  if (trigger === "action") return;
2026-05-05 11:06:25 +01:00
Eric Allam 47ac1b6b5a feat(chat): add chat.endRun()
Exit the loop after the current turn completes without the
upgrade-required signal that chat.requestUpgrade() sends. Use when an
agent finishes its work on its own terms — one-shot responses, goal
achieved, budget exhausted — instead of waiting idle for the next
user message. Callable from run(), chat.defer(),
onBeforeTurnComplete, or onTurnComplete.

Resolves TRI-8391.
2026-05-05 11:06:25 +01:00
Eric Allam f0909df632 feat(chat): expose finishReason on turn-complete events
Surface the AI SDK's FinishReason on TurnCompleteEvent and
BeforeTurnCompleteEvent. Gives hooks a clean signal for distinguishing
a normal turn end from one paused on a pending tool call (HITL flows
like ask_user). Undefined for manual pipeChat() or aborted streams.
2026-05-05 11:06:25 +01:00
Eric Allam c4e0a040ea feat(sdk): add mockChatAgent test harness with locals DI
Drives a chat.agent definition through real turns offline — send
messages, actions, and stop signals; inspect captured chunks; assert
on hook order. Pre-seed dependencies via setupLocals so hooks read
test instances (DB clients, stubs) via locals.get() instead of
leaking through untrusted clientData.

Adds ai-chat reference tests exercising the harness across basic
flow, onValidateMessages, hydrateMessages, and actions.
2026-05-05 11:06:25 +01:00
Eric Allam 056805c1bd feat(core): add runInMockTaskContext test infrastructure
In-memory managers for locals, lifecycle hooks, runtime, input streams,
and realtime streams, plus a mock TaskContext. Lets task code be driven
end-to-end without hitting the Trigger.dev runtime — send data into
input streams, inspect chunks written to output streams, and pre-seed
locals for dependency injection.
2026-05-05 11:06:25 +01:00
Eric Allam 0469366ea2 fix(ai-chat): defer multi-tab broadcasts, disable streamdown word animation 2026-05-05 11:06:25 +01:00
Eric Allam 29097d7d08 feat(chat): multi-tab coordination via BroadcastChannel 2026-05-05 11:06:25 +01:00
Eric Allam 964794a447 chore(ai-chat): remove test-big-error trigger from onValidateMessages 2026-05-05 11:06:25 +01:00
Eric Allam c46543b725 feat(ai-chat): add askUser tool for HITL testing, verify TRI-8556 fix 2026-05-05 11:06:25 +01:00
Eric Allam c7af17fff5 fix pnpm lock file 2026-05-05 11:06:25 +01:00
Eric Allam 1fb15e1000 fix(chat): prevent useChat resume from hanging on completed turns
Add isStreaming flag to session state — set true when streaming starts,
  false on turn-complete. reconnectToStream returns null immediately when
  isStreaming is false, so resume: true is safe to pass unconditionally.
2026-05-05 11:06:25 +01:00
Eric Allam bbb5cfb033 feat(chat): add hydrateMessages, chat.history, and custom actions
Backend-controlled message history via hydrateMessages hook — loads from
  DB on every turn, replacing the linear accumulator. Imperative chat.history
  API (rollbackTo, remove, replace, slice) for modifying history from any
  hook or run(). Custom actions via actionSchema + onAction — typed actions
  sent through transport.sendAction() that wake the agent, modify state,
  then trigger run(). Adds sendAction to both TriggerChatTransport and
  AgentChat.
2026-05-05 11:06:25 +01:00
Eric Allam 375409c1da feat(chat): add chat.response API for persistent data parts, transient flag support 2026-05-05 11:06:25 +01:00
Eric Allam d61414711a prevent preloads from firing twice when in React strictMode 2026-05-05 11:06:25 +01:00
Eric Allam 216ba98dd2 fix: restore applyPrepareMessages call after agentcrumbs strip 2026-05-05 11:06:25 +01:00
Eric Allam 3241e1cd71 feat(chat): tool approvals support — ID-matched message replacement, sendEmail example, approval UI 2026-05-05 11:06:25 +01:00
Eric Allam cc2ad1f565 feat(chat): allow generateMessageId in uiMessageStreamOptions, auto-pass originalMessages 2026-05-05 11:06:25 +01:00
Eric Allam 569cd7c5c1 fix(sdk): inject prepareStep in toStreamTextOptions even without chat.prompt.set() 2026-05-05 11:06:25 +01:00
Eric Allam 925add20b2 feat(chat): add stopGeneration, fix onTurnComplete/onFinishPromise, add /chats/[chatId] route to ai-chat 2026-05-05 11:06:25 +01:00
Eric Allam 4b9bb59013 Add run agent view 2026-05-05 11:06:25 +01:00
Eric Allam 611904b3e7 Support for upgrading an agent to a new version 2026-05-05 11:06:25 +01:00
Eric Allam ee0fb66999 Add support for optionally validating UI messages 2026-05-05 11:06:25 +01:00
Eric Allam d973dd118d add agent mcp tools 2026-05-05 11:06:25 +01:00
Eric Allam 8c5f012c4b Add server-to-server chat support and subagent support to the playground, plus docs 2026-05-05 11:06:25 +01:00
Eric Allam badc7de6d3 Add the chat client and strip agent crumbs 2026-05-05 11:06:25 +01:00
Eric Allam 581c1ec92f Some design tweaks, improvements to playground options, rename unnamed preloaded conversations on first message 2026-05-05 11:06:25 +01:00
Eric Allam 5569201532 playground ui tweaks 2026-05-05 11:06:25 +01:00
Eric Allam 8245586b0b feat: upgrade streamdown to v2.5.0 with custom Trigger.dev Shiki theme
- Upgrade streamdown from v1.4.0 to v2.5.0 with @streamdown/code plugin
- Custom Shiki theme matching the Trigger.dev VS Code dark theme colors
- Consolidate duplicated lazy StreamdownRenderer into shared component
- Patch streamdown to inline highlighted body (fixes Arc browser)
- Add streamdown storybook page for visual testing
- Handle AGENT triggerSource in TestTaskPresenter exhaustive switch
- Update Tailwind config to scan streamdown dist for utility classes
2026-05-05 11:06:25 +01:00
Eric Allam ed40628946 chat.task -> chat.agent
plus playground support, including playground conversations, and a new
agent list
2026-05-05 11:06:25 +01:00
Eric Allam 84d430edcd Add support for triggering from the backend 2026-05-05 11:06:25 +01:00
Eric Allam 9e7b2d1ac5 feat(sdk): add onChatSuspend/onChatResume hooks, exitAfterPreloadIdle, chat.withClientData, and ChatBuilder 2026-05-05 11:06:25 +01:00
Eric Allam 828c34a819 feat(sdk): ctx on chat.task hooks; ai-chat E2B sandbox; docs patterns
- Pass TaskRunContext through chat lifecycle events, CompactedEvent, and
  ChatTaskRunPayload; use ctx.run.id for chat access tokens
- Export TaskRunContext from @trigger.dev/sdk
- ai-chat reference: executeCode via E2B, code-sandbox module, warm on
  onTurnStart, dispose on token onWait and onComplete; chat.local run id
- Docs: database persistence + code sandbox pattern pages; reference and
  backend updates for ctx; chat.defer anchor; navigation
2026-05-05 11:06:25 +01:00
Eric Allam b3106f8b2c Add run-scoped PAT renewal for chat transport 2026-05-05 11:06:25 +01:00
Eric Allam dbe5ae4b56 feat(sdk): ToolSet typing for toolFromTask, ai.toolExecute, deprecate ai.tool
- Assert toolFromTask return as AI SDK ToolSet-compatible; import ToolSet from ai
- Add changesets for @trigger.dev/sdk
- ai-chat reference: chat-tools module, registry language model helper, streamText cleanup
- Prisma migration removing user tool demo; demo docs and next config tweaks
2026-05-05 11:06:25 +01:00
Eric Allam fe90614142 feat(chat): add chat.inject() for background context injection and chat.defer improvements
- chat.inject(): queue model messages from background work for injection
  at the next prepareStep boundary or before the next turn's run()
- Deferred work from onTurnComplete no longer blocks waiting for next message
- Background queue persists across turns (not reset) so deferred work from
  onTurnComplete can inject into the next turn
- Reference app: self-review pattern using generateObject + chat.inject()
- Hide transient data-turn-status and data-background-context-injected parts in UI
2026-05-05 11:06:25 +01:00
Eric Allam 973bd2c711 feat(ai): add triggerAndSubscribe method and use it in ai.tool
Replace triggerAndWait with triggerAndSubscribe in ai.tool to fix:
- Parallel tool calls (no more preventMultipleWaits errors)
- Stop signal while suspended (parent stays alive, child gets cancelled)

New task.triggerAndSubscribe() method: trigger + subscribeToRun in a
single span, with abort signal support and cancelOnAbort option.

Convert deepResearch to a schemaTask + ai.tool in the reference app.

refs TRI-7986
2026-05-05 11:06:25 +01:00
Eric Allam a503e99a57 Add a writer to easily write chunks in callbacks 2026-05-05 11:06:25 +01:00
Eric Allam 843b85e356 feat(chat): add compaction option, pendingMessages steering, and usePendingMessages hook 2026-05-05 11:06:25 +01:00
Eric Allam e94ce5e8a8 better compaction support in our other chat variants 2026-05-05 11:06:25 +01:00
Eric Allam e06fbdb71d feat: support message compaction 2026-05-05 11:06:25 +01:00