@trigger.dev/sdk 0.0.0-chat-prerelease-20260502065709 → 0.0.0-chat-prerelease-20260505140031

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.
Files changed (53) hide show
  1. package/dist/commonjs/v3/ai-shared.d.ts +59 -2
  2. package/dist/commonjs/v3/ai.js +227 -14
  3. package/dist/commonjs/v3/ai.js.map +1 -1
  4. package/dist/commonjs/v3/chat-client.d.ts +35 -1
  5. package/dist/commonjs/v3/chat-client.js +30 -0
  6. package/dist/commonjs/v3/chat-client.js.map +1 -1
  7. package/dist/commonjs/v3/chat-server.d.ts +191 -0
  8. package/dist/commonjs/v3/chat-server.js +678 -0
  9. package/dist/commonjs/v3/chat-server.js.map +1 -0
  10. package/dist/commonjs/v3/chat-server.test.d.ts +1 -0
  11. package/dist/commonjs/v3/chat-server.test.js +515 -0
  12. package/dist/commonjs/v3/chat-server.test.js.map +1 -0
  13. package/dist/commonjs/v3/chat.d.ts +37 -0
  14. package/dist/commonjs/v3/chat.js +185 -0
  15. package/dist/commonjs/v3/chat.js.map +1 -1
  16. package/dist/commonjs/v3/chat.test.js +206 -0
  17. package/dist/commonjs/v3/chat.test.js.map +1 -1
  18. package/dist/commonjs/v3/sessions.js +2 -1
  19. package/dist/commonjs/v3/sessions.js.map +1 -1
  20. package/dist/commonjs/v3/test/mock-chat-agent.d.ts +32 -0
  21. package/dist/commonjs/v3/test/mock-chat-agent.js +34 -7
  22. package/dist/commonjs/v3/test/mock-chat-agent.js.map +1 -1
  23. package/dist/commonjs/v3/test/test-session-handle.d.ts +10 -4
  24. package/dist/commonjs/v3/test/test-session-handle.js +56 -5
  25. package/dist/commonjs/v3/test/test-session-handle.js.map +1 -1
  26. package/dist/commonjs/version.js +1 -1
  27. package/dist/esm/v3/ai-shared.d.ts +59 -2
  28. package/dist/esm/v3/ai.js +227 -14
  29. package/dist/esm/v3/ai.js.map +1 -1
  30. package/dist/esm/v3/chat-client.d.ts +35 -1
  31. package/dist/esm/v3/chat-client.js +30 -0
  32. package/dist/esm/v3/chat-client.js.map +1 -1
  33. package/dist/esm/v3/chat-server.d.ts +191 -0
  34. package/dist/esm/v3/chat-server.js +675 -0
  35. package/dist/esm/v3/chat-server.js.map +1 -0
  36. package/dist/esm/v3/chat-server.test.d.ts +1 -0
  37. package/dist/esm/v3/chat-server.test.js +513 -0
  38. package/dist/esm/v3/chat-server.test.js.map +1 -0
  39. package/dist/esm/v3/chat.d.ts +37 -0
  40. package/dist/esm/v3/chat.js +185 -0
  41. package/dist/esm/v3/chat.js.map +1 -1
  42. package/dist/esm/v3/chat.test.js +206 -0
  43. package/dist/esm/v3/chat.test.js.map +1 -1
  44. package/dist/esm/v3/sessions.js +2 -1
  45. package/dist/esm/v3/sessions.js.map +1 -1
  46. package/dist/esm/v3/test/mock-chat-agent.d.ts +32 -0
  47. package/dist/esm/v3/test/mock-chat-agent.js +34 -7
  48. package/dist/esm/v3/test/mock-chat-agent.js.map +1 -1
  49. package/dist/esm/v3/test/test-session-handle.d.ts +10 -4
  50. package/dist/esm/v3/test/test-session-handle.js +57 -6
  51. package/dist/esm/v3/test/test-session-handle.js.map +1 -1
  52. package/dist/esm/version.js +1 -1
  53. package/package.json +18 -3
@@ -15,7 +15,7 @@
15
15
  * import from `ai.ts`.
16
16
  */
17
17
  import type { Task, AnyTask } from "@trigger.dev/core/v3";
