Skip to content

feat(chat): hand run() a streamText with the managed options already applied - #4884

Open
ericallam wants to merge 1 commit into
fix/chat-agent-accumulatorfrom
feat/chat-bound-streamtext
Open

feat(chat): hand run() a streamText with the managed options already applied#4884
ericallam wants to merge 1 commit into
fix/chat-agent-accumulatorfrom
feat/chat-bound-streamtext

Conversation

@ericallam

@ericallam ericallam commented Sep 3, 2026

Copy link
Copy Markdown
Member

Summary

Every run() had to spread chat.toStreamTextOptions(), and leaving it out dropped six things with no error: the managed prompt and its cache control, the registry-resolved model, the prompt's sampling config, telemetry, the skill tools, and the prepareStep that delivers steering, compaction and injected context.

Before:

import { chat } from "@trigger.dev/sdk/ai";
import { streamText, stepCountIs } from "ai";
import { anthropic } from "@ai-sdk/anthropic";

export const myChat = chat.agent({
  id: "my-chat",
  tools: { myTool },
  run: async ({ messages, tools, signal }) =>
    streamText({
      ...chat.toStreamTextOptions({ registry, tools }),
      model: anthropic("claude-sonnet-4-5"),
      system: "You are a helpful assistant.",
      messages,
      abortSignal: signal,
      stopWhen: stepCountIs(15),
    }),
});

After:

import { chat } from "@trigger.dev/sdk/ai";
import { stepCountIs } from "ai";
import { anthropic } from "@ai-sdk/anthropic";

export const myChat = chat.agent({
  id: "my-chat",
  system: "You are a helpful assistant.",
  registry,
  tools: { myTool },
  run: async ({ messages, tools, signal, streamText }) =>
    streamText({
      model: anthropic("claude-sonnet-4-5"),
      messages,
      tools,
      abortSignal: signal,
      stopWhen: stepCountIs(15),
    }),
});

streamText comes from run's argument and shadows the one imported from ai, so the correct call is now the shorter one and the managed options cannot be lost by omission. chat.toStreamTextOptions() is unchanged and still supported, and is still the only option in a custom agent.

What changes when your options collide with the managed ones

Spread order decides the outcome today, and losing is silent:

streamText({ ...chat.toStreamTextOptions(), tools: myTools })       // skill tools dropped
streamText({ ...chat.toStreamTextOptions(), prepareStep: mine })    // steering, compaction and injection off

The managed streamText merges instead. tools are passed into the helper so skill tools survive, and a prepareStep you pass runs after the managed one rather than replacing it. Everything else you name is left alone and wins, telemetry included.

system is the exception: it can be set on chat.agent({ system }), through chat.prompt.set(), or at the call site, but only in one of them. Two at once throws and names the one that already owns it. No shape merges two system values across every supported AI SDK version, since v5 rejects an array of blocks and a structured block carries the provider options that make prompt caching work.

onAction

A response produced from an action gets the same streamText. Before, a regenerate answered with no system prompt and no skill tools, so the replacement answer came from a differently configured model than every other turn.

Before:

import { streamText } from "ai";

onAction: async ({ action, messages }) => {
  if (action.type !== "regenerate") return;
  chat.history.slice(0, -1);
  return streamText({ model: anthropic("claude-sonnet-4-5"), messages });
},

After:

onAction: async ({ action, messages, streamText }) => {
  if (action.type !== "regenerate") return;
  chat.history.slice(0, -1);
  return streamText({ model: anthropic("claude-sonnet-4-5"), messages });
},

The only edit is the destructure. The two calls look the same and produce answers configured differently.

chat.headStart and chat.startHeadStart

buildStreamTextOptions supplies messages, stopWhen: stepCountIs(1) and abortSignal. Step 1 belongs to the route handler and step 2 onward to the agent, so re-setting stopWhen after a spread hands over a stream that has already run past step 1.

Before:

import { streamText, stepCountIs } from "ai";

export const POST = chat.headStart({
  agentId: "my-chat",
  run: async ({ chat: helper }) =>
    streamText({
      ...helper.toStreamTextOptions({ tools: headStartTools }),
      model: anthropic("claude-sonnet-4-6"),
      system: "You are a helpful assistant.",
    }),
});

After:

export const POST = chat.headStart({
  agentId: "my-chat",
  run: async ({ streamText }) =>
    streamText({
      model: anthropic("claude-sonnet-4-6"),
      system: "You are a helpful assistant.",
      tools: headStartTools,
    }),
});

Passing messages, stopWhen or abortSignal to that streamText is a type error, with a runtime throw behind it for JavaScript callers. The old shape only warned in prose.

Also in here

  • chat.agent() takes system, registry, cacheControl and systemProviderOptions, so a managed prompt's model and its cache breakpoint no longer have to be passed at the call site.
  • ChatStreamText is exported for typing a loop factored out of run.

The signature is taken from the AI SDK's own declaration:

import type { streamText as aiStreamTextSignature } from "ai";
type AiStreamTextFn = typeof aiStreamTextSignature;

The peer range spans ai v5, v6 and v7, whose options differ. typeof resolves to whichever version is installed, so generics and tool inference are the caller's own and a v8 option needs no change here.

Verification

