@trigger.dev/sdk 0.0.0-prerelease-20260911094758 → 0.0.0-prerelease-20260911153544
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/commonjs/v3/ai.d.ts +36 -0
- package/dist/commonjs/v3/ai.js +211 -29
- package/dist/commonjs/v3/ai.js.map +1 -1
- package/dist/commonjs/v3/auth.d.ts +10 -4
- package/dist/commonjs/v3/auth.js.map +1 -1
- package/dist/commonjs/v3/test/transcript-storage-tests.js.map +1 -1
- package/dist/commonjs/v3/transcriptStorage.d.ts +1 -1
- package/dist/commonjs/v3/transcriptStorage.js.map +1 -1
- package/dist/commonjs/version.js +1 -1
- package/dist/esm/v3/ai.d.ts +36 -0
- package/dist/esm/v3/ai.js +211 -29
- package/dist/esm/v3/ai.js.map +1 -1
- package/dist/esm/v3/auth.d.ts +10 -4
- package/dist/esm/v3/auth.js.map +1 -1
- package/dist/esm/v3/test/transcript-storage-tests.js.map +1 -1
- package/dist/esm/v3/transcriptStorage.d.ts +1 -1
- package/dist/esm/v3/transcriptStorage.js.map +1 -1
- package/dist/esm/version.js +1 -1
- package/docs/ai-chat/background-injection.mdx +54 -1
- package/docs/ai-chat/migrating-from-hydrate-messages.mdx +299 -0
- package/docs/ai-chat/patterns/database-persistence.mdx +18 -7
- package/docs/ai-chat/reference.mdx +2 -1
- package/docs/ai-chat/side-channels.mdx +1 -1
- package/docs/ai-chat/transcript-storage.mdx +14 -6
- package/package.json +2 -2
package/dist/commonjs/v3/ai.d.ts
CHANGED
|
@@ -149,6 +149,41 @@ export declare const ai: {
|
|
|
149
149
|
* ```
|
|
150
150
|
*/
|
|
151
151
|
declare function createChatAccessToken<TTask extends AnyTask>(taskId: TaskIdentifier<TTask>): Promise<string>;
|
|
152
|
+
/**
|
|
153
|
+
* Register work that must land before anything from this turn reaches the
|
|
154
|
+
* frontend.
|
|
155
|
+
*
|
|
156
|
+
* Like {@link chatDefer} the work starts immediately and is never awaited by
|
|
157
|
+
* the hook that registered it, so it runs alongside the model and costs no
|
|
158
|
+
* time to first token. Unlike `chat.defer`, the output stream waits for it:
|
|
159
|
+
* no chunk of the answer is written to the session until it settles. That
|
|
160
|
+
* makes it the right home for a write the next page load has to see (a
|
|
161
|
+
* conversation row, a message insert), because a reader that can see the
|
|
162
|
+
* answer can also see what the write persisted.
|
|
163
|
+
*
|
|
164
|
+
* Reach for `chat.defer` instead when the timing does not matter for a
|
|
165
|
+
* reload: analytics, audit logs, search-index updates.
|
|
166
|
+
*
|
|
167
|
+
* This is not a consistency barrier for the turn. The work is still in flight
|
|
168
|
+
* while the model runs, so a tool, a `prepareStep`, or anything else executing
|
|
169
|
+
* during the turn can still read the state as it was before the write. It
|
|
170
|
+
* orders the write against what the frontend can see, nothing more. When the
|
|
171
|
+
* turn's own code has to read the write back, `await` it instead and accept
|
|
172
|
+
* the cost.
|
|
173
|
+
*
|
|
174
|
+
* A write registered here that fails, or outlasts the internal timeout, lets
|
|
175
|
+
* the stream through rather than stalling the conversation.
|
|
176
|
+
*
|
|
177
|
+
* @example
|
|
178
|
+
* ```ts
|
|
179
|
+
* onTurnStart: async ({ chatId, uiMessages }) => {
|
|
180
|
+
* chat.deferBeforeOutput(
|
|
181
|
+
* db.chat.update({ where: { id: chatId }, data: { messages: uiMessages } })
|
|
182
|
+
* );
|
|
183
|
+
* },
|
|
184
|
+
* ```
|
|
185
|
+
*/
|
|
186
|
+
declare function chatDeferBeforeOutput(promiseOrFn: Promise<unknown> | (() => Promise<unknown>)): void;
|
|
152
187
|
/**
|
|
153
188
|
* A stream writer passed to chat lifecycle callbacks (`onPreload`, `onChatStart`,
|
|
154
189
|
* `onTurnStart`, `onTurnComplete`, `onCompacted`).
|
|
@@ -3382,6 +3417,7 @@ export declare const chat: {
|
|
|
3382
3417
|
cleanupAbortedParts: typeof cleanupAbortedParts;
|
|
3383
3418
|
/** Register background work that runs in parallel with streaming. See {@link chatDefer}. */
|
|
3384
3419
|
defer: typeof chatDefer;
|
|
3420
|
+
deferBeforeOutput: typeof chatDeferBeforeOutput;
|
|
3385
3421
|
/** Queue model messages for injection at the next `prepareStep` boundary. See {@link injectBackgroundContext}. */
|
|
3386
3422
|
inject: typeof injectBackgroundContext;
|
|
3387
3423
|
/** Typed chat output stream for writing custom chunks or piping from subtasks. */
|
package/dist/commonjs/v3/ai.js
CHANGED
|
@@ -683,31 +683,143 @@ exports.ai = {
|
|
|
683
683
|
function createChatAccessToken(taskId) {
|
|
684
684
|
return auth_js_1.auth.createTriggerPublicToken(taskId, { expirationTime: "24h" });
|
|
685
685
|
}
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
686
|
+
function createChatOutGate() {
|
|
687
|
+
let resolveOpened;
|
|
688
|
+
const opened = new Promise((resolve) => {
|
|
689
|
+
resolveOpened = resolve;
|
|
690
|
+
});
|
|
691
|
+
const gate = {
|
|
692
|
+
pending: new Set(),
|
|
693
|
+
open: false,
|
|
694
|
+
opened,
|
|
695
|
+
failOpen() {
|
|
696
|
+
if (gate.open)
|
|
697
|
+
return;
|
|
698
|
+
gate.open = true;
|
|
699
|
+
resolveOpened();
|
|
700
|
+
},
|
|
701
|
+
};
|
|
702
|
+
return gate;
|
|
703
|
+
}
|
|
704
|
+
const chatOutGateKey = locals_js_1.locals.create("chat.outGate");
|
|
689
705
|
/**
|
|
690
|
-
*
|
|
691
|
-
*
|
|
692
|
-
*
|
|
706
|
+
* How long a write waits on the gate before giving up. A storage that hangs
|
|
707
|
+
* degrades to an ungated write rather than stalling the conversation.
|
|
708
|
+
* @internal
|
|
709
|
+
*/
|
|
710
|
+
const CHAT_OUT_GATE_TIMEOUT_MS = 10_000;
|
|
711
|
+
async function awaitChatOutGate() {
|
|
712
|
+
const gate = locals_js_1.locals.get(chatOutGateKey);
|
|
713
|
+
if (!gate || gate.open)
|
|
714
|
+
return;
|
|
715
|
+
const deadline = Date.now() + CHAT_OUT_GATE_TIMEOUT_MS;
|
|
716
|
+
while (gate.pending.size > 0) {
|
|
717
|
+
const remaining = deadline - Date.now();
|
|
718
|
+
if (remaining <= 0) {
|
|
719
|
+
gate.failOpen();
|
|
720
|
+
return;
|
|
721
|
+
}
|
|
722
|
+
const waitingOn = [...gate.pending];
|
|
723
|
+
let timedOut = false;
|
|
724
|
+
let timer;
|
|
725
|
+
try {
|
|
726
|
+
await Promise.race([
|
|
727
|
+
Promise.allSettled(waitingOn),
|
|
728
|
+
gate.opened,
|
|
729
|
+
new Promise((resolve) => {
|
|
730
|
+
timer = setTimeout(() => {
|
|
731
|
+
timedOut = true;
|
|
732
|
+
resolve();
|
|
733
|
+
}, remaining);
|
|
734
|
+
}),
|
|
735
|
+
]);
|
|
736
|
+
}
|
|
737
|
+
finally {
|
|
738
|
+
if (timer)
|
|
739
|
+
clearTimeout(timer);
|
|
740
|
+
}
|
|
741
|
+
if (gate.open)
|
|
742
|
+
return;
|
|
743
|
+
if (timedOut) {
|
|
744
|
+
gate.failOpen();
|
|
745
|
+
return;
|
|
746
|
+
}
|
|
747
|
+
for (const settled of waitingOn)
|
|
748
|
+
gate.pending.delete(settled);
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
/**
|
|
752
|
+
* Register work that must land before anything from this turn reaches the
|
|
753
|
+
* frontend.
|
|
754
|
+
*
|
|
755
|
+
* Like {@link chatDefer} the work starts immediately and is never awaited by
|
|
756
|
+
* the hook that registered it, so it runs alongside the model and costs no
|
|
757
|
+
* time to first token. Unlike `chat.defer`, the output stream waits for it:
|
|
758
|
+
* no chunk of the answer is written to the session until it settles. That
|
|
759
|
+
* makes it the right home for a write the next page load has to see (a
|
|
760
|
+
* conversation row, a message insert), because a reader that can see the
|
|
761
|
+
* answer can also see what the write persisted.
|
|
762
|
+
*
|
|
763
|
+
* Reach for `chat.defer` instead when the timing does not matter for a
|
|
764
|
+
* reload: analytics, audit logs, search-index updates.
|
|
765
|
+
*
|
|
766
|
+
* This is not a consistency barrier for the turn. The work is still in flight
|
|
767
|
+
* while the model runs, so a tool, a `prepareStep`, or anything else executing
|
|
768
|
+
* during the turn can still read the state as it was before the write. It
|
|
769
|
+
* orders the write against what the frontend can see, nothing more. When the
|
|
770
|
+
* turn's own code has to read the write back, `await` it instead and accept
|
|
771
|
+
* the cost.
|
|
693
772
|
*
|
|
694
|
-
*
|
|
773
|
+
* A write registered here that fails, or outlasts the internal timeout, lets
|
|
774
|
+
* the stream through rather than stalling the conversation.
|
|
775
|
+
*
|
|
776
|
+
* @example
|
|
695
777
|
* ```ts
|
|
696
|
-
*
|
|
697
|
-
*
|
|
698
|
-
*
|
|
699
|
-
*
|
|
700
|
-
*
|
|
701
|
-
* },
|
|
702
|
-
* });
|
|
703
|
-
* await waitUntilComplete();
|
|
778
|
+
* onTurnStart: async ({ chatId, uiMessages }) => {
|
|
779
|
+
* chat.deferBeforeOutput(
|
|
780
|
+
* db.chat.update({ where: { id: chatId }, data: { messages: uiMessages } })
|
|
781
|
+
* );
|
|
782
|
+
* },
|
|
704
783
|
* ```
|
|
705
|
-
*
|
|
706
|
-
* Backed by the Session primitive so a chat's output outlives any single
|
|
707
|
-
* run — subscribers (browser transport, server-side `ChatStream`) read
|
|
708
|
-
* the session's `.out`, not a per-run stream. Run-scoped `target`
|
|
709
|
-
* options on `.pipe()` are honoured as no-ops; the session is the target.
|
|
710
784
|
*/
|
|
785
|
+
function chatDeferBeforeOutput(promiseOrFn) {
|
|
786
|
+
const gate = locals_js_1.locals.get(chatOutGateKey);
|
|
787
|
+
const work = typeof promiseOrFn === "function" ? promiseOrFn() : promiseOrFn;
|
|
788
|
+
if (!gate || gate.open)
|
|
789
|
+
return;
|
|
790
|
+
gate.pending.add(work);
|
|
791
|
+
}
|
|
792
|
+
function gateWriterOptions(options) {
|
|
793
|
+
return {
|
|
794
|
+
...options,
|
|
795
|
+
execute: async (api) => {
|
|
796
|
+
await awaitChatOutGate();
|
|
797
|
+
return await options.execute(api);
|
|
798
|
+
},
|
|
799
|
+
};
|
|
800
|
+
}
|
|
801
|
+
function gateOutStream(value) {
|
|
802
|
+
return (async function* () {
|
|
803
|
+
await awaitChatOutGate();
|
|
804
|
+
if (isReadableStream(value)) {
|
|
805
|
+
const reader = value.getReader();
|
|
806
|
+
try {
|
|
807
|
+
while (true) {
|
|
808
|
+
const { done, value: chunk } = await reader.read();
|
|
809
|
+
if (done)
|
|
810
|
+
break;
|
|
811
|
+
yield chunk;
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
finally {
|
|
815
|
+
reader.releaseLock();
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
else {
|
|
819
|
+
yield* value;
|
|
820
|
+
}
|
|
821
|
+
})();
|
|
822
|
+
}
|
|
711
823
|
const chatStream = {
|
|
712
824
|
// Stable opaque label for the run-scoped `RealtimeDefinedStream` shape.
|
|
713
825
|
// `chatStream` is backed by the Session's `.out` channel — this id is
|
|
@@ -717,7 +829,7 @@ const chatStream = {
|
|
|
717
829
|
id: "chat",
|
|
718
830
|
pipe(value, options) {
|
|
719
831
|
const { target: _target, ...sessionOptions } = (options ?? {});
|
|
720
|
-
return getChatSession().out.pipe(value, sessionOptions);
|
|
832
|
+
return getChatSession().out.pipe(gateOutStream(value), sessionOptions);
|
|
721
833
|
},
|
|
722
834
|
async read(_runId, options) {
|
|
723
835
|
// Session channels don't need a runId — the session is the address.
|
|
@@ -727,10 +839,11 @@ const chatStream = {
|
|
|
727
839
|
},
|
|
728
840
|
async append(value, options) {
|
|
729
841
|
const { target: _target, ...sessionOptions } = (options ?? {});
|
|
842
|
+
await awaitChatOutGate();
|
|
730
843
|
return getChatSession().out.append(value, sessionOptions);
|
|
731
844
|
},
|
|
732
845
|
writer(options) {
|
|
733
|
-
return getChatSession().out.writer(options);
|
|
846
|
+
return getChatSession().out.writer(gateWriterOptions(options));
|
|
734
847
|
},
|
|
735
848
|
};
|
|
736
849
|
// ---------------------------------------------------------------------------
|
|
@@ -780,9 +893,13 @@ function createLazyChatWriter() {
|
|
|
780
893
|
let mergeImpl = null;
|
|
781
894
|
let waitPromise = null;
|
|
782
895
|
let resolveExecute = null;
|
|
896
|
+
let started = false;
|
|
897
|
+
const bufferedParts = [];
|
|
898
|
+
const bufferedStreams = [];
|
|
783
899
|
function ensureInitialized() {
|
|
784
|
-
if (
|
|
900
|
+
if (started)
|
|
785
901
|
return;
|
|
902
|
+
started = true;
|
|
786
903
|
const executePromise = new Promise((resolve) => {
|
|
787
904
|
resolveExecute = resolve;
|
|
788
905
|
});
|
|
@@ -792,7 +909,11 @@ function createLazyChatWriter() {
|
|
|
792
909
|
execute: ({ write, merge }) => {
|
|
793
910
|
writeImpl = write;
|
|
794
911
|
mergeImpl = merge;
|
|
795
|
-
|
|
912
|
+
for (const part of bufferedParts.splice(0))
|
|
913
|
+
write(part);
|
|
914
|
+
for (const stream of bufferedStreams.splice(0))
|
|
915
|
+
merge(stream);
|
|
916
|
+
return executePromise;
|
|
796
917
|
},
|
|
797
918
|
});
|
|
798
919
|
waitPromise = waitUntilComplete;
|
|
@@ -802,11 +923,17 @@ function createLazyChatWriter() {
|
|
|
802
923
|
write(part) {
|
|
803
924
|
ensureInitialized();
|
|
804
925
|
queueResponsePart(part);
|
|
805
|
-
writeImpl
|
|
926
|
+
if (writeImpl)
|
|
927
|
+
writeImpl(part);
|
|
928
|
+
else
|
|
929
|
+
bufferedParts.push(part);
|
|
806
930
|
},
|
|
807
931
|
merge(stream) {
|
|
808
932
|
ensureInitialized();
|
|
809
|
-
mergeImpl
|
|
933
|
+
if (mergeImpl)
|
|
934
|
+
mergeImpl(stream);
|
|
935
|
+
else
|
|
936
|
+
bufferedStreams.push(stream);
|
|
810
937
|
},
|
|
811
938
|
},
|
|
812
939
|
async flush() {
|
|
@@ -3955,6 +4082,17 @@ function chatAgent(options) {
|
|
|
3955
4082
|
* keeps an action's write cursor-neutral.
|
|
3956
4083
|
*/
|
|
3957
4084
|
let lastSnapshotOutEventId;
|
|
4085
|
+
/**
|
|
4086
|
+
* The `lastInEventId` the most recent snapshot carried.
|
|
4087
|
+
*
|
|
4088
|
+
* A turn-start save happens after the incoming message has been handed to
|
|
4089
|
+
* the turn loop, so the router's live resume floor has already advanced
|
|
4090
|
+
* past it. Persisting that floor before the turn runs would let the next
|
|
4091
|
+
* boot resume past a message this run never answered, which is exactly
|
|
4092
|
+
* what a deferred or recovered message depends on. Turn-start carries
|
|
4093
|
+
* this instead.
|
|
4094
|
+
*/
|
|
4095
|
+
let lastSnapshotInEventId;
|
|
3958
4096
|
const storageTrigger = (trigger) => trigger === "regenerate-message"
|
|
3959
4097
|
? "regenerate-message"
|
|
3960
4098
|
: trigger === "action" || trigger === "action-turn"
|
|
@@ -3968,7 +4106,8 @@ function chatAgent(options) {
|
|
|
3968
4106
|
*/
|
|
3969
4107
|
/** The runtime's opaque state as of the last save; carried on every changeset's transcript. */
|
|
3970
4108
|
let transcriptState = null;
|
|
3971
|
-
|
|
4109
|
+
let transcriptSaveChain = Promise.resolve();
|
|
4110
|
+
const runSaveTranscript = async (opts) => {
|
|
3972
4111
|
const { changes, shadow } = (0, transcriptStorage_js_1.diffTranscript)(transcriptShadow, opts.messages, {
|
|
3973
4112
|
nonFinalIds: opts.nonFinalIds,
|
|
3974
4113
|
});
|
|
@@ -3992,8 +4131,15 @@ function chatAgent(options) {
|
|
|
3992
4131
|
if (runtimeState !== null || persistedStateSet) {
|
|
3993
4132
|
changes.push({ op: "state", value: runtimeState });
|
|
3994
4133
|
}
|
|
4134
|
+
if (opts.skipIfUnchanged && changes.length === 0)
|
|
4135
|
+
return;
|
|
3995
4136
|
transcriptState = runtimeState;
|
|
3996
|
-
const
|
|
4137
|
+
const liveInCursor = chatInputRouter().resumeFloor();
|
|
4138
|
+
const inCursor = opts.carryInCursor
|
|
4139
|
+
? lastSnapshotInEventId
|
|
4140
|
+
: liveInCursor !== undefined
|
|
4141
|
+
? String(liveInCursor)
|
|
4142
|
+
: undefined;
|
|
3997
4143
|
await transcriptStorage.save({
|
|
3998
4144
|
chatId: payload.chatId,
|
|
3999
4145
|
clientData: opts.clientData,
|
|
@@ -4014,12 +4160,28 @@ function chatAgent(options) {
|
|
|
4014
4160
|
},
|
|
4015
4161
|
cursors: {
|
|
4016
4162
|
lastOutEventId: opts.lastOutEventId,
|
|
4017
|
-
lastInEventId: inCursor
|
|
4163
|
+
lastInEventId: inCursor,
|
|
4018
4164
|
},
|
|
4019
4165
|
});
|
|
4020
4166
|
transcriptShadow = shadow;
|
|
4167
|
+
lastSnapshotInEventId = inCursor;
|
|
4021
4168
|
persistedStateSet = runtimeState !== null;
|
|
4022
4169
|
};
|
|
4170
|
+
/**
|
|
4171
|
+
* Serialise every save onto one chain. `runSaveTranscript` derives its
|
|
4172
|
+
* changeset from `transcriptShadow` and only advances it once the write
|
|
4173
|
+
* lands, so two overlapping saves would diff against stale state. The
|
|
4174
|
+
* message list is copied on the way in because the accumulator keeps
|
|
4175
|
+
* mutating while a queued save waits its turn. A rejection is handed to
|
|
4176
|
+
* the caller but never poisons the chain.
|
|
4177
|
+
*/
|
|
4178
|
+
const saveTranscript = (opts) => {
|
|
4179
|
+
const queued = { ...opts, messages: [...opts.messages] };
|
|
4180
|
+
const run = () => runSaveTranscript(queued);
|
|
4181
|
+
const next = transcriptSaveChain.then(run, run);
|
|
4182
|
+
transcriptSaveChain = next.then(() => undefined, () => undefined);
|
|
4183
|
+
return next;
|
|
4184
|
+
};
|
|
4023
4185
|
/**
|
|
4024
4186
|
* Persist the accumulator outside a turn.
|
|
4025
4187
|
*
|
|
@@ -4126,6 +4288,7 @@ function chatAgent(options) {
|
|
|
4126
4288
|
// turn (chain self-bootstraps from turn 2), so this is purely an
|
|
4127
4289
|
// optimization to keep continuation runs bounded from the first turn.
|
|
4128
4290
|
lastSnapshotOutEventId = bootSnapshot?.lastOutEventId;
|
|
4291
|
+
lastSnapshotInEventId = bootSnapshot?.lastInEventId;
|
|
4129
4292
|
if (bootSnapshot?.lastOutEventId !== undefined) {
|
|
4130
4293
|
const seeded = Number.parseInt(bootSnapshot.lastOutEventId, 10);
|
|
4131
4294
|
if (Number.isFinite(seeded)) {
|
|
@@ -4875,6 +5038,7 @@ function chatAgent(options) {
|
|
|
4875
5038
|
// (errors are caught by the outer try/catch which writes an error chunk)
|
|
4876
5039
|
locals_js_1.locals.set(chatPipeCountKey, 0);
|
|
4877
5040
|
locals_js_1.locals.set(chatDeferKey, new Set());
|
|
5041
|
+
locals_js_1.locals.set(chatOutGateKey, createChatOutGate());
|
|
4878
5042
|
locals_js_1.locals.set(chatCompactionStateKey, undefined);
|
|
4879
5043
|
locals_js_1.locals.set(chatSteeringQueueKey, []);
|
|
4880
5044
|
locals_js_1.locals.set(chatPendingBackgroundKey, []);
|
|
@@ -5363,6 +5527,23 @@ function chatAgent(options) {
|
|
|
5363
5527
|
// A no-op turn skips this block, and with it `followSessionPin`:
|
|
5364
5528
|
// there is nothing to answer, so nothing to hand over.
|
|
5365
5529
|
if ((!isAction || actionTurn) && !isNoOpTurn) {
|
|
5530
|
+
if (!hydrateMessages) {
|
|
5531
|
+
chatDeferBeforeOutput(saveTranscript({
|
|
5532
|
+
reason: "turn-start",
|
|
5533
|
+
messages: accumulatedUIMessages,
|
|
5534
|
+
turn,
|
|
5535
|
+
trigger: storageTrigger(currentWirePayload.trigger),
|
|
5536
|
+
clientData,
|
|
5537
|
+
lastOutEventId: lastSnapshotOutEventId,
|
|
5538
|
+
skipIfUnchanged: true,
|
|
5539
|
+
carryInCursor: true,
|
|
5540
|
+
}).catch((error) => {
|
|
5541
|
+
v3_1.logger.warn("chat.agent: turn-start transcript write failed; a reload mid-answer may not show the message being answered", {
|
|
5542
|
+
error: error instanceof Error ? error.message : String(error),
|
|
5543
|
+
sessionId: sessionIdForSnapshot,
|
|
5544
|
+
});
|
|
5545
|
+
}));
|
|
5546
|
+
}
|
|
5366
5547
|
// Mint a scoped public access token once per turn, reused for
|
|
5367
5548
|
// onChatStart, onTurnStart, onTurnComplete, and the turn-complete chunk.
|
|
5368
5549
|
const currentRunId = ctx.run.id;
|
|
@@ -8839,6 +9020,7 @@ exports.chat = {
|
|
|
8839
9020
|
cleanupAbortedParts,
|
|
8840
9021
|
/** Register background work that runs in parallel with streaming. See {@link chatDefer}. */
|
|
8841
9022
|
defer: chatDefer,
|
|
9023
|
+
deferBeforeOutput: chatDeferBeforeOutput,
|
|
8842
9024
|
/** Queue model messages for injection at the next `prepareStep` boundary. See {@link injectBackgroundContext}. */
|
|
8843
9025
|
inject: injectBackgroundContext,
|
|
8844
9026
|
/** Typed chat output stream for writing custom chunks or piping from subtasks. */
|