18
- import type { UIMessage } from "ai";
18
+ import type { ModelMessage, UIMessage } from "ai";
19
19
  /**
20
20
  * Message-part `type` value for the pending-message data part the agent
21
21
  * injects when a follow-up message arrives mid-turn.
@@ -28,7 +28,16 @@ export declare const PENDING_MESSAGE_INJECTED_TYPE: "data-pending-message-inject
28
28
  export type ChatTaskWirePayload<TMessage extends UIMessage = UIMessage, TMetadata = unknown> = {
29
29
  messages: TMessage[];
30
30
  chatId: string;
31
- trigger: "submit-message" | "regenerate-message" | "preload" | "close" | "action";
31
+ trigger: "submit-message" | "regenerate-message" | "preload" | "close" | "action"
32
+ /**
33
+ * The customer's `chat.handover` route handler kicked us off in
34
+ * parallel with the first-turn `streamText` running in the warm
35
+ * Next.js process. The run sits idle on `session.in` waiting for
36
+ * a `kind: "handover"` (continue from tool execution) or
37
+ * `kind: "handover-skip"` (handler finished pure-text, exit
38
+ * cleanly). See `chat.handover` in `@trigger.dev/sdk/chat-server`.
39
+ */
40
+ | "handover-prepare";
32
41
  messageId?: string;
33
42
  metadata?: TMetadata;
34
43
  /** Custom action payload when `trigger` is `"action"`. Validated against `actionSchema` on the backend. */
@@ -77,6 +86,54 @@ export type ChatInputChunk<TMessage extends UIMessage = UIMessage, TMetadata = u
77
86
  kind: "stop";
78
87
  /** Optional human-readable reason. Maps to the legacy `chat-stop` record. */
79
88
  message?: string;
89
+ } | {
90
+ /**
91
+ * Sent by `chat.headStart` when the customer's first-turn
92
+ * `streamText` finishes. The agent run (currently parked in
93
+ * `handover-prepare`) wakes, seeds its accumulators with
94
+ * `partialAssistantMessage`, and runs the normal turn loop
95
+ * (`onChatStart` → `onTurnStart` → … → `onTurnComplete`).
96
+ *
97
+ * What happens after that depends on `isFinal`:
98
+ *
99
+ * - `isFinal: false` — step 1 ended with `finishReason:
100
+ * "tool-calls"`. The partial carries the assistant's
101
+ * tool-call(s) wrapped in AI SDK's tool-approval round. The
102
+ * agent's `streamText` runs the approved tools and continues
103
+ * from step 2.
104
+ * - `isFinal: true` — step 1 ended pure-text (no tool calls).
105
+ * The partial carries the final assistant text. The agent
106
+ * skips the LLM call entirely (the response is already
107
+ * complete on the customer side) and runs `onTurnComplete`
108
+ * with the partial as `responseMessage` so persistence and
109
+ * any post-turn work fire normally.
110
+ */
111
+ kind: "handover";
112
+ /** Customer's step-1 response messages (ModelMessage form). */
113
+ partialAssistantMessage: ModelMessage[];
114
+ /**
115
+ * The UI messageId the customer's handler used for its step-1
116
+ * assistant message. The agent reuses this so any post-handover
117
+ * chunks (tool-output-available, step-2 text, data-* parts
118
+ * written by hooks) merge into the SAME assistant message on
119
+ * the browser side instead of starting a new one.
120
+ */
121
+ messageId?: string;
122
+ /**
123
+ * Whether the customer's step 1 is the final response. See
124
+ * `kind` description above for the two branches.
125
+ */
126
+ isFinal: boolean;
127
+ } | {
128
+ /**
129
+ * Sent by `chat.headStart` only when the customer's handler
130
+ * ABORTS before producing a finishReason (e.g., dispatch error,
131
+ * stream cancelled before any tokens). The agent run exits
132
+ * cleanly without firing turn hooks. Normal pure-text and
133
+ * tool-call finishes go through `kind: "handover"` with the
134
+ * appropriate `isFinal` flag.
135
+ */
136
+ kind: "handover-skip";
80
137
  };
81
138
  /**
82
139
  * Extracts the client-data (`metadata`) type from a chat task.
@@ -672,12 +672,113 @@ const stopInput = {
672
672
  await getChatSession().in.send({ kind: "stop", message: data?.message }, options?.requestOptions);
673
673
  },
674
674
  };
675
+ /**
676
+ * Internal facade for waiting on the handover signal. Mirrors
677
+ * `messagesInput` / `stopInput` so the wait paths and tracing
678
+ * attributes stay consistent across all input-stream branches.
679
+ * @internal
680
+ */
681
+ const handoverInput = {
682
+ async waitWithIdleTimeout(options) {
683
+ while (true) {
684
+ const result = await getChatSession().in.waitWithIdleTimeout(options);
685
+ if (!result.ok)
686
+ return result;
687
+ if (result.output.kind === "handover" ||
688
+ result.output.kind === "handover-skip") {
689
+ return { ok: true, output: result.output };
690
+ }
691
+ // Other kinds (message, stop) are not expected during handover-prepare.
692
+ // Loop back; the message and stop facades have their own listeners
693
+ // running so signals on those kinds aren't lost.
694
+ }
695
+ },
696
+ };
675
697
  /**
676
698
  * Per-turn deferred promises. Registered via `chat.defer()`, awaited
677
699
  * before `onTurnComplete` fires. Reset each turn.
678
700
  * @internal
679
701
  */