Typecheck and the full suite pass on both ai@6.0.116 and ai@7.0.66. The option merge is a pure function so the merged object can be asserted directly, which is how experimental_telemetry being dropped was caught: most streamText options never reach the provider, so a test that observes the model cannot see them.

Run end to end against a deployed agent with every run rewritten to the new form and no spread anywhere: steering, undo across a cold boot, and regenerate all still pass, a caller's own prepareStep runs while managed steering still fires inside the turn, and consecutive injections arrive one per turn. The handover-owned options are pinned by @ts-expect-error assertions in a typechecked test rather than only by the runtime throw.

@changeset-bot

changeset-bot Bot commented Sep 3, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: f36bb10

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 27 packages
Name Type
@trigger.dev/sdk Minor
@trigger.dev/python Minor
@internal/dashboard-agent Patch
@trigger.dev/build Minor
trigger.dev Minor
@trigger.dev/core Minor
@trigger.dev/react-hooks Minor
@trigger.dev/redis-worker Minor
@trigger.dev/rsc Minor
@trigger.dev/schema-to-json Minor
@trigger.dev/database Minor
@trigger.dev/otlp-importer Minor
@trigger.dev/rbac Minor
@trigger.dev/sso Minor
@internal/clickhouse Patch
@internal/llm-model-catalog Patch
@internal/metrics-pipeline Patch
@internal/redis Patch
@internal/replication Patch
@internal/run-engine Patch
@internal/run-store Patch
@internal/schedule-engine Patch
@internal/tracing Patch
@internal/webhook-engine Patch
@internal/webhook-sources Patch
@internal/testcontainers Patch
@internal/cache Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@coderabbitai

coderabbitai Bot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

The SDK now supplies managed streamText functions to agent runs and action handlers. These functions preserve prompts, tools, telemetry, registry settings, caching options, and prepareStep behavior. Head Start handlers receive bound functions that own handover options. Runtime exports, public types, tests, release notes, and chat-agent documentation were updated.

Merge Risk: 🟡 Moderate · up to f36bb

