Skip to content

Commit bd7cc35

Browse files
ericallamTrigger.dev RepoOps
authored andcommitted
fix(sdk): trace model steps and tool calls in chat.agent by default
`chat.agent` now traces each model step and tool call by default: the `streamText` passed to `run()` turns AI SDK telemetry on even when no prompt is stored with `chat.prompt.set()`. An agent with `telemetry: { recordUserMessage: false }` gets these spans without prompt and response content, including when it stores a prompt (whose variables are left out of the span metadata). Pass `experimental_telemetry: { isEnabled: false }` to turn them off, or `recordInputs` / `recordOutputs: false` to keep them without content. Mono-RevId: 8c1936ba08de8d3b6199a5d9e23384440b81f7d5
1 parent 7ee436b commit bd7cc35

5 files changed

Lines changed: 118 additions & 7 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@trigger.dev/sdk": patch
3+
---
4+
5+
The `streamText` passed to a `chat.agent`'s `run()` now turns AI SDK telemetry on by default, so each model step and tool call shows up as a span in the run trace even when the agent doesn't store a prompt with `chat.prompt.set()`. An agent with `telemetry: { recordUserMessage: false }` gets these spans without prompt and response content, including when it stores a prompt (whose variables are left out of the span metadata). Pass `experimental_telemetry: { isEnabled: false }` at the call site to turn them off, or `recordInputs: false` / `recordOutputs: false` to keep them without content.

‎docs/ai-chat/backend.mdx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -272,7 +272,7 @@ export const myChat = chat.agent({
272272
});
273273
```
274274

275-
The managed `streamText` carries the stored prompt's `system`, `model` (resolved through the agent's `registry`), sampling config, and `experimental_telemetry`. Options you pass at the call site win, apart from `system`, which throws when the prompt already set one.
275+
The managed `streamText` carries the stored prompt's `system`, `model` (resolved through the agent's `registry`), sampling config, and `experimental_telemetry`. AI SDK telemetry is on even without a stored prompt, so model steps and tool calls show in the run trace. Options you pass at the call site win, apart from `system`, which throws when the prompt already set one.
276276

277277
`chat.toStreamTextOptions()` remains available for the same job, and is the only option in a [custom agent](#custom-agents), which has no `run` argument to take a bound `streamText` from. A `chat.headStart` route does get one, and there it also owns `messages`, `stopWhen` and `abortSignal`, since the handover depends on them. Pass `{ registry }` when a prompt names a provider-prefixed model, and `{ tools }` when you want HITL tool approvals, so the SDK knows which calls pause on `needsApproval`.
278278

‎docs/ai-chat/patterns/redacting-telemetry.mdx‎

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ sidebarTitle: "Redact chat content"
44
description: "Stop a chat agent recording user messages and clientData on its spans, turn off AI SDK prompt and response recording, or redact span content with a span processor before it leaves the worker."
55
---
66

7-
A chat agent's run trace records conversation content by default: the user's message and the `clientData` your frontend sends. When AI SDK telemetry is on, it also records the prompts and responses of each model call. This page covers the three controls that change that, and what they don't cover.
7+
A chat agent's run trace records conversation content by default: the user's message, the `clientData` your frontend sends, and, through AI SDK telemetry, the prompts and responses of each model call. This page covers the three controls that change that, and what they don't cover.
88

99
## What the run trace records
1010

@@ -13,7 +13,7 @@ A chat agent's run trace records conversation content by default: the user's mes
1313
| `chat turn N` | The last user message (`chat.user_message`) and its first 80 characters as the span preview | `telemetry.recordUserMessage` |
1414
| `chat turn N` | The raw `clientData` (`chat.client_data`) | `telemetry.recordClientData` |
1515
| `pending message injected` | Messages sent mid-turn (`pending.messages`) | `telemetry.recordUserMessage` |
16-
| AI SDK model call spans, when AI SDK telemetry is on | Prompt messages, response text, tool calls | `experimental_telemetry` on the model call |
16+
| AI SDK model call spans (on by default with the `streamText` from `run`) | Prompt messages, response text, tool calls | `telemetry.recordUserMessage` for the default config, or `experimental_telemetry` on the model call |
1717
| Any span | Everything above, plus your own attributes and span events | A span processor |
1818

1919
## Stop recording chat content
@@ -32,11 +32,11 @@ export const supportAgent = chat.agent({
3232
});
3333
```
3434

