Skip to content
5 changes: 5 additions & 0 deletions .changeset/action-stream-into-conversation.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@trigger.dev/sdk": patch
---

A response streamed back from `onAction` is now part of the conversation. Returning a `StreamTextResult` from an action sent it to the browser and nowhere else, so a regenerate showed the user a new answer that the model had no memory of — the next turn carried on from the answer that had just been replaced.
5 changes: 5 additions & 0 deletions .changeset/inject-instructions-shape.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@trigger.dev/sdk": patch
---

Injected system context is merged into a single instruction block, so it works on every supported AI SDK version. Note that a cached system prompt gives up its cache entry for as long as an injection is live, since the cached prefix has changed.
5 changes: 5 additions & 0 deletions .changeset/inject-system-to-instructions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@trigger.dev/sdk": patch
---

`chat.inject()` with `role: "system"` now works. It previously put the system message into the conversation, which AI SDK 7 rejects for every provider — the next turn died with a generic "An error occurred." and persisted an empty assistant message, so the agent looked like it had simply stopped answering. System-role context is now appended to the model's instructions, which is also the only way to inject context the agent will treat as trusted.
5 changes: 5 additions & 0 deletions .changeset/persist-action-history-mutations.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@trigger.dev/sdk": patch
---

Undo, edit and regenerate now survive a run ending. History rolled back from `onAction` was only kept in the running worker's memory, so the rollback held while that worker stayed warm and then reverted on the next continuation — the undone messages came back, minutes later, with no error.
5 changes: 5 additions & 0 deletions .changeset/steering-messages-accumulator.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@trigger.dev/sdk": patch
---

Steering messages injected mid-answer are now part of the conversation your hooks see. Previously they reached the model and the browser but not `onTurnComplete`, so an app storing its own transcript lost the instruction the answer was shaped by — it vanished from the conversation on reload, and later turns had no record of it.
55 changes: 54 additions & 1 deletion docs/ai-chat/background-injection.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -189,9 +189,62 @@ export const myChat = chat.agent({
| **Source** | Backend task code | Frontend user input |
| **Triggered by** | Your code (e.g. `onTurnComplete` + `chat.defer()`) | User sending a message during streaming |
| **Injection point** | Start of next turn, or next `prepareStep` boundary | Next `prepareStep` boundary only |
| **Message role** | Any (`system`, `user`, `assistant`) | Typically `user` |
| **Message role** | Any `system` becomes an instruction, others join the conversation (see below) | Typically `user` |
| **Frontend visibility** | Not visible unless you write custom `data-*` chunks | Visible via `usePendingMessages` hook |

## Two lanes: trusted and untrusted

The role you inject with decides more than position — it decides whether the model
treats the content as trustworthy.

**`role: "system"` goes to the instructions lane.** The block is appended to the
system instructions for subsequent inference calls, so it carries the same standing
as your system prompt. This is the lane for context the agent should simply believe:
entitlements, plan changes, operational notices.

It has to work this way. On AI SDK 7 a system message inside `messages` is rejected
for every provider — `standardizePrompt` throws before any provider is called, and
its own advice is to use the instructions option. `Instructions` accepts
`Array<SystemModelMessage>`, so the injected block is appended there rather than
smuggled into the transcript.

<Warning>
The instructions lane is delivered by `chat.toStreamTextOptions()`, because that
is the only place the SDK can set `streamText`'s instructions for you. If your
`run()` calls `streamText({ model, messages, abortSignal })` without spreading
`chat.toStreamTextOptions()`, a `role: "system"` injection never reaches the
model. The conversational lane has no such requirement — it arrives through
`messages` either way.
</Warning>

Two things worth knowing:

- An injection applies to the next inference call only. The lane is drained once
applied, so a block injected in `onTurnComplete` shapes the following turn and is
not repeated on every turn after it.
- A new instruction block changes the cached prefix, so the first call carrying it
misses the prompt cache. Only the turns where something was actually injected pay
that.
- The injected text is merged into a single instruction rather than added as a
second block, because AI SDK 5 rejects an array of system blocks while accepting
one structured block. That means a cached system prompt loses its cache entry for
as long as an injection is live — the prefix changed, so there is nothing to hit.
If you rely on prompt caching, inject sparingly and prefer facts that go stale, so
the injection clears.

**Any other role joins the conversation, and is untrusted by construction.** A
message injected as `user` is indistinguishable from something the user typed, and a
well-aligned model treats it accordingly — it may say so and re-derive the answer
from tools instead of taking it at face value:

> "that text arrived embedded in your message, not from a tool I called, so I
> verified it myself rather than trusting it"

That is correct behaviour, not a bug. So inject **checkable facts** in the
conversational lane and put **directives** in the instructions lane. A conclusion
injected as a user message is the worst of both: the model neither trusts it nor
ignores it, and may contradict it in front of the user.

## API reference

### chat.inject()
Expand Down
Loading
Loading