@trigger.dev/sdk 4.6.0 → 4.6.2

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/esm/v3/ai.js CHANGED
@@ -2,6 +2,7 @@ import { accessoryAttributes, apiClientManager, controlSubtype, generateJWT, get
2
2
  // Runtime VALUES go through the ESM/CJS shim so the CJS build can `require`
3
3
  // ESM-only `ai@7` (see ../imports/ai-runtime.ts).
4
4
  import { trace } from "@opentelemetry/api";
5
+ import { traceSessionIdle } from "./sessionTracing.js";
5
6
  import { tool as aiTool, convertToModelMessages, dynamicTool, generateId as generateMessageId, getToolName, isToolUIPart, jsonSchema, readUIMessageStream, streamText as aiStreamText, zodSchema, } from "../imports/ai-runtime.js";
6
7
  import { createTranscriptShadow, defaultStorage, diffTranscript, fingerprintMessage, parseTranscriptRuntimeState, restoreModelLane, } from "./transcriptStorage.js";
7
8
  let transcriptStorageOverride;
@@ -1087,7 +1088,7 @@ async function waitOnChatRoute(route, options) {
1087
1088
  return tracer.startActiveSpan(options.spanName ?? `chat.${route}.wait()`, async (span) => {
1088
1089
  const idleMs = (options.idleTimeoutInSeconds ?? 0) * 1000;
1089
1090
  if (idleMs > 0) {
1090
- const warm = await router.next(route, { timeoutMs: idleMs });
1091
+ const warm = await traceSessionIdle(session.id, idleMs / 1000, () => router.next(route, { timeoutMs: idleMs }));
1091
1092
  if (warm) {
1092
1093
  span.setAttribute("wait.resolved", "idle");
1093
1094
  return { ok: true, output: warm.data, record: warm };
@@ -1141,10 +1142,6 @@ async function waitOnChatRoute(route, options) {
1141
1142
  session: session.id,
1142
1143
  io: "in",
1143
1144
  route,
1144
- ...accessoryAttributes({
1145
- items: [{ text: `${session.id}.in:${route}`, variant: "normal" }],
1146
- style: "codepath",
1147
- }),
1148
1145
  },
1149
1146
  });
1150
1147
  }
@@ -1820,6 +1817,14 @@ const chatHandoverPartialKey = locals.create("chat.handoverPartial");
1820
1817
  * @internal
1821
1818
  */
1822
1819
  const chatHandoverMessageIdKey = locals.create("chat.handoverMessageId");
1820
+ /**
1821
+ * The model messages a turn-0 head-start splice contributed to the model lane,
1822
+ * keyed by the UI message id it was synthesized under. When the agent's response
1823
+ * completes that message under the same id, this is the run to replace: the UI
1824
+ * form alone converts to something else (its pending tool calls drop out, the
1825
+ * approval round was never on it), so it cannot locate the run itself.
1826
+ */
1827
+ const chatHandoverSplicedRunKey = locals.create("chat.handoverSplicedRun");
1823
1828
  /**
1824
1829
  * Run-scoped slot indicating that the customer's step-1 head-start
1825
1830
  * response is the FINAL turn response. When true, turn 0 runs through
@@ -1903,16 +1908,19 @@ function synthesizeHandoverUIMessage(partial, messageId) {
1903
1908
  */
1904
1909
  function spliceHandoverPartial(modelMessages, uiMessages, signal) {
1905
1910
  if (!signal.partialAssistantMessage || signal.partialAssistantMessage.length === 0) {
1906
- return;
1911
+ return undefined;
1907
1912
  }
1908
1913
  // Skip if the hydrated chain already persisted the partial under this id.
1909
1914
  const alreadyInChain = signal.messageId !== undefined && uiMessages.some((m) => m.id === signal.messageId);
1910
1915
  if (alreadyInChain)
1911
- return;
1912
- modelMessages.push(...signal.partialAssistantMessage);
1916
+ return undefined;
1917
+ const run = [...signal.partialAssistantMessage];
1918
+ modelMessages.push(...run);
1913
1919
  const partialUI = synthesizeHandoverUIMessage(signal.partialAssistantMessage, signal.messageId);
1914
- if (partialUI)
1915
- uiMessages.push(partialUI);
1920
+ if (!partialUI)
1921
+ return undefined;
1922
+ uiMessages.push(partialUI);
1923
+ return { id: partialUI.id, run };
1916
1924
  }
1917
1925
  /**
1918
1926
  * Per-turn background context queue. Messages added via `chat.backgroundWork.inject()`
@@ -3652,8 +3660,8 @@ function isActionTurn(value) {
3652
3660
  * tail does not match the old message's conversion, nothing is changed and
3653
3661
  * `false` is returned so the caller can fall back to a full reconversion.
3654
3662
  */
3655
- async function replaceModelRun(lane, oldUi, newUi, tailAfter) {
3656
- const oldRun = await toModelMessages([stripProviderMetadata(oldUi)]);
3663
+ async function replaceModelRun(lane, oldUi, newUi, tailAfter, knownOldRun) {
3664
+ const oldRun = knownOldRun ?? (await toModelMessages([stripProviderMetadata(oldUi)]));
3657
3665
  const newRun = await toModelMessages([stripProviderMetadata(newUi)]);
3658
3666
  // A message that converts to nothing (a pending tool call with no output yet,
3659
3667
  // which `ignoreIncompleteToolCalls` drops) locates no run in the lane. Matching
@@ -4785,7 +4793,7 @@ function chatAgent(options) {
4785
4793
  const preloadResult = await messagesInput.waitWithIdleTimeout({
4786
4794
  idleTimeoutInSeconds: effectivePreloadIdleTimeout,
4787
4795
  timeout: effectivePreloadTimeout,
4788
- spanName: "waiting for first message",
4796
+ spanName: "first message",
4789
4797
  skipSuspend: exitAfterPreloadIdle,
4790
4798
  onSuspend: onChatSuspend
4791
4799
  ? async () => {
@@ -4937,7 +4945,7 @@ function chatAgent(options) {
4937
4945
  const continuationResult = await messagesInput.waitWithIdleTimeout({
4938
4946
  idleTimeoutInSeconds: effectiveIdleTimeout,
4939
4947
  timeout: effectiveTurnTimeout,
4940
- spanName: "waiting for first message (continuation)",
4948
+ spanName: "first message (continuation)",
4941
4949
  onSuspend: onChatSuspend
4942
4950
  ? async () => {
4943
4951
  await tracer.startActiveSpan("onChatSuspend()", async () => {
@@ -5466,10 +5474,12 @@ function chatAgent(options) {
5466
5474
  // `UIMessageStreamError: No tool invocation found`.
5467
5475
  const pendingHandoverPartial = locals.get(chatHandoverPartialKey);
5468
5476
  if (pendingHandoverPartial && pendingHandoverPartial.length > 0) {
5469
- spliceHandoverPartial(accumulatedMessages, accumulatedUIMessages, {
5477
+ const spliced = spliceHandoverPartial(accumulatedMessages, accumulatedUIMessages, {
5470
5478
  partialAssistantMessage: pendingHandoverPartial,
5471
5479
  messageId: locals.get(chatHandoverMessageIdKey),
5472
5480
  });
5481
+ if (spliced)
5482
+ locals.set(chatHandoverSplicedRunKey, spliced);
5473
5483
  locals.set(chatHandoverPartialKey, []); // consume once
5474
5484
  splicedHandoverPartial = true;
5475
5485
  }
@@ -5814,6 +5824,7 @@ function chatAgent(options) {
5814
5824
  // never reports final usage), which would block the turn loop
5815
5825
  // from ever firing onTurnComplete / writeTurnComplete.
5816
5826
  let turnUsage;
5827
+ let lastStepUsage;
5817
5828
  if (runResult != null &&
5818
5829
  typeof runResult.totalUsage?.then === "function") {
5819
5830
  try {
@@ -5826,6 +5837,18 @@ function chatAgent(options) {
5826
5837
  /* non-fatal — usage capture failed */
5827
5838
  }
5828
5839
  }
5840
+ const lastStepUsagePromise = runResult != null ? runResult.usage : undefined;
5841
+ if (typeof lastStepUsagePromise?.then === "function") {
5842
+ try {
5843
+ lastStepUsage = (await Promise.race([
5844
+ lastStepUsagePromise,
5845
+ new Promise((r) => setTimeout(() => r(undefined), 2_000)),
5846
+ ]));
5847
+ }
5848
+ catch {
5849
+ /* non-fatal — usage capture failed */
5850
+ }
5851
+ }
5829
5852
  if (turnUsage) {
5830
5853
  cumulativeUsage = addUsage(cumulativeUsage, turnUsage);
5831
5854
  previousTurnUsage = turnUsage;
@@ -5971,8 +5994,14 @@ function chatAgent(options) {
5971
5994
  stripProviderMetadata(capturedResponseMessage),
5972
5995
  ]);
5973
5996
  if (existingIdx !== -1) {
5997
+ const spliced = locals.get(chatHandoverSplicedRunKey);
5998
+ const splicedRun = spliced && previousAtIdx && spliced.id === previousAtIdx.id
5999
+ ? spliced.run
6000
+ : undefined;
5974
6001
  const ok = previousAtIdx !== undefined &&
5975
- (await replaceModelRun(accumulatedMessages, previousAtIdx, capturedResponseMessage, steerTailThisTurn));
6002
+ (await replaceModelRun(accumulatedMessages, previousAtIdx, capturedResponseMessage, steerTailThisTurn, splicedRun));
6003
+ if (splicedRun)
6004
+ locals.set(chatHandoverSplicedRunKey, undefined);
5976
6005
  if (!ok) {
5977
6006
  logger.warn("chat.agent: replaced response not found at the model lane tail; reconverting the lane");
5978
6007
  accumulatedMessages = await toModelMessages(accumulatedUIMessages);
@@ -6032,12 +6061,14 @@ function chatAgent(options) {
6032
6061
  const outerCompaction = locals.get(chatAgentCompactionKey);
6033
6062
  const innerCompactionState = locals.get(chatCompactionStateKey);
6034
6063
  if (outerCompaction && !innerCompactionState && turnUsage && !wasStopped) {
6064
+ const contextUsage = lastStepUsage ?? turnUsage;
6035
6065
  const shouldTrigger = await outerCompaction.shouldCompact({
6036
6066
  messages: accumulatedMessages,
6037
- totalTokens: turnUsage.totalTokens,
6038
- inputTokens: turnUsage.inputTokens,
6039
- outputTokens: turnUsage.outputTokens,
6040
- usage: turnUsage,
6067
+ totalTokens: contextUsage.totalTokens,
6068
+ inputTokens: contextUsage.inputTokens,
6069
+ outputTokens: contextUsage.outputTokens,
6070
+ usage: contextUsage,
6071
+ turnUsage,
6041
6072
  totalUsage: cumulativeUsage,
6042
6073
  chatId: currentWirePayload.chatId,
6043
6074
  turn,
@@ -6362,7 +6393,7 @@ function chatAgent(options) {
6362
6393
  const next = await messagesInput.waitWithIdleTimeout({
6363
6394
  idleTimeoutInSeconds: effectiveIdleTimeout,
6364
6395
  timeout: effectiveTurnTimeout,
6365
- spanName: "waiting for next message",
6396
+ spanName: "next message",
6366
6397
  onSuspend: onChatSuspend
6367
6398
  ? async () => {
6368
6399
  await tracer.startActiveSpan("onChatSuspend()", async () => {
@@ -6699,7 +6730,7 @@ function chatAgent(options) {
6699
6730
  const next = await messagesInput.waitWithIdleTimeout({
6700
6731
  idleTimeoutInSeconds: effectiveIdleTimeout,
6701
6732
  timeout: effectiveTurnTimeout,
6702
- spanName: "waiting for next message (after error)",
6733
+ spanName: "next message (after error)",
6703
6734
  });
6704
6735
  if (!next.ok) {
6705
6736
  return; // Timed out — end run gracefully
@@ -7739,6 +7770,8 @@ class ChatMessageAccumulator {
7739
7770
  modelMessages = [];
7740
7771
  uiMessages = [];
7741
7772
  _compaction;
7773
+ /** The run a spliced head-start partial contributed, until its response replaces it. */
7774
+ _handoverRun;
7742
7775
  _pendingMessages;
7743
7776
  _steeringQueue = [];
7744
7777
  constructor(options) {
@@ -7782,7 +7815,7 @@ class ChatMessageAccumulator {
7782
7815
  * `consumeHandover` for the wait+seed+apply convenience.
7783
7816
  */
7784
7817
  applyHandover(signal) {
7785
- spliceHandoverPartial(this.modelMessages, this.uiMessages, signal);
7818
+ this._handoverRun = spliceHandoverPartial(this.modelMessages, this.uiMessages, signal);
7786
7819
  }
7787
7820
  /**
7788
7821
  * One-call `chat.headStart` handover for a custom-agent loop: waits for the
@@ -7825,8 +7858,13 @@ class ChatMessageAccumulator {
7825
7858
  if (existingIdx !== -1) {
7826
7859
  const previous = this.uiMessages[existingIdx];
7827
7860
  this.uiMessages[existingIdx] = response;
7861
+ const handoverRun = this._handoverRun && this._handoverRun.id === previous.id
7862
+ ? this._handoverRun.run
7863
+ : undefined;
7864
+ if (handoverRun)
7865
+ this._handoverRun = undefined;
7828
7866
  try {
7829
- if (!(await replaceModelRun(this.modelMessages, previous, response, 0))) {
7867
+ if (!(await replaceModelRun(this.modelMessages, previous, response, 0, handoverRun))) {
7830
7868
  this.modelMessages = await toModelMessages(this.uiMessages.map((m) => stripProviderMetadata(m)));
7831
7869
  }
7832
7870
  }
@@ -7929,8 +7967,10 @@ class ChatMessageAccumulator {
7929
7967
  }
7930
7968
  /**
7931
7969
  * Run outer-loop compaction if needed. Call after adding the response
7932
- * and capturing usage. Applies `compactModelMessages` and `compactUIMessages`
7933
- * callbacks if configured.
7970
+ * and capturing usage. Pass the LAST step's usage (`result.usage`), which is
7971
+ * the context the model held on its final call; `result.totalUsage` sums every
7972
+ * step of a tool-using turn and belongs in `context.turnUsage`. Applies
7973
+ * `compactModelMessages` and `compactUIMessages` callbacks if configured.
7934
7974
  *
7935
7975
  * @returns `true` if compaction was performed, `false` otherwise.
7936
7976
  */
@@ -7943,6 +7983,7 @@ class ChatMessageAccumulator {
7943
7983
  inputTokens: usage.inputTokens,
7944
7984
  outputTokens: usage.outputTokens,
7945
7985
  usage,
7986
+ turnUsage: context?.turnUsage,
7946
7987
  totalUsage: context?.totalUsage,
7947
7988
  chatId: context?.chatId,
7948
7989
  turn: context?.turn,
@@ -8175,8 +8216,8 @@ function createChatSession(payload, options) {
8175
8216
  idleTimeoutInSeconds: sessionIdleTimeoutOpt ?? currentPayload.idleTimeoutInSeconds ?? 30,
8176
8217
  timeout,
8177
8218
  spanName: currentPayload.trigger === "preload"
8178
- ? "waiting for first message"
8179
- : "waiting for first message (continuation)",
8219
+ ? "first message"
8220
+ : "first message (continuation)",
8180
8221
  });
8181
8222
  if (!result.ok || runSignal.aborted) {
8182
8223
  stop.cleanup();
@@ -8213,7 +8254,7 @@ function createChatSession(payload, options) {
8213
8254
  const next = await messagesInput.waitWithIdleTimeout({
8214
8255
  idleTimeoutInSeconds,
8215
8256
  timeout,
8216
- spanName: "waiting for next message",
8257
+ spanName: "next message",
8217
8258
  });
8218
8259
  if (!next.ok || runSignal.aborted) {
8219
8260
  stop.cleanup();
@@ -8424,6 +8465,7 @@ function createChatSession(payload, options) {
8424
8465
  // indefinitely, which would wedge the turn loop (same guard as
8425
8466
  // chat.agent's turn loop).
8426
8467
  let turnUsage;
8468
+ let lastStepUsage;
8427
8469
  if (typeof source.totalUsage?.then === "function") {
8428
8470
  try {
8429
8471
  const usage = (await Promise.race([
@@ -8440,14 +8482,28 @@ function createChatSession(payload, options) {
8440
8482
  /* non-fatal */
8441
8483
  }
8442
8484
  }
8485
+ const lastStepUsagePromise = source.usage;
8486
+ if (typeof lastStepUsagePromise?.then === "function") {
8487
+ try {
8488
+ lastStepUsage = (await Promise.race([
8489
+ lastStepUsagePromise,
8490
+ new Promise((r) => setTimeout(() => r(undefined), 2_000)),
8491
+ ]));
8492
+ }
8493
+ catch {
8494
+ /* non-fatal */
8495
+ }
8496
+ }
8443
8497
  // Outer-loop compaction (same logic as chat.agent)
8444
8498
  if (sessionCompaction && turnUsage && !turnObj.stopped) {
8499
+ const contextUsage = lastStepUsage ?? turnUsage;
8445
8500
  const shouldTrigger = await sessionCompaction.shouldCompact({
8446
8501
  messages: accumulator.modelMessages,
8447
- totalTokens: turnUsage.totalTokens,
8448
- inputTokens: turnUsage.inputTokens,
8449
- outputTokens: turnUsage.outputTokens,
8450
- usage: turnUsage,
8502
+ totalTokens: contextUsage.totalTokens,
8503
+ inputTokens: contextUsage.inputTokens,
8504
+ outputTokens: contextUsage.outputTokens,
8505
+ usage: contextUsage,
8506
+ turnUsage,
8451
8507
  totalUsage: cumulativeUsage,
8452
8508
  chatId: currentPayload.chatId,
8453
8509
  turn,