35-
The spans still appear in the trace with their timings and token usage. The turn spans have no preview text.
35+
The spans still appear in the trace with their timings and token usage. The turn spans have no preview text. `recordUserMessage: false` also keeps prompts and responses out of the AI SDK spans that the `streamText` passed to `run()` turns on, with or without a prompt stored by `chat.prompt.set()`, and leaves a stored prompt's variables out of the span metadata, unless the call passes its own `experimental_telemetry`.
3636

3737
## Stop the AI SDK recording prompts and responses
3838

39-
AI SDK model call spans appear when AI SDK telemetry is on: [`chat.prompt.set()`](/ai-chat/backend#using-prompts) turns it on for you, or you pass `experimental_telemetry` yourself. Those spans include prompts and responses unless you turn that off with `recordInputs` and `recordOutputs`:
39+
The `streamText` passed to `run()` turns AI SDK telemetry on, so each model call and tool call is a span in the run trace (pass `experimental_telemetry: { isEnabled: false }` to turn the spans off). If you call `streamText` from `ai` yourself, pass `experimental_telemetry` to get them. Those spans include prompts and responses unless you turn that off with `recordInputs` and `recordOutputs`:
4040

4141
```ts /trigger/support-agent.ts
4242
import { chat } from "@trigger.dev/sdk/ai";
@@ -50,7 +50,7 @@ export const supportAgent = chat.agent({
5050
model: anthropic("claude-sonnet-4-5"),
5151
messages,
5252
abortSignal: signal,
53-
// Replaces any telemetry config from chat.prompt.set(), so keep isEnabled on
53+
// Replaces the default telemetry config (and chat.prompt.set()'s), so keep isEnabled on
5454
experimental_telemetry: { isEnabled: true, recordInputs: false, recordOutputs: false },
5555
}),
5656
});

‎packages/trigger-sdk/src/v3/ai.ts‎

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5282,6 +5282,25 @@ function toStreamTextOptions(
52825282
Object.assign(result, telemetry);
52835283
}
52845284

5285+
const telemetry = (result.experimental_telemetry as Record<string, unknown> | undefined) ?? {
5286+
isEnabled: true,
5287+
metadata: { ...(options?.telemetry ?? {}) },
5288+
};
5289+
if (locals.get(chatTurnContextKey)?.recordUserMessage === false) {
5290+
const { "prompt.input": _input, ...metadata } = (telemetry.metadata ?? {}) as Record<
5291+
string,
5292+
unknown
5293+
>;
5294+
result.experimental_telemetry = {
5295+
...telemetry,
5296+
metadata,
5297+
recordInputs: false,
5298+
recordOutputs: false,
5299+
};
5300+
} else {
5301+
result.experimental_telemetry = telemetry;
5302+
}
5303+
52855304
// Skills: merge auto-injected tools with any user-provided tools.
52865305
// User tools override on name conflict (though we namespace ours).
52875306
if (skills && skills.length > 0) {
@@ -16873,7 +16892,8 @@ export const chat = {
1687316892
/**
1687416893
* Returns an options object ready to spread into `streamText()`.
1687516894
* Reads the stored prompt and returns `{ system, experimental_telemetry, ...config }`.
16876-
* Returns `{}` if no prompt has been set.
16895+
* With no stored prompt it still returns `experimental_telemetry` (on), so the model call is
16896+
* traced.
1687716897
*/
1687816898
toStreamTextOptions,
1687916899
/**

‎packages/trigger-sdk/test/bound-streamtext.test.ts‎

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { mockChatAgent } from "../src/v3/test/index.js";
55
import { describe, expect, it } from "vitest";
66
import { chat, __buildManagedStreamTextOptionsForTests as buildManaged } from "../src/v3/ai.js";
77
import { chat as chatServer } from "../src/v3/chat-server.js";
8+
import { definePrompt } from "../src/v3/prompt.js";
89
import { simulateReadableStream, stepCountIs, tool } from "ai";
910
import { MockLanguageModelV3 } from "ai/test";
1011
import type { LanguageModelV3StreamPart } from "@ai-sdk/provider";
@@ -401,6 +402,91 @@ describe("what the managed streamText passes through", () => {
401402
expect(merged.experimental_telemetry).toBe(callerTelemetry);
402403
});
403404

405+
it("turns AI SDK telemetry on when no prompt is stored, so model steps and tool calls are traced", () => {
406+
const merged = buildManaged({ model: "m" }, {});
407+
expect(merged.experimental_telemetry).toEqual({ isEnabled: true, metadata: {} });
408+
});
409+
410+
it.each([
411+
{ name: "default telemetry", key: "default", prompt: undefined, metadata: {} },
412+
{
413+
name: "stored prompt telemetry",
414+
key: "string-prompt",
415+
prompt: async () => "You are terse.",
416+
metadata: {},
417+
},
418+
{
419+
name: "resolved prompt telemetry and its variables",
420+
key: "resolved-prompt",
421+
prompt: () =>
422+
definePrompt({
423+
id: "bound-streamtext-redacted",
424+
content: "Help {{name}}.",
425+
}).resolve({ name: "secret" }),
426+
metadata: {
427+
"prompt.slug": "bound-streamtext-redacted",
428+
"prompt.id": "bound-streamtext-redacted",
429+
"prompt.version": "0",
430+
"prompt.labels": "local",
431+
},
432+
},
433+
])(
434+
"keeps prompt and response content out of $name when recordUserMessage is off",
435+
async ({ key, prompt, metadata }) => {
436+
const stored = await prompt?.();
437+
const seen: unknown[] = [];
438+
const model = new MockLanguageModelV3({
439+
doStream: async () => ({
440+
stream: simulateReadableStream({
441+
chunks: [
442+
{ type: "text-start", id: "t" },
443+
{ type: "text-delta", id: "t", delta: "ok" },
444+
{ type: "text-end", id: "t" },
445+
{
446+
type: "finish",
447+
finishReason: { unified: "stop", raw: "stop" },
448+
usage: {
449+
inputTokens: { total: 1, noCache: 1, cacheRead: 0, cacheWrite: 0 },
450+
outputTokens: { total: 1, text: 1, reasoning: 0 },
451+
},
452+
},
453+
] as LanguageModelV3StreamPart[],
454+
}),
455+
}),
456+
});
457+
const agent = chat.agent({
458+
id: `bound-streamtext.redacted-telemetry-${key}`,
459+
telemetry: { recordUserMessage: false },
460+
run: async ({ messages, signal, streamText }) => {
461+
if (stored) chat.prompt.set(stored);
462+
seen.push(chat.toStreamTextOptions().experimental_telemetry);
463+
return streamText({ model, messages, abortSignal: signal });
464+
},
465+
});
466+
const harness = mockChatAgent(agent, { chatId: `bound-streamtext-redacted-${key}` });
467+
try {
468+
await harness.sendMessage({
469+
id: "u1",
470+
role: "user",
471+
parts: [{ type: "text", text: "secret" }],
472+
});
473+
expect(seen[0]).toEqual({
474+
isEnabled: true,
475+
metadata,
476+
recordInputs: false,
477+
recordOutputs: false,
478+
});
479+
} finally {
480+
await harness.close();
481+
}
482+
}
483+
);
484+
485+
it("lets a caller turn telemetry off", () => {
486+
const merged = buildManaged({ model: "m", experimental_telemetry: { isEnabled: false } }, {});
487+
expect(merged.experimental_telemetry).toEqual({ isEnabled: false });
488+
});
489+
404490
it("carries the agent's system when the caller names none", () => {
405491
const merged = buildManaged({ model: "m" }, { system: "AGENT-SYSTEM" });
406492
expect(JSON.stringify(merged.system)).toContain("AGENT-SYSTEM");

0 commit comments

Comments
 (0)