680
702
  const chatDeferKey = locals_js_1.locals.create("chat.defer");
703
+ /**
704
+ * Run-scoped slot holding the partial assistant message handed over by
705
+ * `chat.handover` from a customer's first-turn `streamText`. Appended
706
+ * to `accumulatedMessages` during turn 0 setup so `streamText` resumes
707
+ * at tool execution. Cleared (read once) after consumption.
708
+ * @internal
709
+ */
710
+ const chatHandoverPartialKey = locals_js_1.locals.create("chat.handoverPartial");
711
+ /**
712
+ * Run-scoped slot holding the assistant `messageId` the customer's
713
+ * `chat.handover` handler used for its step-1 stream. The agent reuses
714
+ * it on the agent-side `toUIMessageStream` (and the synthesized
715
+ * partial UIMessage in `originalMessages`) so all chunks merge into a
716
+ * single assistant message on the browser side.
717
+ * @internal
718
+ */
719
+ const chatHandoverMessageIdKey = locals_js_1.locals.create("chat.handoverMessageId");
720
+ /**
721
+ * Run-scoped slot indicating that the customer's step-1 head-start
722
+ * response is the FINAL turn response. When true, turn 0 runs through
723
+ * the full turn-loop hooks but SKIPS the `userRun` / `streamText`
724
+ * call — the customer's partial already IS the response. The agent's
725
+ * `onTurnComplete` fires with that partial so persistence + any
726
+ * post-turn work happens normally. Cleared after consumption.
727
+ * @internal
728
+ */
729
+ const chatHandoverIsFinalKey = locals_js_1.locals.create("chat.handoverIsFinal");
730
+ /**
731
+ * Build a UIMessage representation of a `chat.handover` partial so AI
732
+ * SDK's `processUIMessageStream` can transition `tool-output-available`
733
+ * chunks (emitted by the initial-tool-execution branch when the
734
+ * approval round runs) onto the existing tool-call. Without this,
735
+ * `state.message.parts` is empty when the agent's `streamText`
736
+ * finishes, and AI SDK throws
737
+ * `UIMessageStreamError: No tool invocation found`.
738
+ *
739
+ * Only the assistant message matters — the synthesized
740
+ * `tool-approval-response` rows are AI-SDK-internal and don't need a
741
+ * UIMessage representation. We map:
742
+ * - `text` parts → `{ type: "text", text }`
743
+ * - `tool-call` parts → `{ type: "tool-${name}", toolCallId,
744
+ * state: "input-available", input }`
745
+ * - `tool-approval-request` parts → skipped (AI SDK derives the
746
+ * approval state from chunks during processing)
747
+ *
748
+ * @internal
749
+ */
750
+ function synthesizeHandoverUIMessage(partial, messageId) {
751
+ const assistant = partial.find((m) => m.role === "assistant");
752
+ if (!assistant || typeof assistant.content === "string")
753
+ return undefined;
754
+ const parts = [];
755
+ for (const part of assistant.content) {
756
+ if (part.type === "text" && typeof part.text === "string") {
757
+ parts.push({ type: "text", text: part.text });
758
+ }
759
+ else if (part.type === "tool-call" && part.toolCallId && part.toolName) {
760
+ parts.push({
761
+ type: `tool-${part.toolName}`,
762
+ toolCallId: part.toolCallId,
763
+ state: "input-available",
764
+ input: part.input,
765
+ });
766
+ }
767
+ // tool-approval-request parts intentionally skipped — they're an
768
+ // AI-SDK protocol detail, not a UI surface.
769
+ }
770
+ if (parts.length === 0)
771
+ return undefined;
772
+ // Use the customer's step-1 messageId if provided (so the agent's
773
+ // post-handover chunks merge into the same assistant message on the
774
+ // browser). Fall back to a fresh id only if the handover signal
775
+ // didn't carry one.
776
+ return {
777
+ id: messageId ?? (0, ai_1.generateId)(),
778
+ role: "assistant",
779
+ parts,
780
+ };
781
+ }
681
782
  /**
682
783
  * Per-turn background context queue. Messages added via `chat.backgroundWork.inject()`
683
784
  * are drained at the next `prepareStep` boundary and appended to the model messages.
@@ -1798,6 +1899,59 @@ function chatAgent(options) {
1798
1899
  return;
1799
1900
  }
1800
1901
  }
1902
+ // Handle handover-prepare runs — wait on session.in for the
1903
+ // customer's `chat.handover` route handler to either hand off
1904
+ // mid-turn (tool calls) or signal pure-text completion.
1905
+ if (payload.trigger === "handover-prepare") {
1906
+ if (activeSpan) {
1907
+ activeSpan.setAttribute("chat.handoverPreparing", true);
1908
+ }
1909
+ const handoverResult = await handoverInput.waitWithIdleTimeout({
1910
+ idleTimeoutInSeconds: idleTimeoutInSeconds ?? payload.idleTimeoutInSeconds ?? 60,
1911
+ spanName: "waiting for handover signal",
1912
+ });
1913
+ if (!handoverResult.ok) {
1914
+ // Handler crashed before signaling — exit cleanly.
1915
+ return;
1916
+ }
1917
+ if (handoverResult.output.kind === "handover-skip") {
1918
+ // Sent only when the customer's handler aborts before
1919
+ // producing a finishReason. Normal pure-text and
1920
+ // tool-call finishes go through `kind: "handover"` with
1921
+ // `isFinal: true | false`. Exit without firing any turn
1922
+ // hooks.
1923
+ return;
1924
+ }
1925
+ // kind === "handover": stash the partial assistant message
1926
+ // so turn-0 setup can append it after loading user
1927
+ // messages. Two branches downstream, switched by `isFinal`:
1928
+ // - `false`: customer's step 1 ended with `tool-calls`.
1929
+ // The agent's `streamText` sees pending tool-calls (via
1930
+ // the approval round in the partial) and executes them,
1931
+ // then runs step 2's LLM call.
1932
+ // - `true`: customer's step 1 ended pure-text. The agent
1933
+ // runs the turn-loop hooks but SKIPS the `streamText`
1934
+ // call entirely (the response is already complete).
1935
+ // `onTurnComplete` fires with the partial as
1936
+ // `responseMessage` so persistence works normally.
1937
+ locals_js_1.locals.set(chatHandoverPartialKey, handoverResult.output.partialAssistantMessage);
1938
+ // Stash the customer-side step-1 messageId. Turn-0 setup
1939
+ // uses it to seed the synthesized partial UIMessage with the
1940
+ // SAME id, so the agent's post-handover chunks merge into
1941
+ // the same assistant message on the browser side.
1942
+ if (handoverResult.output.messageId) {
1943
+ locals_js_1.locals.set(chatHandoverMessageIdKey, handoverResult.output.messageId);
1944
+ }
1945
+ locals_js_1.locals.set(chatHandoverIsFinalKey, handoverResult.output.isFinal);
1946
+ // Synthesize a wire payload that the turn loop treats as a
1947
+ // normal first-turn message. The original user-history
1948
+ // messages came in via `payload.messages` at trigger time;
1949
+ // reuse them.
1950
+ currentWirePayload = {
1951
+ ...payload,
1952
+ trigger: "submit-message",
1953
+ };
1954
+ }
1801
1955
  for (let turn = 0; turn < maxTurns; turn++) {
1802
1956
  try {
1803
1957
  // Extract turn-level context before entering the span
@@ -2122,6 +2276,38 @@ function chatAgent(options) {
2122
2276
  if (lastModel)
2123
2277
  turnNewModelMessages.push(lastModel);
2124
2278
  }
2279
+ // If a `chat.handover` route handler signalled a
2280
+ // mid-turn handover, splice its partial assistant
2281
+ // response (text + pending tool-calls + the
2282
+ // synthesized tool-approval round) onto the
2283
+ // accumulator. `streamText` will hit AI SDK's
2284
+ // initial-tool-execution branch, run the
2285
+ // agent-side tool executes, and resume from step 2
2286
+ // — skipping the first model call (already done
2287
+ // by the handler).
2288
+ //
2289
+ // We also synthesize a UIMessage form of the
2290
+ // partial assistant and push it to
2291
+ // `accumulatedUIMessages`. AI SDK's
2292
+ // `processUIMessageStream` (invoked when our
2293
+ // run-loop calls `runResult.toUIMessageStream({
2294
+ // onFinish })`) initializes `state.message` from
2295
+ // the trailing assistant in `originalMessages`.
2296
+ // Without that, the `tool-output-available`
2297
+ // chunks emitted by the initial-tool-execution
2298
+ // branch can't find their matching tool-call in
2299
+ // state and AI SDK throws
2300
+ // `UIMessageStreamError: No tool invocation found`.
2301
+ const pendingHandoverPartial = locals_js_1.locals.get(chatHandoverPartialKey);
2302
+ if (pendingHandoverPartial && pendingHandoverPartial.length > 0) {
2303
+ accumulatedMessages.push(...pendingHandoverPartial);
2304
+ const handoverMessageId = locals_js_1.locals.get(chatHandoverMessageIdKey);
2305
+ const partialUI = synthesizeHandoverUIMessage(pendingHandoverPartial, handoverMessageId);
2306
+ if (partialUI) {
2307
+ accumulatedUIMessages.push(partialUI);
2308
+ }
2309
+ locals_js_1.locals.set(chatHandoverPartialKey, []); // consume once
2310
+ }
2125
2311
  }
2126
2312
  else if (currentWirePayload.trigger === "regenerate-message") {
2127
2313
  // Regenerate: frontend sent full history with last assistant message
@@ -2281,6 +2467,18 @@ function chatAgent(options) {
2281
2467
  });
2282
2468
  let onFinishAttached = false;
2283
2469
  let runResult;
2470
+ // Pure-text head-start: customer's step 1 IS the
2471
+ // final response. Skip the user's `run` callback
2472
+ // (no LLM call) and use the synthesized partial
2473
+ // UIMessage as `capturedResponseMessage`. The post-
2474
+ // turn flow (`onBeforeTurnComplete` →
2475
+ // `onTurnComplete` → trigger:turn-complete) fires
2476
+ // normally so persistence works.
2477
+ const headStartIsFinal = locals_js_1.locals.get(chatHandoverIsFinalKey);
2478
+ const isHeadStartFinalTurn = turn === 0 && headStartIsFinal === true;
2479
+ if (isHeadStartFinalTurn) {
2480
+ locals_js_1.locals.set(chatHandoverIsFinalKey, undefined); // consume once
2481
+ }
2284
2482
  try {
2285
2483
  // Drain any messages injected by background work (e.g. self-review from previous turn).
2286
2484
  // Skip if the last message is a tool message — appending after it would
@@ -2292,20 +2490,35 @@ function chatAgent(options) {
2292
2490
  if (bgQueue && bgQueue.length > 0 && lastAccumulated?.role !== "tool") {
2293
2491
  accumulatedMessages.push(...bgQueue.splice(0));
2294
2492
  }
2295
- runResult = await userRun({
2296
- ...restWire,
2297
- messages: await applyPrepareMessages(accumulatedMessages, "run"),
2298
- clientData,
2299
- continuation,
2300
- previousRunId,
2301
- preloaded,
2302
- previousTurnUsage,
2303
- totalUsage: cumulativeUsage,
2304
- ctx,
2305
- signal: combinedSignal,
2306
- cancelSignal,
2307
- stopSignal,
2308
- });
2493
+ if (isHeadStartFinalTurn) {
2494
+ // The synthesized partial UIMessage IS the response.
2495
+ // It was pushed to `accumulatedUIMessages` during the
2496
+ // submit-message branch's splice; recover it as the
2497
+ // last assistant.
2498
+ const lastUI = accumulatedUIMessages[accumulatedUIMessages.length - 1];
2499
+ if (lastUI && lastUI.role === "assistant") {
2500
+ capturedResponseMessage = lastUI;
2501
+ capturedFinishReason = "stop";
2502
+ }
2503
+ // Don't call userRun. Don't pipe. Skip directly
2504
+ // to the post-turn flow below.
2505
+ }
2506
+ else {
2507
+ runResult = await userRun({
2508
+ ...restWire,
2509
+ messages: await applyPrepareMessages(accumulatedMessages, "run"),
2510
+ clientData,
2511
+ continuation,
2512
+ previousRunId,
2513
+ preloaded,
2514
+ previousTurnUsage,
2515
+ totalUsage: cumulativeUsage,
2516
+ ctx,
2517
+ signal: combinedSignal,
2518
+ cancelSignal,
2519
+ stopSignal,
2520
+ });
2521
+ }
2309
2522
  // Auto-pipe if the run function returned a StreamTextResult or similar,
2310
2523
  // but only if pipeChat() wasn't already called manually during this turn.
2311
2524
  // We call toUIMessageStream ourselves to attach onFinish for response capture.