@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.
@@ -667,14 +667,24 @@ export type CompactedEvent = {
667
667
  export type ShouldCompactEvent = {
668
668
  /** The current model messages (full conversation). */
669
669
  messages: ModelMessage[];
670
- /** Total token count from the triggering step/turn. */
670
+ /**
671
+ * Total token count of the triggering model call: the step that just finished
672
+ * (`"inner"`), or the turn's LAST step (`"outer"`). This is the size of the
673
+ * context the model held on that call, which is what a compaction decision is
674
+ * about. It is never the sum over a multi-step turn; see `turnUsage` for that.
675
+ */
671
676
  totalTokens: number | undefined;
672
- /** Input token count from the triggering step/turn. */
677
+ /** Input token count of the triggering model call (see `totalTokens`). */
673
678
  inputTokens: number | undefined;
674
- /** Output token count from the triggering step/turn. */
679
+ /** Output token count of the triggering model call (see `totalTokens`). */
675
680
  outputTokens: number | undefined;
676
- /** Full usage object from the triggering step/turn. */
681
+ /** Full usage object of the triggering model call (see `totalTokens`). */
677
682
  usage?: LanguageModelUsage;
683
+ /**
684
+ * The whole turn's usage summed over every step, as the provider billed it.
685
+ * Only present when `source` is `"outer"`.
686
+ */
687
+ turnUsage?: LanguageModelUsage;
678
688
  /** Cumulative token usage across all completed turns. Present in chat.agent contexts. */
679
689
  totalUsage?: LanguageModelUsage;
680
690
  /** The chat session ID (if running inside a chat.agent). */
@@ -2899,6 +2909,8 @@ declare class ChatMessageAccumulator {
2899
2909
  modelMessages: ModelMessage[];
2900
2910
  uiMessages: UIMessage[];
2901
2911
  private _compaction?;
2912
+ /** The run a spliced head-start partial contributed, until its response replaces it. */
2913
+ private _handoverRun?;
2902
2914
  private _pendingMessages?;
2903
2915
  private _steeringQueue;
2904
2916
  constructor(options?: {
@@ -2995,8 +3007,10 @@ declare class ChatMessageAccumulator {
2995
3007
  } | undefined>) | undefined;
2996
3008
  /**
2997
3009
  * Run outer-loop compaction if needed. Call after adding the response
2998
- * and capturing usage. Applies `compactModelMessages` and `compactUIMessages`
2999
- * callbacks if configured.
3010
+ * and capturing usage. Pass the LAST step's usage (`result.usage`), which is
3011
+ * the context the model held on its final call; `result.totalUsage` sums every
3012
+ * step of a tool-using turn and belongs in `context.turnUsage`. Applies
3013
+ * `compactModelMessages` and `compactUIMessages` callbacks if configured.
3000
3014
  *
3001
3015
  * @returns `true` if compaction was performed, `false` otherwise.
3002
3016
  */
@@ -3004,6 +3018,8 @@ declare class ChatMessageAccumulator {
3004
3018
  chatId?: string;
3005
3019
  turn?: number;
3006
3020
  clientData?: unknown;
3021
+ /** The whole turn summed over its steps (`result.totalUsage`). */
3022
+ turnUsage?: LanguageModelUsage;
3007
3023
  totalUsage?: LanguageModelUsage;
3008
3024
  }): Promise<boolean>;
3009
3025
  }
@@ -15,6 +15,7 @@ const v3_1 = require("@trigger.dev/core/v3");
15
15
  // Runtime VALUES go through the ESM/CJS shim so the CJS build can `require`
16
16
  // ESM-only `ai@7` (see ../imports/ai-runtime.ts).
17
17
  const api_1 = require("@opentelemetry/api");
18
+ const sessionTracing_js_1 = require("./sessionTracing.js");
18
19
  const ai_runtime_js_1 = require("../imports/ai-runtime.js");
19
20
  const transcriptStorage_js_1 = require("./transcriptStorage.js");
20
21
  let transcriptStorageOverride;
@@ -1110,7 +1111,7 @@ async function waitOnChatRoute(route, options) {
1110
1111
  return tracer_js_1.tracer.startActiveSpan(options.spanName ?? `chat.${route}.wait()`, async (span) => {
1111
1112
  const idleMs = (options.idleTimeoutInSeconds ?? 0) * 1000;
1112
1113
  if (idleMs > 0) {
1113
- const warm = await router.next(route, { timeoutMs: idleMs });
1114
+ const warm = await (0, sessionTracing_js_1.traceSessionIdle)(session.id, idleMs / 1000, () => router.next(route, { timeoutMs: idleMs }));
1114
1115
  if (warm) {
1115
1116
  span.setAttribute("wait.resolved", "idle");
1116
1117
  return { ok: true, output: warm.data, record: warm };
@@ -1164,10 +1165,6 @@ async function waitOnChatRoute(route, options) {
1164
1165
  session: session.id,
1165
1166
  io: "in",
1166
1167
  route,
1167
- ...(0, v3_1.accessoryAttributes)({
1168
- items: [{ text: `${session.id}.in:${route}`, variant: "normal" }],
1169
- style: "codepath",
1170
- }),
1171
1168
  },
1172
1169
  });
1173
1170
  }
@@ -1843,6 +1840,14 @@ const chatHandoverPartialKey = locals_js_1.locals.create("chat.handoverPartial")
1843
1840
  * @internal
1844
1841
  */
1845
1842
  const chatHandoverMessageIdKey = locals_js_1.locals.create("chat.handoverMessageId");
1843
+ /**
1844
+ * The model messages a turn-0 head-start splice contributed to the model lane,
1845
+ * keyed by the UI message id it was synthesized under. When the agent's response
1846
+ * completes that message under the same id, this is the run to replace: the UI
1847
+ * form alone converts to something else (its pending tool calls drop out, the
1848
+ * approval round was never on it), so it cannot locate the run itself.
1849
+ */
1850
+ const chatHandoverSplicedRunKey = locals_js_1.locals.create("chat.handoverSplicedRun");
1846
1851
  /**
1847
1852
  * Run-scoped slot indicating that the customer's step-1 head-start
1848
1853
  * response is the FINAL turn response. When true, turn 0 runs through
@@ -1926,16 +1931,19 @@ function synthesizeHandoverUIMessage(partial, messageId) {
1926
1931
  */
1927
1932
  function spliceHandoverPartial(modelMessages, uiMessages, signal) {
1928
1933
  if (!signal.partialAssistantMessage || signal.partialAssistantMessage.length === 0) {
1929
- return;
1934
+ return undefined;
1930
1935
  }
1931
1936
  // Skip if the hydrated chain already persisted the partial under this id.
1932
1937
  const alreadyInChain = signal.messageId !== undefined && uiMessages.some((m) => m.id === signal.messageId);
1933
1938
  if (alreadyInChain)
1934
- return;
1935
- modelMessages.push(...signal.partialAssistantMessage);
1939
+ return undefined;
1940
+ const run = [...signal.partialAssistantMessage];
1941
+ modelMessages.push(...run);
1936
1942
  const partialUI = synthesizeHandoverUIMessage(signal.partialAssistantMessage, signal.messageId);
1937
- if (partialUI)
1938
- uiMessages.push(partialUI);
1943
+ if (!partialUI)
1944
+ return undefined;
1945
+ uiMessages.push(partialUI);
1946
+ return { id: partialUI.id, run };
1939
1947
  }
1940
1948
  /**
1941
1949
  * Per-turn background context queue. Messages added via `chat.backgroundWork.inject()`
@@ -3665,8 +3673,8 @@ function isActionTurn(value) {
3665
3673
  * tail does not match the old message's conversion, nothing is changed and
3666
3674
  * `false` is returned so the caller can fall back to a full reconversion.
3667
3675
  */
3668
- async function replaceModelRun(lane, oldUi, newUi, tailAfter) {
3669
- const oldRun = await toModelMessages([stripProviderMetadata(oldUi)]);
3676
+ async function replaceModelRun(lane, oldUi, newUi, tailAfter, knownOldRun) {
3677
+ const oldRun = knownOldRun ?? (await toModelMessages([stripProviderMetadata(oldUi)]));
3670
3678
  const newRun = await toModelMessages([stripProviderMetadata(newUi)]);
3671
3679
  // A message that converts to nothing (a pending tool call with no output yet,
3672
3680
  // which `ignoreIncompleteToolCalls` drops) locates no run in the lane. Matching
@@ -4798,7 +4806,7 @@ function chatAgent(options) {
4798
4806
  const preloadResult = await messagesInput.waitWithIdleTimeout({
4799
4807
  idleTimeoutInSeconds: effectivePreloadIdleTimeout,
4800
4808
  timeout: effectivePreloadTimeout,
4801
- spanName: "waiting for first message",
4809
+ spanName: "first message",
4802
4810
  skipSuspend: exitAfterPreloadIdle,
4803
4811
  onSuspend: onChatSuspend
4804
4812
  ? async () => {
@@ -4950,7 +4958,7 @@ function chatAgent(options) {
4950
4958
  const continuationResult = await messagesInput.waitWithIdleTimeout({
4951
4959
  idleTimeoutInSeconds: effectiveIdleTimeout,
4952
4960
  timeout: effectiveTurnTimeout,
4953
- spanName: "waiting for first message (continuation)",
4961
+ spanName: "first message (continuation)",
4954
4962
  onSuspend: onChatSuspend
4955
4963
  ? async () => {
4956
4964
  await tracer_js_1.tracer.startActiveSpan("onChatSuspend()", async () => {
@@ -5479,10 +5487,12 @@ function chatAgent(options) {
5479
5487
  // `UIMessageStreamError: No tool invocation found`.
5480
5488
  const pendingHandoverPartial = locals_js_1.locals.get(chatHandoverPartialKey);
5481
5489
  if (pendingHandoverPartial && pendingHandoverPartial.length > 0) {
5482
- spliceHandoverPartial(accumulatedMessages, accumulatedUIMessages, {
5490
+ const spliced = spliceHandoverPartial(accumulatedMessages, accumulatedUIMessages, {
5483
5491
  partialAssistantMessage: pendingHandoverPartial,
5484
5492
  messageId: locals_js_1.locals.get(chatHandoverMessageIdKey),
5485
5493
  });
5494
+ if (spliced)
5495
+ locals_js_1.locals.set(chatHandoverSplicedRunKey, spliced);
5486
5496
  locals_js_1.locals.set(chatHandoverPartialKey, []); // consume once
5487
5497
  splicedHandoverPartial = true;
5488
5498
  }
@@ -5827,6 +5837,7 @@ function chatAgent(options) {
5827
5837
  // never reports final usage), which would block the turn loop
5828
5838
  // from ever firing onTurnComplete / writeTurnComplete.
5829
5839
  let turnUsage;
5840
+ let lastStepUsage;
5830
5841
  if (runResult != null &&
5831
5842
  typeof runResult.totalUsage?.then === "function") {
5832
5843
  try {
@@ -5839,6 +5850,18 @@ function chatAgent(options) {
5839
5850
  /* non-fatal — usage capture failed */
5840
5851
  }
5841
5852
  }
5853
+ const lastStepUsagePromise = runResult != null ? runResult.usage : undefined;
5854
+ if (typeof lastStepUsagePromise?.then === "function") {
5855
+ try {
5856
+ lastStepUsage = (await Promise.race([
5857
+ lastStepUsagePromise,
5858
+ new Promise((r) => setTimeout(() => r(undefined), 2_000)),
5859
+ ]));
5860
+ }
5861
+ catch {
5862
+ /* non-fatal — usage capture failed */
5863
+ }
5864
+ }
5842
5865
  if (turnUsage) {
5843
5866
  cumulativeUsage = addUsage(cumulativeUsage, turnUsage);
5844
5867
  previousTurnUsage = turnUsage;
@@ -5984,8 +6007,14 @@ function chatAgent(options) {
5984
6007
  stripProviderMetadata(capturedResponseMessage),
5985
6008
  ]);
5986
6009
  if (existingIdx !== -1) {
6010
+ const spliced = locals_js_1.locals.get(chatHandoverSplicedRunKey);
6011
+ const splicedRun = spliced && previousAtIdx && spliced.id === previousAtIdx.id
6012
+ ? spliced.run
6013
+ : undefined;
5987
6014
  const ok = previousAtIdx !== undefined &&
5988
- (await replaceModelRun(accumulatedMessages, previousAtIdx, capturedResponseMessage, steerTailThisTurn));
6015
+ (await replaceModelRun(accumulatedMessages, previousAtIdx, capturedResponseMessage, steerTailThisTurn, splicedRun));
6016
+ if (splicedRun)
6017
+ locals_js_1.locals.set(chatHandoverSplicedRunKey, undefined);
5989
6018
  if (!ok) {
5990
6019
  v3_1.logger.warn("chat.agent: replaced response not found at the model lane tail; reconverting the lane");
5991
6020
  accumulatedMessages = await toModelMessages(accumulatedUIMessages);
@@ -6045,12 +6074,14 @@ function chatAgent(options) {
6045
6074
  const outerCompaction = locals_js_1.locals.get(chatAgentCompactionKey);
6046
6075
  const innerCompactionState = locals_js_1.locals.get(chatCompactionStateKey);
6047
6076
  if (outerCompaction && !innerCompactionState && turnUsage && !wasStopped) {
6077
+ const contextUsage = lastStepUsage ?? turnUsage;
6048
6078
  const shouldTrigger = await outerCompaction.shouldCompact({
6049
6079
  messages: accumulatedMessages,
6050
- totalTokens: turnUsage.totalTokens,
6051
- inputTokens: turnUsage.inputTokens,
6052
- outputTokens: turnUsage.outputTokens,
6053
- usage: turnUsage,
6080
+ totalTokens: contextUsage.totalTokens,
6081
+ inputTokens: contextUsage.inputTokens,
6082
+ outputTokens: contextUsage.outputTokens,
6083
+ usage: contextUsage,
6084
+ turnUsage,
6054
6085
  totalUsage: cumulativeUsage,
6055
6086
  chatId: currentWirePayload.chatId,
6056
6087
  turn,
@@ -6375,7 +6406,7 @@ function chatAgent(options) {
6375
6406
  const next = await messagesInput.waitWithIdleTimeout({
6376
6407
  idleTimeoutInSeconds: effectiveIdleTimeout,
6377
6408
  timeout: effectiveTurnTimeout,
6378
- spanName: "waiting for next message",
6409
+ spanName: "next message",
6379
6410
  onSuspend: onChatSuspend
6380
6411
  ? async () => {
6381
6412
  await tracer_js_1.tracer.startActiveSpan("onChatSuspend()", async () => {
@@ -6712,7 +6743,7 @@ function chatAgent(options) {
6712
6743
  const next = await messagesInput.waitWithIdleTimeout({
6713
6744
  idleTimeoutInSeconds: effectiveIdleTimeout,
6714
6745
  timeout: effectiveTurnTimeout,
6715
- spanName: "waiting for next message (after error)",
6746
+ spanName: "next message (after error)",
6716
6747
  });
6717
6748
  if (!next.ok) {
6718
6749
  return; // Timed out — end run gracefully
@@ -7752,6 +7783,8 @@ class ChatMessageAccumulator {
7752
7783
  modelMessages = [];
7753
7784
  uiMessages = [];
7754
7785
  _compaction;
7786
+ /** The run a spliced head-start partial contributed, until its response replaces it. */
7787
+ _handoverRun;
7755
7788
  _pendingMessages;
7756
7789
  _steeringQueue = [];
7757
7790
  constructor(options) {
@@ -7795,7 +7828,7 @@ class ChatMessageAccumulator {
7795
7828
  * `consumeHandover` for the wait+seed+apply convenience.
7796
7829
  */
7797
7830
  applyHandover(signal) {
7798
- spliceHandoverPartial(this.modelMessages, this.uiMessages, signal);
7831
+ this._handoverRun = spliceHandoverPartial(this.modelMessages, this.uiMessages, signal);
7799
7832
  }
7800
7833
  /**
7801
7834
  * One-call `chat.headStart` handover for a custom-agent loop: waits for the
@@ -7838,8 +7871,13 @@ class ChatMessageAccumulator {
7838
7871
  if (existingIdx !== -1) {
7839
7872
  const previous = this.uiMessages[existingIdx];
7840
7873
  this.uiMessages[existingIdx] = response;
7874
+ const handoverRun = this._handoverRun && this._handoverRun.id === previous.id
7875
+ ? this._handoverRun.run
7876
+ : undefined;
7877
+ if (handoverRun)
7878
+ this._handoverRun = undefined;
7841
7879
  try {
7842
- if (!(await replaceModelRun(this.modelMessages, previous, response, 0))) {
7880
+ if (!(await replaceModelRun(this.modelMessages, previous, response, 0, handoverRun))) {
7843
7881
  this.modelMessages = await toModelMessages(this.uiMessages.map((m) => stripProviderMetadata(m)));
7844
7882
  }
7845
7883
  }
@@ -7942,8 +7980,10 @@ class ChatMessageAccumulator {
7942
7980
  }
7943
7981
  /**
7944
7982
  * Run outer-loop compaction if needed. Call after adding the response
7945
- * and capturing usage. Applies `compactModelMessages` and `compactUIMessages`
7946
- * callbacks if configured.
7983
+ * and capturing usage. Pass the LAST step's usage (`result.usage`), which is
7984
+ * the context the model held on its final call; `result.totalUsage` sums every
7985
+ * step of a tool-using turn and belongs in `context.turnUsage`. Applies
7986
+ * `compactModelMessages` and `compactUIMessages` callbacks if configured.
7947
7987
  *
7948
7988
  * @returns `true` if compaction was performed, `false` otherwise.
7949
7989
  */
@@ -7956,6 +7996,7 @@ class ChatMessageAccumulator {
7956
7996
  inputTokens: usage.inputTokens,
7957
7997
  outputTokens: usage.outputTokens,
7958
7998
  usage,
7999
+ turnUsage: context?.turnUsage,
7959
8000
  totalUsage: context?.totalUsage,
7960
8001
  chatId: context?.chatId,
7961
8002
  turn: context?.turn,
@@ -8188,8 +8229,8 @@ function createChatSession(payload, options) {
8188
8229
  idleTimeoutInSeconds: sessionIdleTimeoutOpt ?? currentPayload.idleTimeoutInSeconds ?? 30,
8189
8230
  timeout,
8190
8231
  spanName: currentPayload.trigger === "preload"
8191
- ? "waiting for first message"
8192
- : "waiting for first message (continuation)",
8232
+ ? "first message"
8233
+ : "first message (continuation)",
8193
8234
  });
8194
8235
  if (!result.ok || runSignal.aborted) {
8195
8236
  stop.cleanup();
@@ -8226,7 +8267,7 @@ function createChatSession(payload, options) {
8226
8267
  const next = await messagesInput.waitWithIdleTimeout({
8227
8268
  idleTimeoutInSeconds,
8228
8269
  timeout,
8229
- spanName: "waiting for next message",
8270
+ spanName: "next message",
8230
8271
  });
8231
8272
  if (!next.ok || runSignal.aborted) {
8232
8273
  stop.cleanup();
@@ -8437,6 +8478,7 @@ function createChatSession(payload, options) {
8437
8478
  // indefinitely, which would wedge the turn loop (same guard as
8438
8479
  // chat.agent's turn loop).
8439
8480
  let turnUsage;
8481
+ let lastStepUsage;
8440
8482
  if (typeof source.totalUsage?.then === "function") {
8441
8483
  try {
8442
8484
  const usage = (await Promise.race([
@@ -8453,14 +8495,28 @@ function createChatSession(payload, options) {
8453
8495
  /* non-fatal */
8454
8496
  }
8455
8497
  }
8498
+ const lastStepUsagePromise = source.usage;
8499
+ if (typeof lastStepUsagePromise?.then === "function") {
8500
+ try {
8501
+ lastStepUsage = (await Promise.race([
8502
+ lastStepUsagePromise,
8503
+ new Promise((r) => setTimeout(() => r(undefined), 2_000)),
8504
+ ]));
8505
+ }
8506
+ catch {
8507
+ /* non-fatal */
8508
+ }
8509
+ }
8456
8510
  // Outer-loop compaction (same logic as chat.agent)
8457
8511
  if (sessionCompaction && turnUsage && !turnObj.stopped) {
8512
+ const contextUsage = lastStepUsage ?? turnUsage;
8458
8513
  const shouldTrigger = await sessionCompaction.shouldCompact({
8459
8514
  messages: accumulator.modelMessages,
8460
- totalTokens: turnUsage.totalTokens,
8461
- inputTokens: turnUsage.inputTokens,
8462
- outputTokens: turnUsage.outputTokens,
8463
- usage: turnUsage,
8515
+ totalTokens: contextUsage.totalTokens,
8516
+ inputTokens: contextUsage.inputTokens,
8517
+ outputTokens: contextUsage.outputTokens,
8518
+ usage: contextUsage,
8519
+ turnUsage,
8464
8520
  totalUsage: cumulativeUsage,
8465
8521
  chatId: currentPayload.chatId,
8466
8522
  turn,