Some supported prompt configurations can be silently ignored, and copied action or Head Start examples can fail or use incorrect context. These issues should be corrected before merging the new public API.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 22.22% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 9 functions across 6 files. (17 skipped: … Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly and concisely describes the primary API change: passing a managed streamText function to run().
Description check ✅ Passed The description provides a detailed summary, before-and-after examples, behavior details, API changes, and verification results. It does not include the template's Closes issue line, checklist, change…
Full details: Docstring Coverage

Explanation

Docstring coverage is 22.22% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 9 functions across 6 files. (17 skipped: 17 unsupported.)

Full details: Description check

Explanation

The description provides a detailed summary, before-and-after examples, behavior details, API changes, and verification results. It does not include the template's Closes issue line, checklist, changelog, or screenshots sections, but the core change and testing information are complete.

  • Fix all pre-merge checks with AI
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/chat-bound-streamtext

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Spreading chat.toStreamTextOptions() is the integration point for six things:
the managed prompt and its cache control, the resolved model, the prompt's
sampling config, telemetry, the skill tools, and the prepareStep that delivers
steering, compaction and injected context. Forgetting the spread drops all six
in silence, and spread order decides whether passing your own tools or
prepareStep clobbers the managed ones.

run() now receives a streamText with those options applied, so the managed
state cannot be lost by omission and the merge happens inside rather than at
the call site: tools go into the helper so skills survive, a caller system
becomes the base the prompt and injections append to, and a caller prepareStep
composes after the managed one instead of replacing it.

The signature is borrowed with typeof import("ai").streamText rather than
restated, so it resolves to whichever of ai v5/v6/v7 the user installed. The
runtime value rides the existing ESM/CJS shim that already isolates value
imports from ai.

PROTOTYPE. Typechecks and passes the suite on ai@6.0.116 and ai@7.0.66, but
adds a public registry option, does not settle what happens when caller and
managed system are both structured, and has no test for the composed
prepareStep.
@ericallam
ericallam force-pushed the feat/chat-bound-streamtext branch from 4984f70 to f36bb10 Compare September 3, 2026 16:58
@pkg-pr-new

pkg-pr-new Bot commented Sep 3, 2026

Copy link
Copy Markdown

Open in StackBlitz

@trigger.dev/build

npm i https://pkg.pr.new/@trigger.dev/build@f36bb10

trigger.dev

npm i https://pkg.pr.new/trigger.dev@f36bb10

@trigger.dev/core

npm i https://pkg.pr.new/@trigger.dev/core@f36bb10

@trigger.dev/python

npm i https://pkg.pr.new/@trigger.dev/python@f36bb10

@trigger.dev/react-hooks

npm i https://pkg.pr.new/@trigger.dev/react-hooks@f36bb10

@trigger.dev/redis-worker

npm i https://pkg.pr.new/@trigger.dev/redis-worker@f36bb10

@trigger.dev/rsc

npm i https://pkg.pr.new/@trigger.dev/rsc@f36bb10

@trigger.dev/schema-to-json

npm i https://pkg.pr.new/@trigger.dev/schema-to-json@f36bb10

@trigger.dev/sdk

npm i https://pkg.pr.new/@trigger.dev/sdk@f36bb10

commit: f36bb10

@ericallam

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 9

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
packages/trigger-sdk/skills/trigger-authoring-chat-agent/SKILL.md (1)

45-45: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Remove remaining direct streamText references from managed-agent examples.

  • packages/trigger-sdk/skills/trigger-authoring-chat-agent/SKILL.md#L45-L45: remove the unused streamText import from ai.
  • packages/trigger-sdk/skills/trigger-chat-agent-advanced/SKILL.md#L261-L261: destructure streamText from run before calling it.
docs/ai-chat/background-injection.mdx (1)

210-216: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Correct the obsolete manual-options guidance.

Managed streamText in an agent run already applies the agent prompt, injections, skills, and resolved tools. These sections still require chat.toStreamTextOptions() for that behavior. Keep that helper as an alternative for manual option assembly, but do not present it as required for managed streamText.

  • docs/ai-chat/background-injection.mdx#L210-L216: State that callback-provided streamText delivers system injections.
  • docs/ai-chat/migrating-from-a-route-handler.mdx#L176-L177: State that resolved tools go directly to managed streamText.
  • docs/ai-chat/patterns/skills.mdx#L120-L120: Describe automatic skill-tool injection through managed streamText.
  • docs/ai-chat/patterns/skills.mdx#L164-L164: Instruct users to pass custom tools directly to managed streamText.
  • docs/ai-chat/patterns/skills.mdx#L181-L181: Describe managed tool merging and agent-config history conversion.
docs/ai-chat/compaction.mdx (1)

48-50: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Correct the prepareStep composition contract.

Managed streamText composes caller prepareStep logic after the managed behavior. It does not let a caller override compaction or pending-message injection by property order. The current text gives users the wrong failure model.

  • docs/ai-chat/compaction.mdx#L48-L50: Replace the override statement with the composition behavior.
  • docs/ai-chat/pending-messages.mdx#L54-L54: Replace the spread-order instruction with managed prepareStep composition behavior.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Team

Run ID: d40c7915-2a3c-4603-9293-5214ccaa08df

📥 Commits

Reviewing files that changed from the base of the PR and between 692c060 and f36bb10.

📒 Files selected for processing (23)
  • .changeset/managed-streamtext-in-run.md
  • docs/ai-chat/actions.mdx
  • docs/ai-chat/anatomy.mdx
  • docs/ai-chat/backend.mdx
  • docs/ai-chat/background-injection.mdx
  • docs/ai-chat/compaction.mdx
  • docs/ai-chat/fast-starts.mdx
  • docs/ai-chat/migrating-from-a-route-handler.mdx
  • docs/ai-chat/patterns/skills.mdx
  • docs/ai-chat/pending-messages.mdx
  • docs/ai-chat/prompt-caching.mdx
  • docs/ai-chat/quick-start.mdx
  • docs/ai-chat/reference.mdx
  • docs/ai-chat/tools.mdx
  • packages/cli-v3/skills/trigger-authoring-chat-agent/SKILL.md
  • packages/trigger-sdk/skills/trigger-authoring-chat-agent/SKILL.md
  • packages/trigger-sdk/skills/trigger-chat-agent-advanced/SKILL.md
  • packages/trigger-sdk/src/imports/ai-runtime-cjs.cts
  • packages/trigger-sdk/src/imports/ai-runtime.ts
  • packages/trigger-sdk/src/v3/ai.ts
  • packages/trigger-sdk/src/v3/chat-server.test.ts
  • packages/trigger-sdk/src/v3/chat-server.ts
  • packages/trigger-sdk/test/bound-streamtext.test.ts

Included review availability: Your plan provides up to 10 included reviews per hour; 9 remain after this review.

📜 Review details
🧰 Additional context used
📓 Path-based instructions (10)
Always import from `@trigger.dev/sdk`.

📄 CodeRabbit inference engine (AGENTS.md)

Files:

  • packages/trigger-sdk/src/imports/ai-runtime.ts
  • packages/trigger-sdk/src/v3/chat-server.test.ts
  • packages/trigger-sdk/src/v3/chat-server.ts
  • packages/trigger-sdk/test/bound-streamtext.test.ts
  • packages/trigger-sdk/src/v3/ai.ts
We use vitest exclusively.

📄 CodeRabbit inference engine (AGENTS.md)

Files:

  • packages/trigger-sdk/src/v3/chat-server.test.ts
  • packages/trigger-sdk/test/bound-streamtext.test.ts
**Prefer static imports over dynamic imports.**

📄 CodeRabbit inference engine (AGENTS.md)

Files:

  • packages/trigger-sdk/src/imports/ai-runtime.ts
  • packages/trigger-sdk/src/v3/chat-server.test.ts
  • packages/trigger-sdk/src/v3/chat-server.ts
  • packages/trigger-sdk/test/bound-streamtext.test.ts
  • packages/trigger-sdk/src/v3/ai.ts
Add crumbs as you write code — not just when debugging.

📄 CodeRabbit inference engine (AGENTS.md)

Files:

  • docs/ai-chat/quick-start.mdx
  • packages/trigger-sdk/skills/trigger-chat-agent-advanced/SKILL.md
  • docs/ai-chat/actions.mdx
  • docs/ai-chat/background-injection.mdx
  • packages/trigger-sdk/skills/trigger-authoring-chat-agent/SKILL.md
  • docs/ai-chat/patterns/skills.mdx
  • docs/ai-chat/backend.mdx
  • docs/ai-chat/prompt-caching.mdx
  • docs/ai-chat/reference.mdx
  • docs/ai-chat/fast-starts.mdx
  • docs/ai-chat/migrating-from-a-route-handler.mdx
  • docs/ai-chat/anatomy.mdx
  • packages/trigger-sdk/src/imports/ai-runtime-cjs.cts
  • docs/ai-chat/tools.mdx
  • packages/cli-v3/skills/trigger-authoring-chat-agent/SKILL.md
  • packages/trigger-sdk/src/imports/ai-runtime.ts
  • docs/ai-chat/pending-messages.mdx
  • packages/trigger-sdk/src/v3/chat-server.test.ts
  • packages/trigger-sdk/src/v3/chat-server.ts
  • packages/trigger-sdk/test/bound-streamtext.test.ts
  • docs/ai-chat/compaction.mdx
  • packages/trigger-sdk/src/v3/ai.ts
In the Trigger.dev SDK (packages/trigger-sdk), prefer isomorphic code like fetch and ReadableStream instead of Node.js-specific code

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Files:

  • packages/trigger-sdk/src/imports/ai-runtime.ts
  • packages/trigger-sdk/src/v3/chat-server.test.ts
  • packages/trigger-sdk/src/v3/chat-server.ts
  • packages/trigger-sdk/test/bound-streamtext.test.ts
  • packages/trigger-sdk/src/v3/ai.ts
Use vitest for all tests in the Trigger.dev repository

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Files:

  • packages/trigger-sdk/src/v3/chat-server.test.ts
  • packages/trigger-sdk/test/bound-streamtext.test.ts
Use function declarations instead of default exports

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Files:

  • packages/trigger-sdk/src/imports/ai-runtime.ts
  • packages/trigger-sdk/src/v3/chat-server.test.ts
  • packages/trigger-sdk/src/v3/chat-server.ts
  • packages/trigger-sdk/test/bound-streamtext.test.ts
  • packages/trigger-sdk/src/v3/ai.ts
MDX documentation pages must include frontmatter with title (required), description (required), and sidebarTitle (optional) in YAML format Use Mintlify components for structured content: , , , , ,

📄 CodeRabbit inference engine (docs/CLAUDE.md)

Files:

  • docs/ai-chat/quick-start.mdx
  • docs/ai-chat/actions.mdx
  • docs/ai-chat/background-injection.mdx
  • docs/ai-chat/patterns/skills.mdx
  • docs/ai-chat/backend.mdx
  • docs/ai-chat/prompt-caching.mdx
  • docs/ai-chat/reference.mdx
  • docs/ai-chat/fast-starts.mdx
  • docs/ai-chat/migrating-from-a-route-handler.mdx
  • docs/ai-chat/anatomy.mdx
  • docs/ai-chat/tools.mdx
  • docs/ai-chat/pending-messages.mdx
  • docs/ai-chat/compaction.mdx
Use types over interfaces for TypeScript Avoid using enums; prefer string unions or const objects instead

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Files:

  • packages/trigger-sdk/src/imports/ai-runtime.ts
  • packages/trigger-sdk/src/v3/chat-server.test.ts
  • packages/trigger-sdk/src/v3/chat-server.ts
  • packages/trigger-sdk/test/bound-streamtext.test.ts
  • packages/trigger-sdk/src/v3/ai.ts
When creating or editing OTEL metrics (counters, histograms, gauges), ensure metric attributes have low cardinality by using only enums, booleans, bounded error codes, or bounded shard IDs Do not use high-cardinality attributes in OTEL metr...

📄 CodeRabbit inference engine (.cursor/rules/otel-metrics.mdc)

Files:

  • packages/trigger-sdk/src/imports/ai-runtime.ts
  • packages/trigger-sdk/src/v3/chat-server.test.ts
  • packages/trigger-sdk/src/v3/chat-server.ts
  • packages/trigger-sdk/test/bound-streamtext.test.ts
  • packages/trigger-sdk/src/v3/ai.ts
🧠 Learnings (2)
📚 Learning: 2026-06-23T13:04:21.413Z
Learnt from: carderne
Repo: triggerdotdev/trigger.dev PR: 4023
File: apps/webapp/app/services/upsertBranch.server.ts:14-18
Timestamp: 2026-06-23T13:04:21.413Z
Learning: In TypeScript, it’s valid to `import { type X }` and then use `typeof X` in a type-only position, e.g. `type Alias = z.infer<typeof X>`. The `type` modifier suppresses the runtime import, but the type checker still has the full exported type so `z.infer<typeof X>` can resolve correctly. In code reviews, don’t flag this as a TypeScript compile error as long as `typeof X` is used in a type context (e.g., with `z.infer`, `type` aliases, generics), not as a runtime value.

Applied to files:

  • packages/trigger-sdk/src/v3/chat-server.ts
📚 Learning: 2026-08-16T18:36:58.179Z
Learnt from: ericallam
Repo: triggerdotdev/trigger.dev PR: 4537
File: packages/trigger-sdk/test/normalizeKeyString.test.ts:1-2
Timestamp: 2026-08-16T18:36:58.179Z
Learning: For related SDK `chat.agent` tests in the Trigger.dev repository—including chat channels, handover, snapshot, and transport-event coverage—keep new test files under `packages/trigger-sdk/test/` rather than colocating them with the `packages/trigger-sdk/src/v3/` source files.

Applied to files:

  • packages/trigger-sdk/test/bound-streamtext.test.ts
🪛 LanguageTool
.changeset/managed-streamtext-in-run.md

[style] ~5-~5: To strengthen your wording, consider replacing the phrasal verb “leave out”.
Context: ...eady applied, so they cannot be lost by leaving out the spread: ```ts run: async ({ messag...

(OMIT_EXCLUDE)


[style] ~12-~12: ‘by accident’ might be wordy. Consider a shorter alternative.
Context: ...instead, so neither can be turned off by accident. system` can be set at the call site,...

(EN_WORDINESS_PREMIUM_BY_ACCIDENT)


[style] ~14-~14: To form a complete sentence, be sure to include a subject.
Context: ...an be turned off by accident. system can be set at the call site, on `chat.agent...

(MISSING_IT_THERE)


[grammar] ~20-~20: Ensure spelling is correct
Context: ...at.headStartandchat.startHeadStarthand theirrun` the same thing, carrying the four optio...

(QB_NEW_EN_ORTHOGRAPHY_ERROR_IDS_1)

🔇 Additional comments (14)
packages/trigger-sdk/src/imports/ai-runtime.ts (1)

23-23: LGTM!

Also applies to: 38-38

packages/trigger-sdk/test/bound-streamtext.test.ts (1)

390-422: LGTM!

packages/trigger-sdk/src/v3/chat-server.ts (1)

350-350: LGTM!

Also applies to: 437-437

packages/trigger-sdk/src/v3/chat-server.test.ts (1)

209-252: LGTM!

.changeset/managed-streamtext-in-run.md (1)

1-20: LGTM!

docs/ai-chat/actions.mdx (1)

47-47: LGTM!

Also applies to: 86-86, 112-112

docs/ai-chat/anatomy.mdx (1)

21-21: LGTM!

Also applies to: 39-41

docs/ai-chat/backend.mdx (1)

33-34: LGTM!

Also applies to: 137-137, 212-214, 235-240, 256-263, 301-301, 330-330, 343-343, 396-396, 432-437, 463-463, 481-482, 593-593, 618-618, 641-641, 652-652, 687-687, 718-718, 736-736, 756-756, 774-774, 799-800

docs/ai-chat/reference.mdx (1)

55-58: LGTM!

Also applies to: 105-105, 248-248

docs/ai-chat/tools.mdx (1)

11-11: LGTM!

Also applies to: 26-28, 83-85, 107-109, 134-137

packages/cli-v3/skills/trigger-authoring-chat-agent/SKILL.md (1)

5-5: LGTM!

Also applies to: 19-22, 26-36, 40-42

packages/trigger-sdk/skills/trigger-authoring-chat-agent/SKILL.md (1)

5-6: LGTM!

Also applies to: 50-52, 125-137, 162-166, 207-208, 237-238, 247-259, 268-269

packages/trigger-sdk/skills/trigger-chat-agent-advanced/SKILL.md (2)

163-166: LGTM!

Also applies to: 171-175, 193-200, 213-214, 234-241, 268-271


146-150: 🔒 Security & Privacy

Avoid injecting untrusted analysis as system instructions.

If analyzeConversation(messages) can return user-influenced text, do not inject its free-form output with role: "system". Use an untrusted message or validate a structured, allowlisted result first.

Comment thread docs/ai-chat/actions.mdx

```ts
onAction: async ({ action, messages }) => {
onAction: async ({ action, messages, streamText }) => {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

🔎 Supported by static analysis

🤖 get_repo_knowledge executed:

get_repo_knowledge triggerdotdev/trigger.dev /tmp/coderabbit-repo-knowledge/triggerdotdev-trigger-dev-0bdd0019/learnings

Length of output: 19367


🏁 Script executed:

#!/bin/bash
set -eu
file="$(git ls-files | rg '(^|/)docs/ai-chat/actions\.mdx$' | head -n 1)"
printf '%s\n' "FILE=$file"
cat -n "$file" | sed -n '1,145p'
printf '\n-- related symbols and examples --\n'
rg -n -C 4 'onAction|chat\.history|streamText|regenerate|messages' "$file"
printf '\n-- package and SDK references --\n'
rg -n -C 3 'onAction|streamText|chat\.history' --glob '!docs/ai-chat/actions.mdx' --glob '*.{ts,tsx,js,jsx,mts,md,mdx,json}' | head -n 240

Repository: triggerdotdev/trigger.dev

Length of output: 16202


🏁 Script executed:

#!/bin/bash
set -eu
printf '%s\n' '-- candidate files --'
git ls-files | rg '(ai-chat|chat|AgentChat|agent)' | head -n 240
printf '\n%s\n' '-- callback construction and history APIs --'
rg -n -C 5 'onAction|history\.slice|class .*History|function .*History|ChatHistory|action.*messages|messages.*action' --glob '*.{ts,tsx,js,jsx,mts,md,mdx}' . | head -n 320

Repository: triggerdotdev/trigger.dev

Length of output: 41273


🏁 Script executed:

#!/bin/bash
set -eu
file=packages/trigger-sdk/src/v3/ai.ts
printf '%s\n' '-- action-related declarations and callback invocation --'
rg -n -C 8 'type .*Action|interface .*Action|onAction|actionSchema|chatHistory|chatCurrentUIMessagesKey|chatOverrideMessagesKey|apply.*override|overrideMessages' "$file" | head -n 420
printf '\n%s\n' '-- relevant implementation ranges --'
cat -n "$file" | sed -n '2840,2910p;3145,3275p;3400,3525p'
printf '\n%s\n' '-- exact onAction references in SDK --'
rg -n -C 12 'onAction' packages/trigger-sdk/src --glob '*.{ts,tsx}'

Repository: triggerdotdev/trigger.dev

Length of output: 50384


🏁 Script executed:

#!/bin/bash
set -eu
file=packages/trigger-sdk/src/v3/ai.ts
printf '%s\n' '-- ActionEvent type --'
cat -n "$file" | sed -n '5495,5565p'
printf '\n%s\n' '-- action callback, override application, and response handling --'
cat -n "$file" | sed -n '7880,8075p'
printf '\n%s\n' '-- message conversion and public chat helpers --'
rg -n -C 5 'toModelMessages|modelMessages|toStreamTextOptions|messages:' "$file" | head -n 260

Repository: triggerdotdev/trigger.dev

Length of output: 26219


🏁 Script executed:

#!/bin/bash
set -eu
file=packages/trigger-sdk/src/v3/ai.ts
printf '%s\n' '-- action result handling and snapshot write --'
cat -n "$file" | sed -n '8080,8235p;6855,6935p'
printf '\n%s\n' '-- stream result capture helpers --'
rg -n -C 10 'actionStreamResult|StreamTextResult|pipeAndCapture|writeSnapshotOutsideTurn|accumulatedMessages|turnNewModelMessages' "$file" | head -n 360

Repository: triggerdotdev/trigger.dev

Length of output: 31187


Use post-mutation model messages for regeneration.

The runtime passes messages to onAction before applying chat.history.slice(). The examples therefore send the removed assistant message as model context. Rebuild the model messages from the post-mutation history in all three examples.

Comment thread docs/ai-chat/backend.mdx
export const simpleChat = chat.agent({
id: "simple-chat",
run: async ({ messages, signal }) => {
run: async ({ messages, signal, streamText }) => {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Pass the managed streamText into runAgentLoop.

streamText is scoped to the run callback, but runAgentLoop calls it without receiving it. The example no longer has a direct import, so copied code fails with an out-of-scope reference.

Proposed fix
-    await runAgentLoop(messages);
+    await runAgentLoop(messages, streamText);

-async function runAgentLoop(messages: ModelMessage[]) {
+async function runAgentLoop(messages: ModelMessage[], streamText: ChatStreamText) {

Comment thread docs/ai-chat/backend.mdx
Comment on lines +273 to +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.

| Form | Use when |
|---|---|
| `chat.toStreamTextOptions()` | Default. Wires up `prepareStep` (compaction, steering, background injection), the stored prompt's `system` / `model` / `config`, and telemetry metadata. |
| `chat.toStreamTextOptions({ registry })` | You're using [Prompts](/ai/prompts) with a provider-prefixed model string (e.g. `"anthropic:claude-sonnet-4-5"`). The registry resolves the prefix to a real model instance via `createProviderRegistry({ anthropic, openai, ... })`. |
| `chat.toStreamTextOptions({ tools })` | You want HITL tool approvals — pass the same `tools` object you give to `streamText`. The SDK then knows which tool calls need to pause on `needsApproval: true`. |
| `chat.toStreamTextOptions({ registry, tools })` | Both of the above. |
`chat.toStreamTextOptions()` remains available for the same job, and is the only option in a [custom agent](#custom-agents) or a `chat.headStart` route, where there is no `run` argument to take it from. 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`.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Align all authoring guidance with managed streamText. These sections still describe chat.toStreamTextOptions() as the primary path or incorrectly say that Head Start has no run argument.

  • docs/ai-chat/backend.mdx#L273-L275: state that Head Start receives managed streamText, and document the special tools and prepareStep merge behavior.
  • docs/ai-chat/tools.mdx#L48-L52: make the run-provided managed callback the canonical pattern for chat.agent().
  • docs/ai-chat/tools.mdx#L161-L163: describe chat.toStreamTextOptions() as the manual/custom-agent alternative.
  • packages/cli-v3/skills/trigger-authoring-chat-agent/SKILL.md#L37-L38: remove the claim that Head Start lacks a run payload.
  • packages/trigger-sdk/skills/trigger-authoring-chat-agent/SKILL.md#L37-L38: remove the same claim.
  • packages/trigger-sdk/skills/trigger-authoring-chat-agent/SKILL.md#L263-L264: keep the helper guidance only for routes without managed streamText.
📍 Affects 4 files
  • docs/ai-chat/backend.mdx#L273-L275 (this comment)
  • docs/ai-chat/tools.mdx#L48-L52
  • docs/ai-chat/tools.mdx#L161-L163
  • packages/cli-v3/skills/trigger-authoring-chat-agent/SKILL.md#L37-L38
  • packages/trigger-sdk/skills/trigger-authoring-chat-agent/SKILL.md#L37-L38
  • packages/trigger-sdk/skills/trigger-authoring-chat-agent/SKILL.md#L263-L264


<Warning>
Omitting `...chat.toStreamTextOptions()` throws no errorcompaction, steering, and background injection just silently never run. Spread it first so any explicit override you write after it takes precedence.
Importing `streamText` from `ai` instead throws no error: compaction, steering and background injection never run. Spreading `chat.toStreamTextOptions()` into the imported one is the equivalent, and is what a `chat.headStart` route has to do, since it has no `run` argument.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Document the managed Head Start callback contract.

chat.headStart and chat.startHeadStart pass a bound streamText function to run. It owns messages, stopWhen, and abortSignal. These sections still say that Head Start has no run argument or require manual helper spreading.

  • docs/ai-chat/migrating-from-a-route-handler.mdx#L169-L169: Remove the claim that a Head Start route has no run argument.
  • docs/ai-chat/fast-starts.mdx#L254-L254: Make callback-provided streamText the documented default.
  • docs/ai-chat/fast-starts.mdx#L637-L652: Add streamText to the callback arguments and describe its owned options.
📍 Affects 2 files
  • docs/ai-chat/migrating-from-a-route-handler.mdx#L169-L169 (this comment)
  • docs/ai-chat/fast-starts.mdx#L254-L254
  • docs/ai-chat/fast-starts.mdx#L637-L652

Comment on lines +4798 to +4805
if (callerSystem !== undefined && managedSystem) {
throw new Error(
"chat.agent: `system` is already set " +
(promptSystem ? "by chat.prompt.set()" : "on chat.agent({ system })") +
", so it cannot also be passed to the `streamText` given to run(). Set it in one place, and add " +
"per-turn context with chat.inject({ role: 'system' }) rather than a second system value."
);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

The conflict check misses the agent + chat.prompt.set() combination.

The documentation for ChatAgentOptions.system (Line 6209) states the system prompt may be set on the agent, at the call site, or through chat.prompt.set(), but only in one of them. The runtime check enforces this only when callerSystem is set. When an agent sets both system and chat.prompt.set(), no error is raised. toStreamTextOptions then resolves promptText = prompt?.text || baseSystemText (Line 4854), so the prompt wins and chat.agent({ system }) is discarded without any signal.

This is the same silent-drop failure the seam is meant to prevent. Either throw for that pair as well, or change the documentation to state the precedence.

🛠️ Proposed fix to enforce the documented rule
   const promptSystem = locals.get(chatPromptKey)?.text;
   const managedSystem = promptSystem || agentSystem;
   if (callerSystem !== undefined && managedSystem) {
     throw new Error(
       "chat.agent: `system` is already set " +
         (promptSystem ? "by chat.prompt.set()" : "on chat.agent({ system })") +
         ", so it cannot also be passed to the `streamText` given to run(). Set it in one place, and add " +
         "per-turn context with chat.inject({ role: 'system' }) rather than a second system value."
     );
   }
+  if (promptSystem && agentSystem) {
+    throw new Error(
+      "chat.agent: `system` is set both on chat.agent({ system }) and by chat.prompt.set(). " +
+        "Set it in one place, and add per-turn context with chat.inject({ role: 'system' })."
+    );
+  }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (callerSystem !== undefined && managedSystem) {
throw new Error(
"chat.agent: `system` is already set " +
(promptSystem ? "by chat.prompt.set()" : "on chat.agent({ system })") +
", so it cannot also be passed to the `streamText` given to run(). Set it in one place, and add " +
"per-turn context with chat.inject({ role: 'system' }) rather than a second system value."
);
}
if (callerSystem !== undefined && managedSystem) {
throw new Error(
"chat.agent: `system` is already set " +
(promptSystem ? "by chat.prompt.set()" : "on chat.agent({ system })") +
", so it cannot also be passed to the `streamText` given to run(). Set it in one place, and add " +
"per-turn context with chat.inject({ role: 'system' }) rather than a second system value."
);
}
if (promptSystem && agentSystem) {
throw new Error(
"chat.agent: `system` is set both on chat.agent({ system }) and by chat.prompt.set(). " +
"Set it in one place, and add per-turn context with chat.inject({ role: 'system' })."
);
}

Comment on lines +4847 to +4854
const baseSystem = options?.system;
const baseSystemText =
typeof baseSystem === "string"
? baseSystem
: typeof baseSystem?.content === "string"
? baseSystem.content
: "";
const promptText = prompt?.text || baseSystemText;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚀 Performance & Scalability | 🟡 Minor | ⚡ Quick win

Preserve providerOptions from structured system messages.

ChatAgentOptions.system and ToStreamTextOptionsOptions.system accept a complete SystemModelMessage, but toStreamTextOptions rebuilds it from only content. On the agent streamText path, this drops message-level cache settings when no higher-precedence option is set. Add baseSystem.providerOptions after the existing chat.prompt.set() provider options. Otherwise, an Anthropic cache breakpoint is lost and the system prompt is sent without caching.

Comment on lines +129 to +134
type HeadStartStreamTextFn = (
options: Omit<Parameters<AiStreamTextFn>[0], "messages" | "prompt" | "stopWhen" | "abortSignal">
) => ReturnType<AiStreamTextFn>;

/** The keys `buildStreamTextOptions` owns. Overriding any of them breaks handover. */
const HEAD_START_OWNED_OPTIONS = ["messages", "stopWhen", "abortSignal"] as const;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

HEAD_START_OWNED_OPTIONS omits "prompt", so the runtime guard and the type disagree.

HeadStartStreamTextFn omits four keys: messages, prompt, stopWhen, and abortSignal. HEAD_START_OWNED_OPTIONS lists only three. prompt is missing.

Because prompt is not filtered, it stays in rest and is spread onto the built options at Line 155, so aiStreamText receives both the managed messages and the caller's prompt. The AI SDK rejects that pair during prompt standardization, so the caller gets an opaque SDK error instead of the guiding message. Any caller that reaches the function from JavaScript, or through a cast, bypasses the documented behavior. The doc at Lines 176-178 promises that passing one of these options throws, and its own wording is inconsistent with the constant — "the four options" at Line 169 against "the three it owns" at Line 178.

Add "prompt" to the constant and align the comment.

🐛 Proposed fix
 /** The keys `buildStreamTextOptions` owns. Overriding any of them breaks handover. */
-const HEAD_START_OWNED_OPTIONS = ["messages", "stopWhen", "abortSignal"] as const;
+const HEAD_START_OWNED_OPTIONS = ["messages", "prompt", "stopWhen", "abortSignal"] as const;

Extend the error text so prompt is explained:

-          "them: `messages` is the converted wire payload, `stopWhen: stepCountIs(1)` stops after " +
+          "them: `messages` is the converted wire payload (so `prompt` cannot be set either), " +
+          "`stopWhen: stepCountIs(1)` stops after " +

And correct the count in the HeadStartRunArgs.streamText doc:

-   * instead, and note that the three it owns are a type error, not just a
+   * instead, and note that the four it owns are a type error, not just a
    * runtime one.
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
type HeadStartStreamTextFn = (
options: Omit<Parameters<AiStreamTextFn>[0], "messages" | "prompt" | "stopWhen" | "abortSignal">
) => ReturnType<AiStreamTextFn>;
/** The keys `buildStreamTextOptions` owns. Overriding any of them breaks handover. */
const HEAD_START_OWNED_OPTIONS = ["messages", "stopWhen", "abortSignal"] as const;
type HeadStartStreamTextFn = (
options: Omit<Parameters<AiStreamTextFn>[0], "messages" | "prompt" | "stopWhen" | "abortSignal">
) => ReturnType<AiStreamTextFn>;
/** The keys `buildStreamTextOptions` owns. Overriding any of them breaks handover. */
const HEAD_START_OWNED_OPTIONS = ["messages", "prompt", "stopWhen", "abortSignal"] as const;

Comment on lines +117 to +119
await new Promise((r) => setTimeout(r, 40));
await harness.sendMessage({ id: "u2", role: "user", parts: [{ type: "text", text: "two" }] });
await new Promise((r) => setTimeout(r, 40));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Poll for model.doStreamCalls before reading the model call

chat.headStart returns the response while stream consumption and handoverWhenDone continue in the background. The fixed 200 ms delay does not guarantee that model.doStreamCalls contains an entry, so .at(-1)! can fail on a loaded runner. Replace the delay with a bounded poll for model.doStreamCalls.length > 0. The existing waitFor helper is file-local, so define or share an equivalent helper in chat-server.test.ts.

Comment on lines +147 to +161
const agent = chat.agent({
id: "bound-streamtext-tools",
tools: { agentTool },
run: async ({ messages, signal, streamText }) =>
streamText({ model, messages, abortSignal: signal, tools: { callerTool } }),
});

const harness = mockChatAgent(agent, { chatId: "bound-streamtext-tools" });

try {
await harness.sendMessage({ id: "u1", role: "user", parts: [{ type: "text", text: "hi" }] });
await new Promise((r) => setTimeout(r, 40));

const names = (model.doStreamCalls.at(-1)!.tools ?? []).map((t) => t.name).sort();
expect(names).toContain("callerTool");

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Cover the skill-tool merge path. chat.agent({ tools }) exposes tools on ChatTaskRunPayload; buildManagedStreamTextOptions does not inject them automatically. The test passes only callerTool, so its agentTool declaration is not exercised. Because no skills are configured, a regression in toStreamTextOptions can also pass. Set a resolved skill with chat.skills.set(...), pass the payload tools when testing agentTool, and assert agentTool, callerTool, loadSkill, readFile, and bash. Rename the test to match.

@ericallam
ericallam marked this pull request as ready for review September 3, 2026 21:47

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 2 potential issues.

Devin Review

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔍 New tests bypass the no-mocking rule

The repository bans mocks, but this suite replaces @trigger.dev/core/v3 with vi.mock. Rewrite the coverage using the approved test infrastructure.

(Refers to this code)

Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +4847 to +4853
const baseSystem = options?.system;
const baseSystemText =
typeof baseSystem === "string"
? baseSystem
: typeof baseSystem?.content === "string"
? baseSystem.content
: "";

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Structured system settings are discarded

When either system source is structured, toStreamTextOptions keeps only its text and discards its providerOptions. Block-level caching and provider settings silently stop applying.

Prompt for agents
Preserve providerOptions from a structured system value passed through either chat.agent({ system }) or the managed streamText call. In packages/trigger-sdk/src/v3/ai.ts, toStreamTextOptions currently extracts only baseSystem.content and reconstructs the system message from separately configured cache options. Incorporate the structured message's providerOptions into the documented precedence without losing explicit systemProviderOptions, cacheControl, or chat.prompt.set() provider options. Add coverage for both agent-level and call-site structured system messages.
Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant