Skip to content

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

Merged
ericallam merged 46 commits into
mainfrom
feat/chat-bound-streamtext
Sep 7, 2026
Merged

ericallam merged 46 commits into
mainfrom
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.

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, prompt, stopWhen or abortSignal to that streamText is a type error, with a runtime throw behind it for JavaScript callers. tools is yours to pass. 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.

Actions. onAction no longer receives streamText or tools: an action is a state edit, and one that returns chat.turn() (added in #4816) is followed by run(), which already has both. The action docs on this branch describe that model. chat.toStreamTextOptions() now also applies chat.agent's system, registry, cacheControl and systemProviderOptions, so the spread form is equivalent to the streamText handed to run(), as the docs say; previously an agent's system prompt was silently dropped on that path. Those options are published on every boot, including for a hydrateMessages agent, which skips the snapshot boot block where they were first set.

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.

Loading
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.

2 participants