@@ -500,6 +500,13 @@ const stopInput = streams.input<{ stop: true; message?: string }>({ id: CHAT_STO
500500 */
501501const chatDeferKey = locals . create < Set < Promise < unknown > > > ( "chat.defer" ) ;
502502
503+ /**
504+ * Per-turn background context queue. Messages added via `chat.backgroundWork.inject()`
505+ * are drained at the next `prepareStep` boundary and appended to the model messages.
506+ * @internal
507+ */
508+ const chatBackgroundQueueKey = locals . create < ModelMessage [ ] > ( "chat.backgroundQueue" ) ;
509+
503510/**
504511 * Run-scoped pipe counter. Stored in locals so concurrent runs in the
505512 * same worker don't share state.
@@ -1458,11 +1465,11 @@ function toStreamTextOptions(options?: ToStreamTextOptionsOptions): Record<strin
14581465 const telemetry = prompt . toAISDKTelemetry ( options ?. telemetry ) ;
14591466 Object . assign ( result , telemetry ) ;
14601467
1461- // Auto-inject prepareStep when compaction or pendingMessages is configured .
1468+ // Auto-inject prepareStep for compaction, pending messages, and background context injection .
14621469 const taskCompaction = locals . get ( chatTaskCompactionKey ) ;
14631470 const taskPendingMessages = locals . get ( chatPendingMessagesKey ) ;
14641471
1465- if ( taskCompaction || taskPendingMessages ) {
1472+ {
14661473 result . prepareStep = async ( { messages, steps } : { messages : ModelMessage [ ] ; steps : CompactionStep [ ] } ) => {
14671474 let resultMessages : ModelMessage [ ] | undefined ;
14681475
@@ -1501,6 +1508,13 @@ function toStreamTextOptions(options?: ToStreamTextOptionsOptions): Record<strin
15011508 }
15021509 }
15031510
1511+ // 3. Background context injection
1512+ const bgQueue = locals . get ( chatBackgroundQueueKey ) ;
1513+ if ( bgQueue && bgQueue . length > 0 ) {
1514+ const injected = bgQueue . splice ( 0 ) ; // drain
1515+ resultMessages = [ ...( resultMessages ?? messages ) , ...injected ] ;
1516+ }
1517+
15041518 return resultMessages ? { messages : resultMessages } : undefined ;
15051519 } ;
15061520 }
@@ -2324,6 +2338,9 @@ function chatTask<
23242338 locals . set ( chatDeferKey , new Set ( ) ) ;
23252339 locals . set ( chatCompactionStateKey , undefined ) ;
23262340 locals . set ( chatSteeringQueueKey , [ ] ) ;
2341+ // NOTE: chatBackgroundQueueKey is NOT reset here — messages injected
2342+ // by deferred work from the previous turn's onTurnComplete need to
2343+ // survive into the next turn. The queue is drained before run().
23272344 locals . set ( chatInjectedMessageIdsKey , new Set ( ) ) ;
23282345
23292346 // Store chat context for auto-detection by ai.tool subtasks
@@ -2537,6 +2554,12 @@ function chatTask<
25372554 let runResult : unknown ;
25382555
25392556 try {
2557+ // Drain any messages injected by background work (e.g. self-review from previous turn)
2558+ const bgQueue = locals . get ( chatBackgroundQueueKey ) ;
2559+ if ( bgQueue && bgQueue . length > 0 ) {
2560+ accumulatedMessages . push ( ...bgQueue . splice ( 0 ) ) ;
2561+ }
2562+
25402563 runResult = await userRun ( {
25412564 ...restWire ,
25422565 messages : await applyPrepareMessages ( accumulatedMessages , "run" ) ,
@@ -2926,6 +2949,12 @@ function chatTask<
29262949 ) ;
29272950 }
29282951
2952+ // NOTE: We intentionally do NOT await deferred work from onTurnComplete here.
2953+ // Promises deferred in onTurnComplete (e.g. background self-review via
2954+ // chat.defer + chat.inject) run during the idle wait. If they complete
2955+ // before the next message, their injected context is picked up in prepareStep.
2956+ // The pre-onBeforeTurnComplete drain handles promises from onTurnStart/run().
2957+
29292958 // If messages arrived during streaming (without pendingMessages config),
29302959 // use the first one immediately as the next turn.
29312960 if ( pendingMessages . length > 0 ) {
@@ -3154,6 +3183,43 @@ function chatDefer(promise: Promise<unknown>): void {
31543183 }
31553184}
31563185
3186+ // ---------------------------------------------------------------------------
3187+ // Background context injection
3188+ // ---------------------------------------------------------------------------
3189+
3190+ /**
3191+ * Queue model messages for injection at the next `prepareStep` boundary.
3192+ *
3193+ * Use this to inject context from background work into the agent's conversation.
3194+ * Messages are appended to the model messages before the next LLM inference call.
3195+ *
3196+ * Combine with `chat.defer()` to run background analysis and inject results:
3197+ *
3198+ * @example
3199+ * ```ts
3200+ * onTurnComplete: async ({ messages }) => {
3201+ * chat.defer((async () => {
3202+ * const review = await generateObject({
3203+ * model: openai("gpt-4o-mini"),
3204+ * messages: [...messages, { role: "user", content: "Review the last response." }],
3205+ * schema: z.object({ suggestions: z.array(z.string()) }),
3206+ * });
3207+ * if (review.object.suggestions.length > 0) {
3208+ * chat.inject([{
3209+ * role: "system",
3210+ * content: `Improvements for next response:\n${review.object.suggestions.join("\n")}`,
3211+ * }]);
3212+ * }
3213+ * })());
3214+ * },
3215+ * ```
3216+ */
3217+ function injectBackgroundContext ( messages : ModelMessage [ ] ) : void {
3218+ const queue = locals . get ( chatBackgroundQueueKey ) ?? [ ] ;
3219+ queue . push ( ...messages ) ;
3220+ locals . set ( chatBackgroundQueueKey , queue ) ;
3221+ }
3222+
31573223// ---------------------------------------------------------------------------
31583224// Aborted message cleanup
31593225// ---------------------------------------------------------------------------
@@ -4154,6 +4220,8 @@ export const chat = {
41544220 cleanupAbortedParts,
41554221 /** Register background work that runs in parallel with streaming. See {@link chatDefer}. */
41564222 defer : chatDefer ,
4223+ /** Queue model messages for injection at the next `prepareStep` boundary. See {@link injectBackgroundContext}. */
4224+ inject : injectBackgroundContext ,
41574225 /** Typed chat output stream for writing custom chunks or piping from subtasks. */
41584226 stream : chatStream ,
41594227 /** Pre-built input stream for receiving messages from the transport. */
0 commit comments