@trigger.dev/sdk 0.0.0-prerelease-20260911094758 → 0.0.0-prerelease-windows-bundle-fix-20260928100448
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 +73 -12
- package/dist/commonjs/v3/ai.js +542 -289
- package/dist/commonjs/v3/ai.js.map +1 -1
- package/dist/commonjs/v3/auth.d.ts +10 -4
- package/dist/commonjs/v3/auth.js +8 -2
- package/dist/commonjs/v3/auth.js.map +1 -1
- package/dist/commonjs/v3/chat-client.js +7 -0
- package/dist/commonjs/v3/chat-client.js.map +1 -1
- package/dist/commonjs/v3/chat-server.d.ts +1 -0
- package/dist/commonjs/v3/chat-server.js +8 -0
- package/dist/commonjs/v3/chat-server.js.map +1 -1
- package/dist/commonjs/v3/chat.js +13 -9
- package/dist/commonjs/v3/chat.js.map +1 -1
- package/dist/commonjs/v3/compactionResponse.d.ts +9 -0
- package/dist/commonjs/v3/compactionResponse.js +37 -0
- package/dist/commonjs/v3/compactionResponse.js.map +1 -0
- package/dist/commonjs/v3/concurrency-shared.d.ts +13 -0
- package/dist/commonjs/v3/concurrency-shared.js +35 -0
- package/dist/commonjs/v3/concurrency-shared.js.map +1 -0
- package/dist/commonjs/v3/concurrencyLimits.d.ts +73 -0
- package/dist/commonjs/v3/concurrencyLimits.js +166 -0
- package/dist/commonjs/v3/concurrencyLimits.js.map +1 -0
- package/dist/commonjs/v3/index.d.ts +2 -1
- package/dist/commonjs/v3/index.js +3 -1
- package/dist/commonjs/v3/index.js.map +1 -1
- package/dist/commonjs/v3/managedChatResponse.d.ts +44 -0
- package/dist/commonjs/v3/managedChatResponse.js +233 -0
- package/dist/commonjs/v3/managedChatResponse.js.map +1 -0
- package/dist/commonjs/v3/queues.d.ts +31 -0
- package/dist/commonjs/v3/queues.js +31 -0
- package/dist/commonjs/v3/queues.js.map +1 -1
- package/dist/commonjs/v3/runs.d.ts +5 -0
- package/dist/commonjs/v3/sessionTracing.d.ts +7 -0
- package/dist/commonjs/v3/sessionTracing.js +44 -0
- package/dist/commonjs/v3/sessionTracing.js.map +1 -0
- package/dist/commonjs/v3/sessions.js +31 -34
- package/dist/commonjs/v3/sessions.js.map +1 -1
- package/dist/commonjs/v3/shared.d.ts +18 -1
- package/dist/commonjs/v3/shared.js +145 -52
- package/dist/commonjs/v3/shared.js.map +1 -1
- package/dist/commonjs/v3/steeringContext.d.ts +41 -0
- package/dist/commonjs/v3/steeringContext.js +118 -0
- package/dist/commonjs/v3/steeringContext.js.map +1 -0
- package/dist/commonjs/v3/test/transcript-storage-tests.js.map +1 -1
- package/dist/commonjs/v3/transcriptStorage.d.ts +5 -2
- package/dist/commonjs/v3/transcriptStorage.js +55 -4
- package/dist/commonjs/v3/transcriptStorage.js.map +1 -1
- package/dist/commonjs/version.js +1 -1
- package/dist/esm/v3/ai.d.ts +73 -12
- package/dist/esm/v3/ai.js +543 -290
- package/dist/esm/v3/ai.js.map +1 -1
- package/dist/esm/v3/auth.d.ts +10 -4
- package/dist/esm/v3/auth.js +8 -2
- package/dist/esm/v3/auth.js.map +1 -1
- package/dist/esm/v3/chat-client.js +7 -0
- package/dist/esm/v3/chat-client.js.map +1 -1
- package/dist/esm/v3/chat-server.d.ts +1 -0
- package/dist/esm/v3/chat-server.js +8 -0
- package/dist/esm/v3/chat-server.js.map +1 -1
- package/dist/esm/v3/chat.js +13 -9
- package/dist/esm/v3/chat.js.map +1 -1
- package/dist/esm/v3/compactionResponse.d.ts +9 -0
- package/dist/esm/v3/compactionResponse.js +34 -0
- package/dist/esm/v3/compactionResponse.js.map +1 -0
- package/dist/esm/v3/concurrency-shared.d.ts +13 -0
- package/dist/esm/v3/concurrency-shared.js +31 -0
- package/dist/esm/v3/concurrency-shared.js.map +1 -0
- package/dist/esm/v3/concurrencyLimits.d.ts +73 -0
- package/dist/esm/v3/concurrencyLimits.js +158 -0
- package/dist/esm/v3/concurrencyLimits.js.map +1 -0
- package/dist/esm/v3/index.d.ts +2 -1
- package/dist/esm/v3/index.js +2 -1
- package/dist/esm/v3/index.js.map +1 -1
- package/dist/esm/v3/managedChatResponse.d.ts +44 -0
- package/dist/esm/v3/managedChatResponse.js +228 -0
- package/dist/esm/v3/managedChatResponse.js.map +1 -0
- package/dist/esm/v3/queues.d.ts +31 -0
- package/dist/esm/v3/queues.js +31 -0
- package/dist/esm/v3/queues.js.map +1 -1
- package/dist/esm/v3/runs.d.ts +5 -0
- package/dist/esm/v3/sessionTracing.d.ts +7 -0
- package/dist/esm/v3/sessionTracing.js +40 -0
- package/dist/esm/v3/sessionTracing.js.map +1 -0
- package/dist/esm/v3/sessions.js +31 -34
- package/dist/esm/v3/sessions.js.map +1 -1
- package/dist/esm/v3/shared.d.ts +18 -1
- package/dist/esm/v3/shared.js +145 -53
- package/dist/esm/v3/shared.js.map +1 -1
- package/dist/esm/v3/steeringContext.d.ts +41 -0
- package/dist/esm/v3/steeringContext.js +113 -0
- package/dist/esm/v3/steeringContext.js.map +1 -0
- package/dist/esm/v3/test/transcript-storage-tests.js.map +1 -1
- package/dist/esm/v3/transcriptStorage.d.ts +5 -2
- package/dist/esm/v3/transcriptStorage.js +55 -5
- package/dist/esm/v3/transcriptStorage.js.map +1 -1
- package/dist/esm/version.js +1 -1
- package/docs/ai-chat/anatomy.mdx +44 -24
- package/docs/ai-chat/background-injection.mdx +54 -1
- package/docs/ai-chat/client-protocol.mdx +2 -0
- package/docs/ai-chat/error-handling.mdx +44 -76
- package/docs/ai-chat/frontend.mdx +27 -21
- package/docs/ai-chat/lifecycle-hooks.mdx +4 -2
- package/docs/ai-chat/migrating-from-hydrate-messages.mdx +299 -0
- package/docs/ai-chat/patterns/branching-conversations.mdx +95 -230
- package/docs/ai-chat/patterns/database-persistence.mdx +18 -7
- package/docs/ai-chat/patterns/human-in-the-loop.mdx +166 -164
- package/docs/ai-chat/patterns/tool-result-auditing.mdx +28 -27
- package/docs/ai-chat/pending-messages.mdx +19 -5
- package/docs/ai-chat/quick-start.mdx +26 -20
- package/docs/ai-chat/reference.mdx +6 -3
- package/docs/ai-chat/side-channels.mdx +1 -1
- package/docs/ai-chat/testing.mdx +16 -4
- package/docs/ai-chat/transcript-storage.mdx +14 -6
- package/docs/cli-env-commands.mdx +114 -0
- package/docs/cli-projects-commands.mdx +62 -0
- package/docs/cli-runs-commands.mdx +94 -0
- package/docs/concurrency.mdx +384 -0
- package/docs/database-connections.mdx +3 -3
- package/docs/deploy-environment-variables.mdx +6 -0
- package/docs/idempotency.mdx +43 -5
- package/docs/introduction.mdx +58 -152
- package/docs/limits.mdx +16 -6
- package/docs/observability/query.mdx +25 -0
- package/docs/queues.mdx +248 -0
- package/docs/reports.mdx +1 -1
- package/docs/runs/priority.mdx +2 -25
- package/docs/self-hosting/env/webapp.mdx +7 -0
- package/docs/tasks/overview.mdx +3 -5
- package/docs/troubleshooting-alerts.mdx +124 -1
- package/docs/troubleshooting.mdx +12 -0
- package/docs/writing-tasks-introduction.mdx +2 -1
- package/package.json +2 -2
- package/docs/queue-concurrency.mdx +0 -358
package/dist/commonjs/v3/ai.js
CHANGED
|
@@ -15,8 +15,12 @@ const v3_1 = require("@trigger.dev/core/v3");
|
|
|
15
15
|
// Runtime VALUES go through the ESM/CJS shim so the CJS build can `require`
|
|
16
16
|
// ESM-only `ai@7` (see ../imports/ai-runtime.ts).
|
|
17
17
|
const api_1 = require("@opentelemetry/api");
|
|
18
|
+
const sessionTracing_js_1 = require("./sessionTracing.js");
|
|
18
19
|
const ai_runtime_js_1 = require("../imports/ai-runtime.js");
|
|
19
20
|
const transcriptStorage_js_1 = require("./transcriptStorage.js");
|
|
21
|
+
const compactionResponse_js_1 = require("./compactionResponse.js");
|
|
22
|
+
const managedChatResponse_js_1 = require("./managedChatResponse.js");
|
|
23
|
+
const steeringContext_js_1 = require("./steeringContext.js");
|
|
20
24
|
let transcriptStorageOverride;
|
|
21
25
|
/**
|
|
22
26
|
* Test-only override for the storage `chat.agent` persists through, so a
|
|
@@ -46,6 +50,7 @@ const externalDeploymentId_js_1 = require("./externalDeploymentId.js");
|
|
|
46
50
|
const chatVersionSkew_js_1 = require("./chatVersionSkew.js");
|
|
47
51
|
const sessions_js_1 = require("./sessions.js");
|
|
48
52
|
const shared_js_1 = require("./shared.js");
|
|
53
|
+
const concurrency_shared_js_1 = require("./concurrency-shared.js");
|
|
49
54
|
const streams_js_1 = require("./streams.js");
|
|
50
55
|
const tracer_js_1 = require("./tracer.js");
|
|
51
56
|
const METADATA_KEY = "tool.execute.options";
|
|
@@ -54,17 +59,17 @@ const METADATA_KEY = "tool.execute.options";
|
|
|
54
59
|
* `ignoreIncompleteToolCalls: true` to prevent failures from
|
|
55
60
|
* stopped/aborted conversations with partial tool parts.
|
|
56
61
|
*/
|
|
57
|
-
function toModelMessages(messages) {
|
|
62
|
+
function toModelMessages(messages, context) {
|
|
58
63
|
// Pass the resolved per-turn `tools` (if any) so the AI SDK can look up each
|
|
59
64
|
// tool's `toModelOutput` and re-apply it to prior-turn tool results. Without
|
|
60
65
|
// `tools` it falls back to JSON-stringifying the raw output (TRI-10149). The
|
|
61
66
|
// conditional spread keeps the options object byte-identical to the no-tools
|
|
62
67
|
// path when nothing was declared.
|
|
63
68
|
const tools = locals_js_1.locals.get(chatResolvedToolsKey);
|
|
64
|
-
return (0, ai_runtime_js_1.convertToModelMessages)(
|
|
69
|
+
return (0, steeringContext_js_1.convertSteeredMessages)(messages, async (batch) => (0, ai_runtime_js_1.convertToModelMessages)(batch, {
|
|
65
70
|
ignoreIncompleteToolCalls: true,
|
|
66
71
|
...(tools ? { tools } : {}),
|
|
67
|
-
});
|
|
72
|
+
}), locals_js_1.locals.get(chatSteeringInjectionsKey) ?? new Map(), context ?? locals_js_1.locals.get(chatCurrentUIMessagesKey) ?? messages, context !== undefined || locals_js_1.locals.get(chatCurrentUIMessagesKey) !== undefined);
|
|
68
73
|
}
|
|
69
74
|
const chatTurnContextKey = locals_js_1.locals.create("chat.turnContext");
|
|
70
75
|
/**
|
|
@@ -683,31 +688,158 @@ exports.ai = {
|
|
|
683
688
|
function createChatAccessToken(taskId) {
|
|
684
689
|
return auth_js_1.auth.createTriggerPublicToken(taskId, { expirationTime: "24h" });
|
|
685
690
|
}
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
691
|
+
function createChatOutGate() {
|
|
692
|
+
let resolveOpened;
|
|
693
|
+
const opened = new Promise((resolve) => {
|
|
694
|
+
resolveOpened = resolve;
|
|
695
|
+
});
|
|
696
|
+
const gate = {
|
|
697
|
+
pending: new Set(),
|
|
698
|
+
open: false,
|
|
699
|
+
opened,
|
|
700
|
+
failOpen() {
|
|
701
|
+
if (gate.open)
|
|
702
|
+
return;
|
|
703
|
+
gate.open = true;
|
|
704
|
+
resolveOpened();
|
|
705
|
+
},
|
|
706
|
+
};
|
|
707
|
+
return gate;
|
|
708
|
+
}
|
|
709
|
+
const chatOutGateKey = locals_js_1.locals.create("chat.outGate");
|
|
710
|
+
/**
|
|
711
|
+
* How long a write waits on the gate before giving up. A storage that hangs
|
|
712
|
+
* degrades to an ungated write rather than stalling the conversation.
|
|
713
|
+
* @internal
|
|
714
|
+
*/
|
|
715
|
+
const CHAT_OUT_GATE_TIMEOUT_MS = 10_000;
|
|
716
|
+
/**
|
|
717
|
+
* The ids to save non-final after a failed turn: the stream's partial answer,
|
|
718
|
+
* but only while the message under its id is still that partial by content.
|
|
719
|
+
* `onTurnComplete` may hand back a cloned history (same content, new objects),
|
|
720
|
+
* which keeps it partial, or replace it in place, which finishes it.
|
|
721
|
+
* @internal
|
|
722
|
+
*/
|
|
723
|
+
function partialStillUnfinished(partial, fingerprint, messages) {
|
|
724
|
+
if (!partial || fingerprint === undefined)
|
|
725
|
+
return undefined;
|
|
726
|
+
const current = messages.find((message) => message.id === partial.id);
|
|
727
|
+
if (!current || (0, transcriptStorage_js_1.fingerprintMessage)(current) !== fingerprint)
|
|
728
|
+
return undefined;
|
|
729
|
+
return new Set([partial.id]);
|
|
730
|
+
}
|
|
731
|
+
async function awaitChatOutGate() {
|
|
732
|
+
const gate = locals_js_1.locals.get(chatOutGateKey);
|
|
733
|
+
if (!gate || gate.open)
|
|
734
|
+
return;
|
|
735
|
+
const deadline = Date.now() + CHAT_OUT_GATE_TIMEOUT_MS;
|
|
736
|
+
while (gate.pending.size > 0) {
|
|
737
|
+
const remaining = deadline - Date.now();
|
|
738
|
+
if (remaining <= 0) {
|
|
739
|
+
gate.failOpen();
|
|
740
|
+
return;
|
|
741
|
+
}
|
|
742
|
+
const waitingOn = [...gate.pending];
|
|
743
|
+
let timedOut = false;
|
|
744
|
+
let timer;
|
|
745
|
+
try {
|
|
746
|
+
await Promise.race([
|
|
747
|
+
Promise.allSettled(waitingOn),
|
|
748
|
+
gate.opened,
|
|
749
|
+
new Promise((resolve) => {
|
|
750
|
+
timer = setTimeout(() => {
|
|
751
|
+
timedOut = true;
|
|
752
|
+
resolve();
|
|
753
|
+
}, remaining);
|
|
754
|
+
}),
|
|
755
|
+
]);
|
|
756
|
+
}
|
|
757
|
+
finally {
|
|
758
|
+
if (timer)
|
|
759
|
+
clearTimeout(timer);
|
|
760
|
+
}
|
|
761
|
+
if (gate.open)
|
|
762
|
+
return;
|
|
763
|
+
if (timedOut) {
|
|
764
|
+
gate.failOpen();
|
|
765
|
+
return;
|
|
766
|
+
}
|
|
767
|
+
for (const settled of waitingOn)
|
|
768
|
+
gate.pending.delete(settled);
|
|
769
|
+
}
|
|
770
|
+
}
|
|
689
771
|
/**
|
|
690
|
-
*
|
|
691
|
-
*
|
|
692
|
-
*
|
|
772
|
+
* Register work that must land before anything from this turn reaches the
|
|
773
|
+
* frontend.
|
|
774
|
+
*
|
|
775
|
+
* Like {@link chatDefer} the work starts immediately and is never awaited by
|
|
776
|
+
* the hook that registered it, so it runs alongside the model and costs no
|
|
777
|
+
* time to first token. Unlike `chat.defer`, the output stream waits for it:
|
|
778
|
+
* no chunk of the answer is written to the session until it settles. That
|
|
779
|
+
* makes it the right home for a write the next page load has to see (a
|
|
780
|
+
* conversation row, a message insert), because a reader that can see the
|
|
781
|
+
* answer can also see what the write persisted.
|
|
782
|
+
*
|
|
783
|
+
* Reach for `chat.defer` instead when the timing does not matter for a
|
|
784
|
+
* reload: analytics, audit logs, search-index updates.
|
|
785
|
+
*
|
|
786
|
+
* This is not a consistency barrier for the turn. The work is still in flight
|
|
787
|
+
* while the model runs, so a tool, a `prepareStep`, or anything else executing
|
|
788
|
+
* during the turn can still read the state as it was before the write. It
|
|
789
|
+
* orders the write against what the frontend can see, nothing more. When the
|
|
790
|
+
* turn's own code has to read the write back, `await` it instead and accept
|
|
791
|
+
* the cost.
|
|
693
792
|
*
|
|
694
|
-
*
|
|
793
|
+
* A write registered here that fails, or outlasts the internal timeout, lets
|
|
794
|
+
* the stream through rather than stalling the conversation.
|
|
795
|
+
*
|
|
796
|
+
* @example
|
|
695
797
|
* ```ts
|
|
696
|
-
*
|
|
697
|
-
*
|
|
698
|
-
*
|
|
699
|
-
*
|
|
700
|
-
*
|
|
701
|
-
* },
|
|
702
|
-
* });
|
|
703
|
-
* await waitUntilComplete();
|
|
798
|
+
* onTurnStart: async ({ chatId, uiMessages }) => {
|
|
799
|
+
* chat.deferBeforeOutput(
|
|
800
|
+
* db.chat.update({ where: { id: chatId }, data: { messages: uiMessages } })
|
|
801
|
+
* );
|
|
802
|
+
* },
|
|
704
803
|
* ```
|
|
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
804
|
*/
|
|
805
|
+
function chatDeferBeforeOutput(promiseOrFn) {
|
|
806
|
+
const gate = locals_js_1.locals.get(chatOutGateKey);
|
|
807
|
+
const work = typeof promiseOrFn === "function" ? promiseOrFn() : promiseOrFn;
|
|
808
|
+
if (!gate || gate.open)
|
|
809
|
+
return;
|
|
810
|
+
gate.pending.add(work);
|
|
811
|
+
}
|
|
812
|
+
function gateWriterOptions(options) {
|
|
813
|
+
return {
|
|
814
|
+
...options,
|
|
815
|
+
execute: async (api) => {
|
|
816
|
+
await awaitChatOutGate();
|
|
817
|
+
return await options.execute(api);
|
|
818
|
+
},
|
|
819
|
+
};
|
|
820
|
+
}
|
|
821
|
+
function gateOutStream(value) {
|
|
822
|
+
return (async function* () {
|
|
823
|
+
await awaitChatOutGate();
|
|
824
|
+
if (isReadableStream(value)) {
|
|
825
|
+
const reader = value.getReader();
|
|
826
|
+
try {
|
|
827
|
+
while (true) {
|
|
828
|
+
const { done, value: chunk } = await reader.read();
|
|
829
|
+
if (done)
|
|
830
|
+
break;
|
|
831
|
+
yield chunk;
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
finally {
|
|
835
|
+
reader.releaseLock();
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
else {
|
|
839
|
+
yield* value;
|
|
840
|
+
}
|
|
841
|
+
})();
|
|
842
|
+
}
|
|
711
843
|
const chatStream = {
|
|
712
844
|
// Stable opaque label for the run-scoped `RealtimeDefinedStream` shape.
|
|
713
845
|
// `chatStream` is backed by the Session's `.out` channel — this id is
|
|
@@ -717,7 +849,7 @@ const chatStream = {
|
|
|
717
849
|
id: "chat",
|
|
718
850
|
pipe(value, options) {
|
|
719
851
|
const { target: _target, ...sessionOptions } = (options ?? {});
|
|
720
|
-
return getChatSession().out.pipe(value, sessionOptions);
|
|
852
|
+
return getChatSession().out.pipe(gateOutStream(value), sessionOptions);
|
|
721
853
|
},
|
|
722
854
|
async read(_runId, options) {
|
|
723
855
|
// Session channels don't need a runId — the session is the address.
|
|
@@ -727,10 +859,11 @@ const chatStream = {
|
|
|
727
859
|
},
|
|
728
860
|
async append(value, options) {
|
|
729
861
|
const { target: _target, ...sessionOptions } = (options ?? {});
|
|
862
|
+
await awaitChatOutGate();
|
|
730
863
|
return getChatSession().out.append(value, sessionOptions);
|
|
731
864
|
},
|
|
732
865
|
writer(options) {
|
|
733
|
-
return getChatSession().out.writer(options);
|
|
866
|
+
return getChatSession().out.writer(gateWriterOptions(options));
|
|
734
867
|
},
|
|
735
868
|
};
|
|
736
869
|
// ---------------------------------------------------------------------------
|
|
@@ -741,7 +874,7 @@ const chatStream = {
|
|
|
741
874
|
* `onTurnComplete`'s `responseMessage` and `uiMessages`.
|
|
742
875
|
*
|
|
743
876
|
* Non-transient data chunks (`type` starts with `data-`, no `transient: true`)
|
|
744
|
-
* are
|
|
877
|
+
* are accumulated in emission order into the assistant response message.
|
|
745
878
|
* Transient or non-data chunks are streamed only (same as `chat.stream`).
|
|
746
879
|
*
|
|
747
880
|
* @example
|
|
@@ -759,15 +892,7 @@ const chatResponse = {
|
|
|
759
892
|
* response message; everything else is stream-only.
|
|
760
893
|
*/
|
|
761
894
|
write(part) {
|
|
762
|
-
|
|
763
|
-
const { waitUntilComplete } = chatStream.writer({
|
|
764
|
-
spanName: "chat.response.write",
|
|
765
|
-
collapsed: true,
|
|
766
|
-
execute: ({ write }) => {
|
|
767
|
-
write(part);
|
|
768
|
-
},
|
|
769
|
-
});
|
|
770
|
-
waitUntilComplete().catch(() => { });
|
|
895
|
+
managedResponse().writeData(part);
|
|
771
896
|
},
|
|
772
897
|
};
|
|
773
898
|
/**
|
|
@@ -776,46 +901,13 @@ const chatResponse = {
|
|
|
776
901
|
* @internal
|
|
777
902
|
*/
|
|
778
903
|
function createLazyChatWriter() {
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
let waitPromise = null;
|
|
782
|
-
let resolveExecute = null;
|
|
783
|
-
function ensureInitialized() {
|
|
784
|
-
if (writeImpl)
|
|
785
|
-
return;
|
|
786
|
-
const executePromise = new Promise((resolve) => {
|
|
787
|
-
resolveExecute = resolve;
|
|
788
|
-
});
|
|
789
|
-
const { waitUntilComplete } = chatStream.writer({
|
|
904
|
+
return (0, managedChatResponse_js_1.createOrderedChatWriter)(() => (locals_js_1.locals.get(chatManagedResponseActiveKey) ? managedResponse() : undefined), async (stream) => {
|
|
905
|
+
const { waitUntilComplete } = chatStream.pipe(stream, {
|
|
790
906
|
collapsed: true,
|
|
791
907
|
spanName: "callback writer",
|
|
792
|
-
execute: ({ write, merge }) => {
|
|
793
|
-
writeImpl = write;
|
|
794
|
-
mergeImpl = merge;
|
|
795
|
-
return executePromise; // Keep execute alive until flush()
|
|
796
|
-
},
|
|
797
908
|
});
|
|
798
|
-
|
|
799
|
-
}
|
|
800
|
-
return {
|
|
801
|
-
writer: {
|
|
802
|
-
write(part) {
|
|
803
|
-
ensureInitialized();
|
|
804
|
-
queueResponsePart(part);
|
|
805
|
-
writeImpl(part);
|
|
806
|
-
},
|
|
807
|
-
merge(stream) {
|
|
808
|
-
ensureInitialized();
|
|
809
|
-
mergeImpl(stream);
|
|
810
|
-
},
|
|
811
|
-
},
|
|
812
|
-
async flush() {
|
|
813
|
-
if (resolveExecute) {
|
|
814
|
-
resolveExecute(); // Signal execute to complete
|
|
815
|
-
await waitPromise(); // Wait for stream to finish piping
|
|
816
|
-
}
|
|
817
|
-
},
|
|
818
|
-
};
|
|
909
|
+
await waitUntilComplete();
|
|
910
|
+
});
|
|
819
911
|
}
|
|
820
912
|
/**
|
|
821
913
|
* Runs a callback with a lazy ChatWriter, flushing the stream after completion.
|
|
@@ -968,7 +1060,7 @@ async function waitOnChatRoute(route, options) {
|
|
|
968
1060
|
return tracer_js_1.tracer.startActiveSpan(options.spanName ?? `chat.${route}.wait()`, async (span) => {
|
|
969
1061
|
const idleMs = (options.idleTimeoutInSeconds ?? 0) * 1000;
|
|
970
1062
|
if (idleMs > 0) {
|
|
971
|
-
const warm = await router.next(route, { timeoutMs: idleMs });
|
|
1063
|
+
const warm = await (0, sessionTracing_js_1.traceSessionIdle)(session.id, idleMs / 1000, () => router.next(route, { timeoutMs: idleMs }));
|
|
972
1064
|
if (warm) {
|
|
973
1065
|
span.setAttribute("wait.resolved", "idle");
|
|
974
1066
|
return { ok: true, output: warm.data, record: warm };
|
|
@@ -1022,10 +1114,6 @@ async function waitOnChatRoute(route, options) {
|
|
|
1022
1114
|
session: session.id,
|
|
1023
1115
|
io: "in",
|
|
1024
1116
|
route,
|
|
1025
|
-
...(0, v3_1.accessoryAttributes)({
|
|
1026
|
-
items: [{ text: `${session.id}.in:${route}`, variant: "normal" }],
|
|
1027
|
-
style: "codepath",
|
|
1028
|
-
}),
|
|
1029
1117
|
},
|
|
1030
1118
|
});
|
|
1031
1119
|
}
|
|
@@ -1701,6 +1789,14 @@ const chatHandoverPartialKey = locals_js_1.locals.create("chat.handoverPartial")
|
|
|
1701
1789
|
* @internal
|
|
1702
1790
|
*/
|
|
1703
1791
|
const chatHandoverMessageIdKey = locals_js_1.locals.create("chat.handoverMessageId");
|
|
1792
|
+
/**
|
|
1793
|
+
* The model messages a turn-0 head-start splice contributed to the model lane,
|
|
1794
|
+
* keyed by the UI message id it was synthesized under. When the agent's response
|
|
1795
|
+
* completes that message under the same id, this is the run to replace: the UI
|
|
1796
|
+
* form alone converts to something else (its pending tool calls drop out, the
|
|
1797
|
+
* approval round was never on it), so it cannot locate the run itself.
|
|
1798
|
+
*/
|
|
1799
|
+
const chatHandoverSplicedRunKey = locals_js_1.locals.create("chat.handoverSplicedRun");
|
|
1704
1800
|
/**
|
|
1705
1801
|
* Run-scoped slot indicating that the customer's step-1 head-start
|
|
1706
1802
|
* response is the FINAL turn response. When true, turn 0 runs through
|
|
@@ -1784,16 +1880,19 @@ function synthesizeHandoverUIMessage(partial, messageId) {
|
|
|
1784
1880
|
*/
|
|
1785
1881
|
function spliceHandoverPartial(modelMessages, uiMessages, signal) {
|
|
1786
1882
|
if (!signal.partialAssistantMessage || signal.partialAssistantMessage.length === 0) {
|
|
1787
|
-
return;
|
|
1883
|
+
return undefined;
|
|
1788
1884
|
}
|
|
1789
1885
|
// Skip if the hydrated chain already persisted the partial under this id.
|
|
1790
1886
|
const alreadyInChain = signal.messageId !== undefined && uiMessages.some((m) => m.id === signal.messageId);
|
|
1791
1887
|
if (alreadyInChain)
|
|
1792
|
-
return;
|
|
1793
|
-
|
|
1888
|
+
return undefined;
|
|
1889
|
+
const run = [...signal.partialAssistantMessage];
|
|
1890
|
+
modelMessages.push(...run);
|
|
1794
1891
|
const partialUI = synthesizeHandoverUIMessage(signal.partialAssistantMessage, signal.messageId);
|
|
1795
|
-
if (partialUI)
|
|
1796
|
-
|
|
1892
|
+
if (!partialUI)
|
|
1893
|
+
return undefined;
|
|
1894
|
+
uiMessages.push(partialUI);
|
|
1895
|
+
return { id: partialUI.id, run };
|
|
1797
1896
|
}
|
|
1798
1897
|
/**
|
|
1799
1898
|
* Per-turn background context queue. Messages added via `chat.backgroundWork.inject()`
|
|
@@ -2399,29 +2498,19 @@ const chatTurnNewUIMessagesKey = locals_js_1.locals.create("chat.turnNewUIMessag
|
|
|
2399
2498
|
const chatPendingSteerKey = locals_js_1.locals.create("chat.pendingSteer");
|
|
2400
2499
|
/** @internal — IDs of messages that were successfully injected via prepareStep */
|
|
2401
2500
|
const chatInjectedMessageIdsKey = locals_js_1.locals.create("chat.injectedMessageIds");
|
|
2402
|
-
|
|
2403
|
-
const
|
|
2404
|
-
|
|
2405
|
-
|
|
2406
|
-
|
|
2407
|
-
|
|
2408
|
-
|
|
2409
|
-
|
|
2410
|
-
|
|
2411
|
-
|
|
2412
|
-
|
|
2413
|
-
}
|
|
2414
|
-
|
|
2415
|
-
* Queue a chunk for accumulation into the response message (if it's a non-transient data part).
|
|
2416
|
-
* Called by `chat.response.write()` and `ChatWriter.write()`.
|
|
2417
|
-
* @internal
|
|
2418
|
-
*/
|
|
2419
|
-
function queueResponsePart(part) {
|
|
2420
|
-
if (!isNonTransientDataPart(part))
|
|
2421
|
-
return;
|
|
2422
|
-
const parts = locals_js_1.locals.get(chatResponsePartsKey) ?? [];
|
|
2423
|
-
parts.push(part);
|
|
2424
|
-
locals_js_1.locals.set(chatResponsePartsKey, parts);
|
|
2501
|
+
const chatManagedResponseKey = locals_js_1.locals.create("chat.managedResponse");
|
|
2502
|
+
const chatManagedResponseActiveKey = locals_js_1.locals.create("chat.managedResponseActive");
|
|
2503
|
+
const chatSteeringInjectionsKey = locals_js_1.locals.create("chat.steeringInjections");
|
|
2504
|
+
function managedResponse() {
|
|
2505
|
+
let response = locals_js_1.locals.get(chatManagedResponseKey);
|
|
2506
|
+
if (!response || response.isClosed) {
|
|
2507
|
+
response = new managedChatResponse_js_1.ManagedChatResponse(async (stream) => {
|
|
2508
|
+
const { waitUntilComplete } = chatStream.pipe(stream, { spanName: "managed chat response" });
|
|
2509
|
+
await waitUntilComplete();
|
|
2510
|
+
});
|
|
2511
|
+
locals_js_1.locals.set(chatManagedResponseKey, response);
|
|
2512
|
+
}
|
|
2513
|
+
return response;
|
|
2425
2514
|
}
|
|
2426
2515
|
/**
|
|
2427
2516
|
* Check that no tool calls are in-flight in a step's content.
|
|
@@ -2653,6 +2742,7 @@ async function chatCompact(messages, steps, options) {
|
|
|
2653
2742
|
locals_js_1.locals.set(chatCompactionStateKey, {
|
|
2654
2743
|
summary,
|
|
2655
2744
|
baseResponseMessageCount: currentStep.response.messages.length,
|
|
2745
|
+
baseResponseStepCount: steps.length,
|
|
2656
2746
|
});
|
|
2657
2747
|
// Set model-only override — UI messages stay intact for persistence.
|
|
2658
2748
|
// The summary becomes the model message history for the next turn,
|
|
@@ -2772,6 +2862,8 @@ function modelFormOf(m, batch, injected) {
|
|
|
2772
2862
|
* @internal
|
|
2773
2863
|
*/
|
|
2774
2864
|
async function drainSteeringQueue(config, messages, steps, queueOverride) {
|
|
2865
|
+
if (steps.length === 0)
|
|
2866
|
+
managedResponse().beginGeneration();
|
|
2775
2867
|
const queue = queueOverride ?? locals_js_1.locals.get(chatSteeringQueueKey);
|
|
2776
2868
|
if (!queue || queue.length === 0)
|
|
2777
2869
|
return EMPTY_DRAIN;
|
|
@@ -2904,31 +2996,24 @@ async function drainSteeringQueue(config, messages, steps, queueOverride) {
|
|
|
2904
2996
|
}
|
|
2905
2997
|
locals_js_1.locals.set(chatPendingSteerKey, pendingSteer);
|
|
2906
2998
|
}
|
|
2907
|
-
// Write injection confirmation chunk to the stream so the frontend
|
|
2908
|
-
// knows which messages were injected and where in the response.
|
|
2909
2999
|
if (injected.length > 0) {
|
|
2910
|
-
|
|
2911
|
-
|
|
2912
|
-
|
|
2913
|
-
|
|
2914
|
-
|
|
2915
|
-
|
|
2916
|
-
|
|
2917
|
-
|
|
2918
|
-
|
|
2919
|
-
|
|
2920
|
-
|
|
2921
|
-
|
|
2922
|
-
|
|
2923
|
-
|
|
2924
|
-
|
|
2925
|
-
|
|
2926
|
-
|
|
2927
|
-
await waitUntilComplete();
|
|
2928
|
-
}
|
|
2929
|
-
catch {
|
|
2930
|
-
/* non-fatal — stream write failed */
|
|
2931
|
-
}
|
|
3000
|
+
const id = (0, ai_runtime_js_1.generateId)();
|
|
3001
|
+
const messageIds = claimedUIMessages.map((m) => m.id);
|
|
3002
|
+
const injections = locals_js_1.locals.get(chatSteeringInjectionsKey) ?? new Map();
|
|
3003
|
+
injections.set(id, { id, messageIds, messages: injected });
|
|
3004
|
+
locals_js_1.locals.set(chatSteeringInjectionsKey, injections);
|
|
3005
|
+
const response = managedResponse();
|
|
3006
|
+
// prepareStep can run ahead of the UI consumer. Admit the marker only
|
|
3007
|
+
// once the actual preceding finish-step chunk has entered this stream.
|
|
3008
|
+
response.afterStep(steps.length);
|
|
3009
|
+
response.writeData({
|
|
3010
|
+
type: ai_shared_js_1.PENDING_MESSAGE_INJECTED_TYPE,
|
|
3011
|
+
id,
|
|
3012
|
+
data: {
|
|
3013
|
+
messageIds,
|
|
3014
|
+
messages: claimedUIMessages.map((m) => ({ id: m.id, text: textOfUIMessage(m) })),
|
|
3015
|
+
},
|
|
3016
|
+
});
|
|
2932
3017
|
}
|
|
2933
3018
|
// Fire onInjected callback
|
|
2934
3019
|
if (config.onInjected && injected.length > 0) {
|
|
@@ -3229,7 +3314,7 @@ function buildManagedStreamTextOptions(options, config) {
|
|
|
3229
3314
|
* regenerate needs: without it a regenerated answer can call nothing.
|
|
3230
3315
|
*/
|
|
3231
3316
|
tools: (tools ?? agentTools),
|
|
3232
|
-
});
|
|
3317
|
+
}, false);
|
|
3233
3318
|
const promptSystem = locals_js_1.locals.get(chatPromptKey)?.text;
|
|
3234
3319
|
/**
|
|
3235
3320
|
* Two managed sources conflict too, not only a caller against a managed one.
|
|
@@ -3256,6 +3341,9 @@ function buildManagedStreamTextOptions(options, config) {
|
|
|
3256
3341
|
return { ...(first ?? {}), ...(second ?? {}) };
|
|
3257
3342
|
};
|
|
3258
3343
|
}
|
|
3344
|
+
if (typeof managed.prepareStep === "function") {
|
|
3345
|
+
managed.prepareStep = (0, steeringContext_js_1.retainStepMessages)(managed.prepareStep);
|
|
3346
|
+
}
|
|
3259
3347
|
return { ...managed, ...rest };
|
|
3260
3348
|
}
|
|
3261
3349
|
/** @internal Test hook for {@link buildManagedStreamTextOptions}. */
|
|
@@ -3271,7 +3359,7 @@ function createBoundStreamText(registry, agentSystem, agentCacheControl, agentSy
|
|
|
3271
3359
|
}));
|
|
3272
3360
|
return bound;
|
|
3273
3361
|
}
|
|
3274
|
-
function toStreamTextOptions(options) {
|
|
3362
|
+
function toStreamTextOptions(options, retain = true) {
|
|
3275
3363
|
const agentDefaults = locals_js_1.locals.get(chatAgentManagedConfigKey);
|
|
3276
3364
|
if (agentDefaults) {
|
|
3277
3365
|
options = {
|
|
@@ -3478,6 +3566,9 @@ function toStreamTextOptions(options) {
|
|
|
3478
3566
|
return resultMessages ? { messages: resultMessages } : undefined;
|
|
3479
3567
|
};
|
|
3480
3568
|
}
|
|
3569
|
+
if (retain && typeof result.prepareStep === "function") {
|
|
3570
|
+
result.prepareStep = (0, steeringContext_js_1.retainStepMessages)(result.prepareStep);
|
|
3571
|
+
}
|
|
3481
3572
|
return result;
|
|
3482
3573
|
}
|
|
3483
3574
|
const actionTurnBrand = Symbol.for("trigger.dev/chat/actionTurn");
|
|
@@ -3523,9 +3614,16 @@ function isActionTurn(value) {
|
|
|
3523
3614
|
* tail does not match the old message's conversion, nothing is changed and
|
|
3524
3615
|
* `false` is returned so the caller can fall back to a full reconversion.
|
|
3525
3616
|
*/
|
|
3526
|
-
async function replaceModelRun(lane, oldUi, newUi, tailAfter) {
|
|
3527
|
-
const oldRun = await toModelMessages([stripProviderMetadata(oldUi)]);
|
|
3617
|
+
async function replaceModelRun(lane, oldUi, newUi, tailAfter, knownOldRun) {
|
|
3618
|
+
const oldRun = knownOldRun ?? (await toModelMessages([stripProviderMetadata(oldUi)]));
|
|
3528
3619
|
const newRun = await toModelMessages([stripProviderMetadata(newUi)]);
|
|
3620
|
+
// A message that converts to nothing (a pending tool call with no output yet,
|
|
3621
|
+
// which `ignoreIncompleteToolCalls` drops) locates no run in the lane. Matching
|
|
3622
|
+
// an empty slice would splice the new run in without removing what the message
|
|
3623
|
+
// actually contributed, such as a spliced head-start partial, and the lane would
|
|
3624
|
+
// then carry the same tool call twice.
|
|
3625
|
+
if (oldRun.length === 0)
|
|
3626
|
+
return false;
|
|
3529
3627
|
const end = lane.length - tailAfter;
|
|
3530
3628
|
const start = end - oldRun.length;
|
|
3531
3629
|
if (start < 0 || end > lane.length)
|
|
@@ -3605,7 +3703,7 @@ function isReadableStream(value) {
|
|
|
3605
3703
|
* }
|
|
3606
3704
|
* ```
|
|
3607
3705
|
*/
|
|
3608
|
-
async function pipeChat(source, options) {
|
|
3706
|
+
async function pipeChat(source, options, capture) {
|
|
3609
3707
|
locals_js_1.locals.set(chatPipeCountKey, (locals_js_1.locals.get(chatPipeCountKey) ?? 0) + 1);
|
|
3610
3708
|
let stream;
|
|
3611
3709
|
if (isUIMessageStreamable(source)) {
|
|
@@ -3634,8 +3732,18 @@ async function pipeChat(source, options) {
|
|
|
3634
3732
|
// accepts opaque UIMessageStreamable / raw iterables whose element
|
|
3635
3733
|
// type we don't know at compile time. Cast — runtime behaviour is
|
|
3636
3734
|
// identical (bytes go to session.out either way).
|
|
3637
|
-
|
|
3638
|
-
|
|
3735
|
+
if (capture) {
|
|
3736
|
+
const response = managedResponse();
|
|
3737
|
+
response.seed(capture.originalMessages);
|
|
3738
|
+
await response.pipe(stream, options?.signal);
|
|
3739
|
+
}
|
|
3740
|
+
else {
|
|
3741
|
+
// Raw/manual pipes intentionally do not feed the managed capture. Never
|
|
3742
|
+
// leave injection events waiting for finish-step chunks it cannot see.
|
|
3743
|
+
managedResponse().useRawPipe();
|
|
3744
|
+
const { waitUntilComplete } = chatStream.pipe(stream, pipeOptions);
|
|
3745
|
+
await waitUntilComplete();
|
|
3746
|
+
}
|
|
3639
3747
|
}
|
|
3640
3748
|
function chatCustomAgent(options) {
|
|
3641
3749
|
const { clientDataSchema, onClientDataValidationError, clientDataReportErrorAt, run: userRun, ...restOptions } = options;
|
|
@@ -3888,11 +3996,13 @@ function chatAgent(options) {
|
|
|
3888
3996
|
if (!pending || pending.length === 0)
|
|
3889
3997
|
return [];
|
|
3890
3998
|
locals_js_1.locals.set(chatPendingSteerKey, []);
|
|
3891
|
-
|
|
3999
|
+
const inlineIds = new Set((0, steeringContext_js_1.steeringMarkers)(options?.response ? [options.response] : []).flatMap((m) => m.messageIds));
|
|
4000
|
+
const standalone = pending.filter((entry) => !inlineIds.has(entry.ui.id));
|
|
4001
|
+
for (const entry of standalone) {
|
|
3892
4002
|
accumulatedMessages.push(...entry.model);
|
|
3893
4003
|
options?.turnNew?.push(...entry.model);
|
|
3894
4004
|
}
|
|
3895
|
-
return
|
|
4005
|
+
return standalone;
|
|
3896
4006
|
};
|
|
3897
4007
|
// Accumulated UI messages for persistence. Mirrors the model accumulator
|
|
3898
4008
|
// but in frontend-friendly UIMessage format (with parts, id, etc.).
|
|
@@ -3955,6 +4065,17 @@ function chatAgent(options) {
|
|
|
3955
4065
|
* keeps an action's write cursor-neutral.
|
|
3956
4066
|
*/
|
|
3957
4067
|
let lastSnapshotOutEventId;
|
|
4068
|
+
/**
|
|
4069
|
+
* The `lastInEventId` the most recent snapshot carried.
|
|
4070
|
+
*
|
|
4071
|
+
* A turn-start save happens after the incoming message has been handed to
|
|
4072
|
+
* the turn loop, so the router's live resume floor has already advanced
|
|
4073
|
+
* past it. Persisting that floor before the turn runs would let the next
|
|
4074
|
+
* boot resume past a message this run never answered, which is exactly
|
|
4075
|
+
* what a deferred or recovered message depends on. Turn-start carries
|
|
4076
|
+
* this instead.
|
|
4077
|
+
*/
|
|
4078
|
+
let lastSnapshotInEventId;
|
|
3958
4079
|
const storageTrigger = (trigger) => trigger === "regenerate-message"
|
|
3959
4080
|
? "regenerate-message"
|
|
3960
4081
|
: trigger === "action" || trigger === "action-turn"
|
|
@@ -3968,13 +4089,17 @@ function chatAgent(options) {
|
|
|
3968
4089
|
*/
|
|
3969
4090
|
/** The runtime's opaque state as of the last save; carried on every changeset's transcript. */
|
|
3970
4091
|
let transcriptState = null;
|
|
3971
|
-
|
|
4092
|
+
let transcriptSaveChain = Promise.resolve();
|
|
4093
|
+
const runSaveTranscript = async (opts) => {
|
|
3972
4094
|
const { changes, shadow } = (0, transcriptStorage_js_1.diffTranscript)(transcriptShadow, opts.messages, {
|
|
3973
4095
|
nonFinalIds: opts.nonFinalIds,
|
|
3974
4096
|
});
|
|
3975
4097
|
const throughId = opts.messages.at(-1)?.id ?? "";
|
|
3976
4098
|
const queued = locals_js_1.locals.get(chatBackgroundQueueKey) ?? [];
|
|
3977
|
-
const
|
|
4099
|
+
const transcriptIds = new Set(opts.messages.map((message) => message.id));
|
|
4100
|
+
const markerIds = new Set((0, steeringContext_js_1.steeringMarkers)(opts.messages).map((marker) => marker.id));
|
|
4101
|
+
const steering = [...(locals_js_1.locals.get(chatSteeringInjectionsKey)?.values() ?? [])].filter((entry) => markerIds.has(entry.id) || entry.messageIds.some((id) => transcriptIds.has(id)));
|
|
4102
|
+
const runtimeState = laneCompacted || laneInjections.length > 0 || queued.length > 0 || steering.length > 0
|
|
3978
4103
|
? {
|
|
3979
4104
|
v: 1,
|
|
3980
4105
|
...(laneCompacted
|
|
@@ -3987,13 +4112,21 @@ function chatAgent(options) {
|
|
|
3987
4112
|
: {}),
|
|
3988
4113
|
...(laneInjections.length > 0 ? { injections: laneInjections } : {}),
|
|
3989
4114
|
...(queued.length > 0 ? { queued: [...queued] } : {}),
|
|
4115
|
+
...(steering.length > 0 ? { steering } : {}),
|
|
3990
4116
|
}
|
|
3991
4117
|
: null;
|
|
3992
4118
|
if (runtimeState !== null || persistedStateSet) {
|
|
3993
4119
|
changes.push({ op: "state", value: runtimeState });
|
|
3994
4120
|
}
|
|
4121
|
+
if (opts.skipIfUnchanged && changes.length === 0)
|
|
4122
|
+
return;
|
|
3995
4123
|
transcriptState = runtimeState;
|
|
3996
|
-
const
|
|
4124
|
+
const liveInCursor = chatInputRouter().resumeFloor();
|
|
4125
|
+
const inCursor = opts.carryInCursor
|
|
4126
|
+
? lastSnapshotInEventId
|
|
4127
|
+
: liveInCursor !== undefined
|
|
4128
|
+
? String(liveInCursor)
|
|
4129
|
+
: undefined;
|
|
3997
4130
|
await transcriptStorage.save({
|
|
3998
4131
|
chatId: payload.chatId,
|
|
3999
4132
|
clientData: opts.clientData,
|
|
@@ -4014,12 +4147,28 @@ function chatAgent(options) {
|
|
|
4014
4147
|
},
|
|
4015
4148
|
cursors: {
|
|
4016
4149
|
lastOutEventId: opts.lastOutEventId,
|
|
4017
|
-
lastInEventId: inCursor
|
|
4150
|
+
lastInEventId: inCursor,
|
|
4018
4151
|
},
|
|
4019
4152
|
});
|
|
4020
4153
|
transcriptShadow = shadow;
|
|
4154
|
+
lastSnapshotInEventId = inCursor;
|
|
4021
4155
|
persistedStateSet = runtimeState !== null;
|
|
4022
4156
|
};
|
|
4157
|
+
/**
|
|
4158
|
+
* Serialise every save onto one chain. `runSaveTranscript` derives its
|
|
4159
|
+
* changeset from `transcriptShadow` and only advances it once the write
|
|
4160
|
+
* lands, so two overlapping saves would diff against stale state. The
|
|
4161
|
+
* message list is copied on the way in because the accumulator keeps
|
|
4162
|
+
* mutating while a queued save waits its turn. A rejection is handed to
|
|
4163
|
+
* the caller but never poisons the chain.
|
|
4164
|
+
*/
|
|
4165
|
+
const saveTranscript = (opts) => {
|
|
4166
|
+
const queued = { ...opts, messages: [...opts.messages] };
|
|
4167
|
+
const run = () => runSaveTranscript(queued);
|
|
4168
|
+
const next = transcriptSaveChain.then(run, run);
|
|
4169
|
+
transcriptSaveChain = next.then(() => undefined, () => undefined);
|
|
4170
|
+
return next;
|
|
4171
|
+
};
|
|
4023
4172
|
/**
|
|
4024
4173
|
* Persist the accumulator outside a turn.
|
|
4025
4174
|
*
|
|
@@ -4126,6 +4275,7 @@ function chatAgent(options) {
|
|
|
4126
4275
|
// turn (chain self-bootstraps from turn 2), so this is purely an
|
|
4127
4276
|
// optimization to keep continuation runs bounded from the first turn.
|
|
4128
4277
|
lastSnapshotOutEventId = bootSnapshot?.lastOutEventId;
|
|
4278
|
+
lastSnapshotInEventId = bootSnapshot?.lastInEventId;
|
|
4129
4279
|
if (bootSnapshot?.lastOutEventId !== undefined) {
|
|
4130
4280
|
const seeded = Number.parseInt(bootSnapshot.lastOutEventId, 10);
|
|
4131
4281
|
if (Number.isFinite(seeded)) {
|
|
@@ -4432,6 +4582,8 @@ function chatAgent(options) {
|
|
|
4432
4582
|
}
|
|
4433
4583
|
try {
|
|
4434
4584
|
const bootRuntimeState = (0, transcriptStorage_js_1.parseTranscriptRuntimeState)(bootTranscriptState);
|
|
4585
|
+
locals_js_1.locals.set(chatSteeringInjectionsKey, new Map((bootRuntimeState?.steering ?? []).map((entry) => [entry.id, entry])));
|
|
4586
|
+
locals_js_1.locals.set(chatCurrentUIMessagesKey, accumulatedUIMessages);
|
|
4435
4587
|
const restored = await (0, transcriptStorage_js_1.restoreModelLane)(accumulatedUIMessages, bootRuntimeState, (messages) => toModelMessages(messages));
|
|
4436
4588
|
accumulatedMessages = restored.messages;
|
|
4437
4589
|
laneCompacted = restored.compacted;
|
|
@@ -4613,7 +4765,7 @@ function chatAgent(options) {
|
|
|
4613
4765
|
const preloadResult = await messagesInput.waitWithIdleTimeout({
|
|
4614
4766
|
idleTimeoutInSeconds: effectivePreloadIdleTimeout,
|
|
4615
4767
|
timeout: effectivePreloadTimeout,
|
|
4616
|
-
spanName: "
|
|
4768
|
+
spanName: "first message",
|
|
4617
4769
|
skipSuspend: exitAfterPreloadIdle,
|
|
4618
4770
|
onSuspend: onChatSuspend
|
|
4619
4771
|
? async () => {
|
|
@@ -4765,7 +4917,7 @@ function chatAgent(options) {
|
|
|
4765
4917
|
const continuationResult = await messagesInput.waitWithIdleTimeout({
|
|
4766
4918
|
idleTimeoutInSeconds: effectiveIdleTimeout,
|
|
4767
4919
|
timeout: effectiveTurnTimeout,
|
|
4768
|
-
spanName: "
|
|
4920
|
+
spanName: "first message (continuation)",
|
|
4769
4921
|
onSuspend: onChatSuspend
|
|
4770
4922
|
? async () => {
|
|
4771
4923
|
await tracer_js_1.tracer.startActiveSpan("onChatSuspend()", async () => {
|
|
@@ -4875,10 +5027,13 @@ function chatAgent(options) {
|
|
|
4875
5027
|
// (errors are caught by the outer try/catch which writes an error chunk)
|
|
4876
5028
|
locals_js_1.locals.set(chatPipeCountKey, 0);
|
|
4877
5029
|
locals_js_1.locals.set(chatDeferKey, new Set());
|
|
5030
|
+
locals_js_1.locals.set(chatOutGateKey, createChatOutGate());
|
|
4878
5031
|
locals_js_1.locals.set(chatCompactionStateKey, undefined);
|
|
4879
5032
|
locals_js_1.locals.set(chatSteeringQueueKey, []);
|
|
4880
5033
|
locals_js_1.locals.set(chatPendingBackgroundKey, []);
|
|
4881
|
-
locals_js_1.locals.
|
|
5034
|
+
await locals_js_1.locals.get(chatManagedResponseKey)?.close();
|
|
5035
|
+
locals_js_1.locals.set(chatManagedResponseKey, undefined);
|
|
5036
|
+
locals_js_1.locals.set(chatManagedResponseActiveKey, true);
|
|
4882
5037
|
// NOTE: chatBackgroundQueueKey is NOT reset here — messages injected
|
|
4883
5038
|
// by deferred work from the previous turn's onTurnComplete need to
|
|
4884
5039
|
// survive into the next turn. The queue is drained before run().
|
|
@@ -5042,7 +5197,7 @@ function chatAgent(options) {
|
|
|
5042
5197
|
if (actionOverride) {
|
|
5043
5198
|
locals_js_1.locals.set(chatOverrideMessagesKey, undefined);
|
|
5044
5199
|
accumulatedUIMessages = [...actionOverride];
|
|
5045
|
-
accumulatedMessages = await toModelMessages(actionOverride);
|
|
5200
|
+
accumulatedMessages = await toModelMessages(actionOverride, actionOverride);
|
|
5046
5201
|
laneCompacted = false;
|
|
5047
5202
|
laneInjections = [];
|
|
5048
5203
|
locals_js_1.locals.set(chatCurrentUIMessagesKey, accumulatedUIMessages);
|
|
@@ -5200,7 +5355,7 @@ function chatAgent(options) {
|
|
|
5200
5355
|
accumulatedUIMessages[accumulatedUIMessages.length - 1].role !== "user") {
|
|
5201
5356
|
accumulatedUIMessages.pop();
|
|
5202
5357
|
}
|
|
5203
|
-
accumulatedMessages = await toModelMessages(accumulatedUIMessages);
|
|
5358
|
+
accumulatedMessages = await toModelMessages(accumulatedUIMessages, accumulatedUIMessages);
|
|
5204
5359
|
laneCompacted = false;
|
|
5205
5360
|
laneInjections = [];
|
|
5206
5361
|
}
|
|
@@ -5252,7 +5407,7 @@ function chatAgent(options) {
|
|
|
5252
5407
|
}
|
|
5253
5408
|
if (!inPlace) {
|
|
5254
5409
|
v3_1.logger.warn("chat.agent: replaced message not found at the model lane tail; reconverting the lane");
|
|
5255
|
-
accumulatedMessages = await toModelMessages(accumulatedUIMessages);
|
|
5410
|
+
accumulatedMessages = await toModelMessages(accumulatedUIMessages, accumulatedUIMessages);
|
|
5256
5411
|
laneCompacted = false;
|
|
5257
5412
|
laneInjections = [];
|
|
5258
5413
|
}
|
|
@@ -5293,10 +5448,12 @@ function chatAgent(options) {
|
|
|
5293
5448
|
// `UIMessageStreamError: No tool invocation found`.
|
|
5294
5449
|
const pendingHandoverPartial = locals_js_1.locals.get(chatHandoverPartialKey);
|
|
5295
5450
|
if (pendingHandoverPartial && pendingHandoverPartial.length > 0) {
|
|
5296
|
-
spliceHandoverPartial(accumulatedMessages, accumulatedUIMessages, {
|
|
5451
|
+
const spliced = spliceHandoverPartial(accumulatedMessages, accumulatedUIMessages, {
|
|
5297
5452
|
partialAssistantMessage: pendingHandoverPartial,
|
|
5298
5453
|
messageId: locals_js_1.locals.get(chatHandoverMessageIdKey),
|
|
5299
5454
|
});
|
|
5455
|
+
if (spliced)
|
|
5456
|
+
locals_js_1.locals.set(chatHandoverSplicedRunKey, spliced);
|
|
5300
5457
|
locals_js_1.locals.set(chatHandoverPartialKey, []); // consume once
|
|
5301
5458
|
splicedHandoverPartial = true;
|
|
5302
5459
|
}
|
|
@@ -5363,6 +5520,23 @@ function chatAgent(options) {
|
|
|
5363
5520
|
// A no-op turn skips this block, and with it `followSessionPin`:
|
|
5364
5521
|
// there is nothing to answer, so nothing to hand over.
|
|
5365
5522
|
if ((!isAction || actionTurn) && !isNoOpTurn) {
|
|
5523
|
+
if (!hydrateMessages) {
|
|
5524
|
+
chatDeferBeforeOutput(saveTranscript({
|
|
5525
|
+
reason: "turn-start",
|
|
5526
|
+
messages: accumulatedUIMessages,
|
|
5527
|
+
turn,
|
|
5528
|
+
trigger: storageTrigger(currentWirePayload.trigger),
|
|
5529
|
+
clientData,
|
|
5530
|
+
lastOutEventId: lastSnapshotOutEventId,
|
|
5531
|
+
skipIfUnchanged: true,
|
|
5532
|
+
carryInCursor: true,
|
|
5533
|
+
}).catch((error) => {
|
|
5534
|
+
v3_1.logger.warn("chat.agent: turn-start transcript write failed; a reload mid-answer may not show the message being answered", {
|
|
5535
|
+
error: error instanceof Error ? error.message : String(error),
|
|
5536
|
+
sessionId: sessionIdForSnapshot,
|
|
5537
|
+
});
|
|
5538
|
+
}));
|
|
5539
|
+
}
|
|
5366
5540
|
// Mint a scoped public access token once per turn, reused for
|
|
5367
5541
|
// onChatStart, onTurnStart, onTurnComplete, and the turn-complete chunk.
|
|
5368
5542
|
const currentRunId = ctx.run.id;
|
|
@@ -5450,7 +5624,7 @@ function chatAgent(options) {
|
|
|
5450
5624
|
if (turnStartOverride) {
|
|
5451
5625
|
locals_js_1.locals.set(chatOverrideMessagesKey, undefined);
|
|
5452
5626
|
accumulatedUIMessages = [...turnStartOverride];
|
|
5453
|
-
accumulatedMessages = await toModelMessages(turnStartOverride);
|
|
5627
|
+
accumulatedMessages = await toModelMessages(turnStartOverride, turnStartOverride);
|
|
5454
5628
|
laneCompacted = false;
|
|
5455
5629
|
laneInjections = [];
|
|
5456
5630
|
locals_js_1.locals.set(chatCurrentUIMessagesKey, accumulatedUIMessages);
|
|
@@ -5530,6 +5704,7 @@ function chatAgent(options) {
|
|
|
5530
5704
|
capturedResponseMessage = lastUI;
|
|
5531
5705
|
capturedFinishReason = "stop";
|
|
5532
5706
|
}
|
|
5707
|
+
managedResponse().seed(accumulatedUIMessages);
|
|
5533
5708
|
// Don't call userRun. Don't pipe. Skip directly
|
|
5534
5709
|
// to the post-turn flow below.
|
|
5535
5710
|
}
|
|
@@ -5589,7 +5764,7 @@ function chatAgent(options) {
|
|
|
5589
5764
|
await pipeChat(tapUIMessageChunks(uiStream, turnBufferedChunks), {
|
|
5590
5765
|
signal: combinedSignal,
|
|
5591
5766
|
spanName: "stream response",
|
|
5592
|
-
});
|
|
5767
|
+
}, { originalMessages: isActionTurn ? undefined : accumulatedUIMessages });
|
|
5593
5768
|
}
|
|
5594
5769
|
}
|
|
5595
5770
|
catch (error) {
|
|
@@ -5617,6 +5792,10 @@ function chatAgent(options) {
|
|
|
5617
5792
|
new Promise((r) => setTimeout(r, 2_000)),
|
|
5618
5793
|
]);
|
|
5619
5794
|
}
|
|
5795
|
+
capturedResponseMessage =
|
|
5796
|
+
(await locals_js_1.locals.get(chatManagedResponseKey)?.snapshot()) ?? capturedResponseMessage;
|
|
5797
|
+
if (capturedResponseMessage)
|
|
5798
|
+
capturedPartialResponse = capturedResponseMessage;
|
|
5620
5799
|
// Capture token usage from the streamText result (if available).
|
|
5621
5800
|
// totalUsage is a PromiseLike that resolves after the stream is consumed.
|
|
5622
5801
|
// Race with a 2s timeout — on stop-abort the AI SDK's totalUsage
|
|
@@ -5624,6 +5803,7 @@ function chatAgent(options) {
|
|
|
5624
5803
|
// never reports final usage), which would block the turn loop
|
|
5625
5804
|
// from ever firing onTurnComplete / writeTurnComplete.
|
|
5626
5805
|
let turnUsage;
|
|
5806
|
+
let lastStepUsage;
|
|
5627
5807
|
if (runResult != null &&
|
|
5628
5808
|
typeof runResult.totalUsage?.then === "function") {
|
|
5629
5809
|
try {
|
|
@@ -5636,6 +5816,18 @@ function chatAgent(options) {
|
|
|
5636
5816
|
/* non-fatal — usage capture failed */
|
|
5637
5817
|
}
|
|
5638
5818
|
}
|
|
5819
|
+
const lastStepUsagePromise = runResult != null ? runResult.usage : undefined;
|
|
5820
|
+
if (typeof lastStepUsagePromise?.then === "function") {
|
|
5821
|
+
try {
|
|
5822
|
+
lastStepUsage = (await Promise.race([
|
|
5823
|
+
lastStepUsagePromise,
|
|
5824
|
+
new Promise((r) => setTimeout(() => r(undefined), 2_000)),
|
|
5825
|
+
]));
|
|
5826
|
+
}
|
|
5827
|
+
catch {
|
|
5828
|
+
/* non-fatal — usage capture failed */
|
|
5829
|
+
}
|
|
5830
|
+
}
|
|
5639
5831
|
if (turnUsage) {
|
|
5640
5832
|
cumulativeUsage = addUsage(cumulativeUsage, turnUsage);
|
|
5641
5833
|
previousTurnUsage = turnUsage;
|
|
@@ -5686,6 +5878,13 @@ function chatAgent(options) {
|
|
|
5686
5878
|
// Check if compaction set a model-only override (preserves UI messages).
|
|
5687
5879
|
// Apply compactUIMessages/compactModelMessages callbacks if configured.
|
|
5688
5880
|
const modelOnlyOverride = locals_js_1.locals.get(chatOverrideModelMessagesKey);
|
|
5881
|
+
const responseCompaction = modelOnlyOverride
|
|
5882
|
+
? locals_js_1.locals.get(chatCompactionStateKey)
|
|
5883
|
+
: undefined;
|
|
5884
|
+
// Capture the original assistant before compactUIMessages can remove it.
|
|
5885
|
+
const originalResponse = responseCompaction && capturedResponseMessage
|
|
5886
|
+
? accumulatedUIMessages.find((m) => m.id === capturedResponseMessage?.id)
|
|
5887
|
+
: undefined;
|
|
5689
5888
|
if (modelOnlyOverride) {
|
|
5690
5889
|
const compactionSummary = locals_js_1.locals.get(chatCompactionStateKey)?.summary ?? "";
|
|
5691
5890
|
const taskCompactionConfig = locals_js_1.locals.get(chatAgentCompactionKey);
|
|
@@ -5720,6 +5919,7 @@ function chatAgent(options) {
|
|
|
5720
5919
|
// branches below, so a turn that captured no response is covered.
|
|
5721
5920
|
const steerTailThisTurn = reconcilePendingSteer({
|
|
5722
5921
|
turnNew: turnNewModelMessages,
|
|
5922
|
+
response: capturedResponseMessage,
|
|
5723
5923
|
}).reduce((n, e) => n + e.model.length, 0) + reconcilePendingBackground();
|
|
5724
5924
|
// Append the assistant's response (partial or complete) to the accumulator.
|
|
5725
5925
|
// The onFinish callback fires even on abort/stop, so partial responses
|
|
@@ -5742,15 +5942,6 @@ function chatAgent(options) {
|
|
|
5742
5942
|
id: (0, ai_runtime_js_1.generateId)(),
|
|
5743
5943
|
};
|
|
5744
5944
|
}
|
|
5745
|
-
// Append any non-transient data parts queued via chat.response or writer.write()
|
|
5746
|
-
const queuedParts = locals_js_1.locals.get(chatResponsePartsKey);
|
|
5747
|
-
if (queuedParts && queuedParts.length > 0) {
|
|
5748
|
-
capturedResponseMessage = {
|
|
5749
|
-
...capturedResponseMessage,
|
|
5750
|
-
parts: [...capturedResponseMessage.parts, ...queuedParts],
|
|
5751
|
-
};
|
|
5752
|
-
locals_js_1.locals.set(chatResponsePartsKey, []);
|
|
5753
|
-
}
|
|
5754
5945
|
const responseHasContent = capturedResponseMessage.parts.some((part) => part.type !== "step-start");
|
|
5755
5946
|
if (responseHasContent) {
|
|
5756
5947
|
// Tool-approval continuations: the AI SDK reuses the trailing
|
|
@@ -5777,15 +5968,42 @@ function chatAgent(options) {
|
|
|
5777
5968
|
// rationale (TRI-9137).
|
|
5778
5969
|
recordToolCallIdsFromMessage(capturedResponseMessage);
|
|
5779
5970
|
try {
|
|
5971
|
+
const responseForModel = (0, compactionResponse_js_1.responseAfterCompaction)(capturedResponseMessage, responseCompaction?.baseResponseStepCount, originalResponse);
|
|
5972
|
+
// Preserve the complete persistence response, including same-ID
|
|
5973
|
+
// replacements whose old tool parts can contain new results.
|
|
5974
|
+
// Convert prefix and suffix separately so each tool output is
|
|
5975
|
+
// converted once, while only the suffix enters model context.
|
|
5976
|
+
const responsePrefixMessages = responseCompaction
|
|
5977
|
+
? await toModelMessages([
|
|
5978
|
+
stripProviderMetadata({
|
|
5979
|
+
...capturedResponseMessage,
|
|
5980
|
+
parts: capturedResponseMessage.parts.slice(0, capturedResponseMessage.parts.length -
|
|
5981
|
+
responseForModel.parts.length),
|
|
5982
|
+
}),
|
|
5983
|
+
])
|
|
5984
|
+
: [];
|
|
5780
5985
|
const responseModelMessages = await toModelMessages([
|
|
5781
|
-
stripProviderMetadata(
|
|
5986
|
+
stripProviderMetadata(responseForModel),
|
|
5782
5987
|
]);
|
|
5783
|
-
if (
|
|
5988
|
+
if (responseCompaction) {
|
|
5989
|
+
// The summary already replaced the original response, including
|
|
5990
|
+
// a same-ID approval/handover prefix. Replacing its old model run
|
|
5991
|
+
// would miss and fall back to the full, uncompacted UI history.
|
|
5992
|
+
accumulatedMessages.push(...responseModelMessages);
|
|
5993
|
+
locals_js_1.locals.set(chatHandoverSplicedRunKey, undefined);
|
|
5994
|
+
}
|
|
5995
|
+
else if (existingIdx !== -1) {
|
|
5996
|
+
const spliced = locals_js_1.locals.get(chatHandoverSplicedRunKey);
|
|
5997
|
+
const splicedRun = spliced && previousAtIdx && spliced.id === previousAtIdx.id
|
|
5998
|
+
? spliced.run
|
|
5999
|
+
: undefined;
|
|
5784
6000
|
const ok = previousAtIdx !== undefined &&
|
|
5785
|
-
(await replaceModelRun(accumulatedMessages, previousAtIdx, capturedResponseMessage, steerTailThisTurn));
|
|
6001
|
+
(await replaceModelRun(accumulatedMessages, previousAtIdx, capturedResponseMessage, steerTailThisTurn, splicedRun));
|
|
6002
|
+
if (splicedRun)
|
|
6003
|
+
locals_js_1.locals.set(chatHandoverSplicedRunKey, undefined);
|
|
5786
6004
|
if (!ok) {
|
|
5787
6005
|
v3_1.logger.warn("chat.agent: replaced response not found at the model lane tail; reconverting the lane");
|
|
5788
|
-
accumulatedMessages = await toModelMessages(accumulatedUIMessages);
|
|
6006
|
+
accumulatedMessages = await toModelMessages(accumulatedUIMessages, accumulatedUIMessages);
|
|
5789
6007
|
laneCompacted = false;
|
|
5790
6008
|
laneInjections = [];
|
|
5791
6009
|
}
|
|
@@ -5793,7 +6011,7 @@ function chatAgent(options) {
|
|
|
5793
6011
|
else {
|
|
5794
6012
|
accumulatedMessages.push(...responseModelMessages);
|
|
5795
6013
|
}
|
|
5796
|
-
turnNewModelMessages.push(...responseModelMessages);
|
|
6014
|
+
turnNewModelMessages.push(...responsePrefixMessages, ...responseModelMessages);
|
|
5797
6015
|
}
|
|
5798
6016
|
catch {
|
|
5799
6017
|
// Conversion failed — skip accumulation for this turn
|
|
@@ -5803,22 +6021,6 @@ function chatAgent(options) {
|
|
|
5803
6021
|
responseWasSkipped = true;
|
|
5804
6022
|
}
|
|
5805
6023
|
}
|
|
5806
|
-
// If there's no captured response (manual pipe mode) but there are
|
|
5807
|
-
// queued data parts, create a minimal response message to hold them.
|
|
5808
|
-
if (!capturedResponseMessage) {
|
|
5809
|
-
const remainingParts = locals_js_1.locals.get(chatResponsePartsKey);
|
|
5810
|
-
if (remainingParts && remainingParts.length > 0) {
|
|
5811
|
-
capturedResponseMessage = {
|
|
5812
|
-
id: (0, ai_runtime_js_1.generateId)(),
|
|
5813
|
-
role: "assistant",
|
|
5814
|
-
parts: [...remainingParts],
|
|
5815
|
-
};
|
|
5816
|
-
locals_js_1.locals.set(chatResponsePartsKey, []);
|
|
5817
|
-
accumulatedUIMessages.push(capturedResponseMessage);
|
|
5818
|
-
turnNewUIMessages.push(capturedResponseMessage);
|
|
5819
|
-
locals_js_1.locals.set(chatCurrentUIMessagesKey, accumulatedUIMessages);
|
|
5820
|
-
}
|
|
5821
|
-
}
|
|
5822
6024
|
if (capturedResponseMessage) {
|
|
5823
6025
|
responseCommitted = true;
|
|
5824
6026
|
capturedPartialResponse = capturedResponseMessage;
|
|
@@ -5842,12 +6044,14 @@ function chatAgent(options) {
|
|
|
5842
6044
|
const outerCompaction = locals_js_1.locals.get(chatAgentCompactionKey);
|
|
5843
6045
|
const innerCompactionState = locals_js_1.locals.get(chatCompactionStateKey);
|
|
5844
6046
|
if (outerCompaction && !innerCompactionState && turnUsage && !wasStopped) {
|
|
6047
|
+
const contextUsage = lastStepUsage ?? turnUsage;
|
|
5845
6048
|
const shouldTrigger = await outerCompaction.shouldCompact({
|
|
5846
6049
|
messages: accumulatedMessages,
|
|
5847
|
-
totalTokens:
|
|
5848
|
-
inputTokens:
|
|
5849
|
-
outputTokens:
|
|
5850
|
-
usage:
|
|
6050
|
+
totalTokens: contextUsage.totalTokens,
|
|
6051
|
+
inputTokens: contextUsage.inputTokens,
|
|
6052
|
+
outputTokens: contextUsage.outputTokens,
|
|
6053
|
+
usage: contextUsage,
|
|
6054
|
+
turnUsage,
|
|
5851
6055
|
totalUsage: cumulativeUsage,
|
|
5852
6056
|
chatId: currentWirePayload.chatId,
|
|
5853
6057
|
turn,
|
|
@@ -5978,6 +6182,8 @@ function chatAgent(options) {
|
|
|
5978
6182
|
totalUsage: cumulativeUsage,
|
|
5979
6183
|
finishReason: capturedFinishReason,
|
|
5980
6184
|
};
|
|
6185
|
+
const beforeHookRevision = locals_js_1.locals.get(chatManagedResponseKey)?.revision ?? 0;
|
|
6186
|
+
let beforeHookHistoryEdited = false;
|
|
5981
6187
|
// Fire onBeforeTurnComplete — stream is still open so the hook
|
|
5982
6188
|
// can write custom chunks to the frontend (e.g. compaction progress).
|
|
5983
6189
|
if (onBeforeTurnComplete) {
|
|
@@ -5988,9 +6194,10 @@ function chatAgent(options) {
|
|
|
5988
6194
|
// Check if the hook replaced messages (compaction or chat.history)
|
|
5989
6195
|
const override = locals_js_1.locals.get(chatOverrideMessagesKey);
|
|
5990
6196
|
if (override) {
|
|
6197
|
+
beforeHookHistoryEdited = true;
|
|
5991
6198
|
locals_js_1.locals.set(chatOverrideMessagesKey, undefined);
|
|
5992
6199
|
accumulatedUIMessages = [...override];
|
|
5993
|
-
accumulatedMessages = await toModelMessages(override);
|
|
6200
|
+
accumulatedMessages = await toModelMessages(override, override);
|
|
5994
6201
|
laneCompacted = false;
|
|
5995
6202
|
laneInjections = [];
|
|
5996
6203
|
locals_js_1.locals.set(chatCurrentUIMessagesKey, accumulatedUIMessages);
|
|
@@ -6007,35 +6214,33 @@ function chatAgent(options) {
|
|
|
6007
6214
|
},
|
|
6008
6215
|
});
|
|
6009
6216
|
}
|
|
6010
|
-
|
|
6011
|
-
|
|
6012
|
-
|
|
6013
|
-
|
|
6014
|
-
|
|
6015
|
-
|
|
6016
|
-
|
|
6017
|
-
|
|
6018
|
-
|
|
6019
|
-
|
|
6020
|
-
|
|
6021
|
-
|
|
6022
|
-
|
|
6023
|
-
|
|
6024
|
-
|
|
6025
|
-
|
|
6026
|
-
|
|
6027
|
-
|
|
6028
|
-
|
|
6029
|
-
|
|
6030
|
-
|
|
6031
|
-
|
|
6032
|
-
|
|
6033
|
-
|
|
6034
|
-
turnCompleteEvent.responseMessage = capturedResponseMessage;
|
|
6217
|
+
const editedResponse = beforeHookHistoryEdited && capturedResponseMessage
|
|
6218
|
+
? accumulatedUIMessages.find((message) => message.id === capturedResponseMessage?.id)
|
|
6219
|
+
: undefined;
|
|
6220
|
+
const finalManagedResponse = await locals_js_1.locals
|
|
6221
|
+
.get(chatManagedResponseKey)
|
|
6222
|
+
?.snapshot(editedResponse
|
|
6223
|
+
? { message: editedResponse, from: beforeHookRevision }
|
|
6224
|
+
: undefined);
|
|
6225
|
+
if (finalManagedResponse?.parts.some((part) => part.type !== "step-start")) {
|
|
6226
|
+
const idx = accumulatedUIMessages.findIndex((m) => m.id === finalManagedResponse.id);
|
|
6227
|
+
const finalized = (wasStopped ? cleanupAbortedParts(finalManagedResponse) : finalManagedResponse);
|
|
6228
|
+
if (idx !== -1 || responseWasSkipped || !capturedResponseMessage) {
|
|
6229
|
+
if (idx !== -1)
|
|
6230
|
+
accumulatedUIMessages[idx] = finalized;
|
|
6231
|
+
else
|
|
6232
|
+
accumulatedUIMessages.push(finalized);
|
|
6233
|
+
const deltaIdx = turnNewUIMessages.findIndex((m) => m.id === finalized.id);
|
|
6234
|
+
if (deltaIdx !== -1)
|
|
6235
|
+
turnNewUIMessages[deltaIdx] = finalized;
|
|
6236
|
+
else
|
|
6237
|
+
turnNewUIMessages.push(finalized);
|
|
6238
|
+
capturedResponseMessage = finalized;
|
|
6239
|
+
capturedPartialResponse = finalized;
|
|
6240
|
+
turnCompleteEvent.responseMessage = finalized;
|
|
6035
6241
|
turnCompleteEvent.uiMessages = accumulatedUIMessages;
|
|
6036
6242
|
locals_js_1.locals.set(chatCurrentUIMessagesKey, accumulatedUIMessages);
|
|
6037
6243
|
}
|
|
6038
|
-
locals_js_1.locals.set(chatResponsePartsKey, []);
|
|
6039
6244
|
}
|
|
6040
6245
|
settleRecoveredTurn(currentWirePayload);
|
|
6041
6246
|
// Write turn-complete control chunk — closes the frontend stream.
|
|
@@ -6052,7 +6257,7 @@ function chatAgent(options) {
|
|
|
6052
6257
|
if (turnCompleteOverride) {
|
|
6053
6258
|
locals_js_1.locals.set(chatOverrideMessagesKey, undefined);
|
|
6054
6259
|
accumulatedUIMessages = [...turnCompleteOverride];
|
|
6055
|
-
accumulatedMessages = await toModelMessages(turnCompleteOverride);
|
|
6260
|
+
accumulatedMessages = await toModelMessages(turnCompleteOverride, turnCompleteOverride);
|
|
6056
6261
|
laneCompacted = false;
|
|
6057
6262
|
laneInjections = [];
|
|
6058
6263
|
locals_js_1.locals.set(chatCurrentUIMessagesKey, accumulatedUIMessages);
|
|
@@ -6172,7 +6377,7 @@ function chatAgent(options) {
|
|
|
6172
6377
|
const next = await messagesInput.waitWithIdleTimeout({
|
|
6173
6378
|
idleTimeoutInSeconds: effectiveIdleTimeout,
|
|
6174
6379
|
timeout: effectiveTurnTimeout,
|
|
6175
|
-
spanName: "
|
|
6380
|
+
spanName: "next message",
|
|
6176
6381
|
onSuspend: onChatSuspend
|
|
6177
6382
|
? async () => {
|
|
6178
6383
|
await tracer_js_1.tracer.startActiveSpan("onChatSuspend()", async () => {
|
|
@@ -6288,7 +6493,8 @@ function chatAgent(options) {
|
|
|
6288
6493
|
!accumulatedUIMessages.some((m) => m.id === erroredWireMessage.id)
|
|
6289
6494
|
? [...accumulatedUIMessages, erroredWireMessage]
|
|
6290
6495
|
: accumulatedUIMessages;
|
|
6291
|
-
let partialResponse =
|
|
6496
|
+
let partialResponse = (await locals_js_1.locals.get(chatManagedResponseKey)?.snapshot()) ??
|
|
6497
|
+
capturedPartialResponse ??
|
|
6292
6498
|
(await assemblePartialFromChunks(turnBufferedChunks));
|
|
6293
6499
|
if (partialResponse) {
|
|
6294
6500
|
partialResponse = cleanupAbortedParts(partialResponse);
|
|
@@ -6296,24 +6502,21 @@ function chatAgent(options) {
|
|
|
6296
6502
|
let partialIdx = partialResponse?.id
|
|
6297
6503
|
? erroredUIMessages.findIndex((m) => m.id === partialResponse.id)
|
|
6298
6504
|
: -1;
|
|
6299
|
-
if (partialResponse &&
|
|
6505
|
+
if (partialResponse &&
|
|
6506
|
+
capturedPartialResponse === undefined &&
|
|
6507
|
+
!locals_js_1.locals.get(chatManagedResponseKey)?.continues(partialResponse.id) &&
|
|
6508
|
+
partialIdx !== -1) {
|
|
6300
6509
|
partialResponse = undefined;
|
|
6301
6510
|
partialIdx = -1;
|
|
6302
6511
|
}
|
|
6303
6512
|
if (partialResponse && !partialResponse.id) {
|
|
6304
6513
|
partialResponse = { ...partialResponse, id: (0, ai_runtime_js_1.generateId)() };
|
|
6305
6514
|
}
|
|
6306
|
-
if (partialResponse && !responseCommitted) {
|
|
6307
|
-
const queuedParts = locals_js_1.locals.get(chatResponsePartsKey);
|
|
6308
|
-
if (queuedParts && queuedParts.length > 0) {
|
|
6309
|
-
partialResponse = {
|
|
6310
|
-
...partialResponse,
|
|
6311
|
-
parts: [...partialResponse.parts, ...queuedParts],
|
|
6312
|
-
};
|
|
6313
|
-
locals_js_1.locals.set(chatResponsePartsKey, []);
|
|
6314
|
-
}
|
|
6315
|
-
}
|
|
6316
6515
|
const includePartial = partialResponse != null && !responseCommitted;
|
|
6516
|
+
// What the stream left behind, by content. After `onTurnComplete` the
|
|
6517
|
+
// partial is still unfinished only if the message under its id is
|
|
6518
|
+
// byte-for-byte this: a clone keeps it partial, an edit finishes it.
|
|
6519
|
+
const partialFingerprint = includePartial && partialResponse ? (0, transcriptStorage_js_1.fingerprintMessage)(partialResponse) : undefined;
|
|
6317
6520
|
let erroredUIMessagesWithPartial = !includePartial
|
|
6318
6521
|
? erroredUIMessages
|
|
6319
6522
|
: partialIdx === -1
|
|
@@ -6341,7 +6544,7 @@ function chatAgent(options) {
|
|
|
6341
6544
|
};
|
|
6342
6545
|
let erroredNewUIMessages = buildErroredNew();
|
|
6343
6546
|
let erroredNewModelMessages = [];
|
|
6344
|
-
const reconciledSteer = reconcilePendingSteer();
|
|
6547
|
+
const reconciledSteer = reconcilePendingSteer({ response: partialResponse });
|
|
6345
6548
|
const backgroundTailThisTurn = reconcilePendingBackground();
|
|
6346
6549
|
if (!responseCommitted) {
|
|
6347
6550
|
try {
|
|
@@ -6352,14 +6555,7 @@ function chatAgent(options) {
|
|
|
6352
6555
|
* the model received it (what `prepare` produced), matching the
|
|
6353
6556
|
* lane. The wire message and partial are converted as before.
|
|
6354
6557
|
*/
|
|
6355
|
-
|
|
6356
|
-
for (const m of erroredNewUIMessages) {
|
|
6357
|
-
const recorded = steerModelById.get(m.id);
|
|
6358
|
-
if (recorded)
|
|
6359
|
-
erroredNewModelMessages.push(...recorded);
|
|
6360
|
-
else
|
|
6361
|
-
erroredNewModelMessages.push(...(await toModelMessages([stripProviderMetadata(m)])));
|
|
6362
|
-
}
|
|
6558
|
+
erroredNewModelMessages = await toModelMessages(erroredNewUIMessages.map(stripProviderMetadata));
|
|
6363
6559
|
}
|
|
6364
6560
|
if (erroredUIMessagesWithPartial !== accumulatedUIMessages) {
|
|
6365
6561
|
if (partialIdx === -1) {
|
|
@@ -6371,7 +6567,7 @@ function chatAgent(options) {
|
|
|
6371
6567
|
backgroundTailThisTurn);
|
|
6372
6568
|
if (!ok) {
|
|
6373
6569
|
v3_1.logger.warn("chat.agent: replaced partial not found at the model lane tail; reconverting the lane");
|
|
6374
|
-
accumulatedMessages = await toModelMessages(erroredUIMessagesWithPartial);
|
|
6570
|
+
accumulatedMessages = await toModelMessages(erroredUIMessagesWithPartial, erroredUIMessagesWithPartial);
|
|
6375
6571
|
laneCompacted = false;
|
|
6376
6572
|
laneInjections = [];
|
|
6377
6573
|
}
|
|
@@ -6386,6 +6582,11 @@ function chatAgent(options) {
|
|
|
6386
6582
|
erroredNewUIMessages = buildErroredNew().filter((m) => m !== partialResponse);
|
|
6387
6583
|
}
|
|
6388
6584
|
}
|
|
6585
|
+
// An earlier hook that set the history and then threw (which is one way
|
|
6586
|
+
// to get here) left its abandoned edit pending. Discard it before the
|
|
6587
|
+
// failed turn continues, so neither the error-path `onTurnComplete`
|
|
6588
|
+
// below nor the next turn's history reads mistake it for a real edit.
|
|
6589
|
+
locals_js_1.locals.set(chatOverrideMessagesKey, undefined);
|
|
6389
6590
|
if (onTurnComplete) {
|
|
6390
6591
|
try {
|
|
6391
6592
|
await tracer_js_1.tracer.startActiveSpan("onTurnComplete()", async () => {
|
|
@@ -6414,6 +6615,23 @@ function chatAgent(options) {
|
|
|
6414
6615
|
error: turnError,
|
|
6415
6616
|
lastEventId: errorTurnCompleteResult?.lastEventId,
|
|
6416
6617
|
});
|
|
6618
|
+
// The hook may edit the history here too (a failure record, a
|
|
6619
|
+
// card the turn left open). Honour it the way the success path
|
|
6620
|
+
// does, so the edit reaches the accumulator and the save below.
|
|
6621
|
+
const errorTurnOverride = locals_js_1.locals.get(chatOverrideMessagesKey);
|
|
6622
|
+
if (errorTurnOverride) {
|
|
6623
|
+
locals_js_1.locals.set(chatOverrideMessagesKey, undefined);
|
|
6624
|
+
// Convert first: a rejected conversion (a tool's `toModelOutput`
|
|
6625
|
+
// can throw) must leave every lane on the history it had.
|
|
6626
|
+
const overrideUIMessages = [...errorTurnOverride];
|
|
6627
|
+
const overrideModelMessages = await toModelMessages(errorTurnOverride, errorTurnOverride);
|
|
6628
|
+
erroredUIMessagesWithPartial = overrideUIMessages;
|
|
6629
|
+
accumulatedUIMessages = overrideUIMessages;
|
|
6630
|
+
accumulatedMessages = overrideModelMessages;
|
|
6631
|
+
laneCompacted = false;
|
|
6632
|
+
laneInjections = [];
|
|
6633
|
+
locals_js_1.locals.set(chatCurrentUIMessagesKey, accumulatedUIMessages);
|
|
6634
|
+
}
|
|
6417
6635
|
}, {
|
|
6418
6636
|
attributes: {
|
|
6419
6637
|
[v3_1.SemanticInternalAttributes.STYLE_ICON]: "task-hook-onComplete",
|
|
@@ -6427,6 +6645,7 @@ function chatAgent(options) {
|
|
|
6427
6645
|
catch {
|
|
6428
6646
|
// A throwing onTurnComplete on the error path must not crash
|
|
6429
6647
|
// the run — keep the conversation alive for the next message.
|
|
6648
|
+
locals_js_1.locals.set(chatOverrideMessagesKey, undefined);
|
|
6430
6649
|
}
|
|
6431
6650
|
}
|
|
6432
6651
|
// Persist a snapshot so the failed turn's user message isn't
|
|
@@ -6444,7 +6663,10 @@ function chatAgent(options) {
|
|
|
6444
6663
|
trigger: storageTrigger(currentWirePayload.trigger),
|
|
6445
6664
|
clientData: turnClientData,
|
|
6446
6665
|
lastOutEventId: lastSnapshotOutEventId,
|
|
6447
|
-
|
|
6666
|
+
// The partial is non-final only while the message under its id is
|
|
6667
|
+
// still what the stream left behind. A hook that replaced it (a
|
|
6668
|
+
// closed card, a finished body) produced a final message.
|
|
6669
|
+
nonFinalIds: partialStillUnfinished(partialResponse, partialFingerprint, erroredUIMessagesWithPartial),
|
|
6448
6670
|
});
|
|
6449
6671
|
}
|
|
6450
6672
|
catch (error) {
|
|
@@ -6479,7 +6701,7 @@ function chatAgent(options) {
|
|
|
6479
6701
|
const next = await messagesInput.waitWithIdleTimeout({
|
|
6480
6702
|
idleTimeoutInSeconds: effectiveIdleTimeout,
|
|
6481
6703
|
timeout: effectiveTurnTimeout,
|
|
6482
|
-
spanName: "
|
|
6704
|
+
spanName: "next message (after error)",
|
|
6483
6705
|
});
|
|
6484
6706
|
if (!next.ok) {
|
|
6485
6707
|
return; // Timed out — end run gracefully
|
|
@@ -6495,6 +6717,11 @@ function chatAgent(options) {
|
|
|
6495
6717
|
}
|
|
6496
6718
|
finally {
|
|
6497
6719
|
turnMsgSub?.off();
|
|
6720
|
+
locals_js_1.locals.set(chatManagedResponseActiveKey, false);
|
|
6721
|
+
await locals_js_1.locals
|
|
6722
|
+
.get(chatManagedResponseKey)
|
|
6723
|
+
?.close()
|
|
6724
|
+
.catch(() => { });
|
|
6498
6725
|
}
|
|
6499
6726
|
}
|
|
6500
6727
|
}
|
|
@@ -7464,7 +7691,7 @@ async function pipeChatAndCapture(source, options) {
|
|
|
7464
7691
|
await pipeChat(tappedStream, {
|
|
7465
7692
|
signal: options?.signal,
|
|
7466
7693
|
spanName: options?.spanName ?? "stream response",
|
|
7467
|
-
});
|
|
7694
|
+
}, { originalMessages: options?.originalMessages });
|
|
7468
7695
|
// The pipe can drain cleanly on a stop — the source stream just ends
|
|
7469
7696
|
// early — so classify by the signal rather than relying on a throw.
|
|
7470
7697
|
if (options?.signal?.aborted) {
|
|
@@ -7490,6 +7717,9 @@ async function pipeChatAndCapture(source, options) {
|
|
|
7490
7717
|
if (!captured && bufferedChunks.length > 0) {
|
|
7491
7718
|
captured = await assemblePartialFromChunks(bufferedChunks);
|
|
7492
7719
|
}
|
|
7720
|
+
captured = (await locals_js_1.locals.get(chatManagedResponseKey)?.snapshot()) ?? captured;
|
|
7721
|
+
if (!locals_js_1.locals.get(chatTurnContextKey))
|
|
7722
|
+
await locals_js_1.locals.get(chatManagedResponseKey)?.close();
|
|
7493
7723
|
return {
|
|
7494
7724
|
message: captured,
|
|
7495
7725
|
status,
|
|
@@ -7519,8 +7749,11 @@ class ChatMessageAccumulator {
|
|
|
7519
7749
|
modelMessages = [];
|
|
7520
7750
|
uiMessages = [];
|
|
7521
7751
|
_compaction;
|
|
7752
|
+
/** The run a spliced head-start partial contributed, until its response replaces it. */
|
|
7753
|
+
_handoverRun;
|
|
7522
7754
|
_pendingMessages;
|
|
7523
7755
|
_steeringQueue = [];
|
|
7756
|
+
_pendingSteer = [];
|
|
7524
7757
|
constructor(options) {
|
|
7525
7758
|
this._compaction = options?.compaction;
|
|
7526
7759
|
this._pendingMessages = options?.pendingMessages;
|
|
@@ -7562,7 +7795,7 @@ class ChatMessageAccumulator {
|
|
|
7562
7795
|
* `consumeHandover` for the wait+seed+apply convenience.
|
|
7563
7796
|
*/
|
|
7564
7797
|
applyHandover(signal) {
|
|
7565
|
-
spliceHandoverPartial(this.modelMessages, this.uiMessages, signal);
|
|
7798
|
+
this._handoverRun = spliceHandoverPartial(this.modelMessages, this.uiMessages, signal);
|
|
7566
7799
|
}
|
|
7567
7800
|
/**
|
|
7568
7801
|
* One-call `chat.headStart` handover for a custom-agent loop: waits for the
|
|
@@ -7593,6 +7826,19 @@ class ChatMessageAccumulator {
|
|
|
7593
7826
|
return { isFinal: signal.isFinal, skipped: false };
|
|
7594
7827
|
}
|
|
7595
7828
|
async addResponse(response) {
|
|
7829
|
+
const inlineIds = new Set((0, steeringContext_js_1.steeringMarkers)([response]).flatMap((m) => m.messageIds));
|
|
7830
|
+
for (const entry of this._pendingSteer.splice(0)) {
|
|
7831
|
+
if (!inlineIds.has(entry.ui.id))
|
|
7832
|
+
continue;
|
|
7833
|
+
// absorbSteering is public and updates context immediately. Move just
|
|
7834
|
+
// those exact appended objects into the response's chronological run;
|
|
7835
|
+
// never rebuild a possibly compacted lane from the UI transcript.
|
|
7836
|
+
for (const message of entry.model) {
|
|
7837
|
+
const index = this.modelMessages.lastIndexOf(message);
|
|
7838
|
+
if (index !== -1)
|
|
7839
|
+
this.modelMessages.splice(index, 1);
|
|
7840
|
+
}
|
|
7841
|
+
}
|
|
7596
7842
|
if (!response.id) {
|
|
7597
7843
|
response = { ...response, id: (0, ai_runtime_js_1.generateId)() };
|
|
7598
7844
|
}
|
|
@@ -7605,8 +7851,13 @@ class ChatMessageAccumulator {
|
|
|
7605
7851
|
if (existingIdx !== -1) {
|
|
7606
7852
|
const previous = this.uiMessages[existingIdx];
|
|
7607
7853
|
this.uiMessages[existingIdx] = response;
|
|
7854
|
+
const handoverRun = this._handoverRun && this._handoverRun.id === previous.id
|
|
7855
|
+
? this._handoverRun.run
|
|
7856
|
+
: undefined;
|
|
7857
|
+
if (handoverRun)
|
|
7858
|
+
this._handoverRun = undefined;
|
|
7608
7859
|
try {
|
|
7609
|
-
if (!(await replaceModelRun(this.modelMessages, previous, response, 0))) {
|
|
7860
|
+
if (!(await replaceModelRun(this.modelMessages, previous, response, 0, handoverRun))) {
|
|
7610
7861
|
this.modelMessages = await toModelMessages(this.uiMessages.map((m) => stripProviderMetadata(m)));
|
|
7611
7862
|
}
|
|
7612
7863
|
}
|
|
@@ -7664,7 +7915,10 @@ class ChatMessageAccumulator {
|
|
|
7664
7915
|
this.uiMessages.push(...fresh);
|
|
7665
7916
|
// Record what the model received. Only when the whole batch is new is
|
|
7666
7917
|
// `injected` known to describe exactly these messages.
|
|
7667
|
-
|
|
7918
|
+
const model = injected && fresh.length === claimed.length ? injected : await toModelMessages(fresh);
|
|
7919
|
+
this.modelMessages.push(...model);
|
|
7920
|
+
for (const ui of fresh)
|
|
7921
|
+
this._pendingSteer.push({ ui, model: modelFormOf(ui, fresh, model) });
|
|
7668
7922
|
}
|
|
7669
7923
|
/**
|
|
7670
7924
|
* Get and clear unconsumed steering messages.
|
|
@@ -7684,7 +7938,7 @@ class ChatMessageAccumulator {
|
|
|
7684
7938
|
const comp = this._compaction;
|
|
7685
7939
|
const pm = this._pendingMessages;
|
|
7686
7940
|
const queue = this._steeringQueue;
|
|
7687
|
-
return async ({ messages, steps }) => {
|
|
7941
|
+
return (0, steeringContext_js_1.retainStepMessages)(async ({ messages, steps }) => {
|
|
7688
7942
|
let resultMessages;
|
|
7689
7943
|
// 1. Compaction
|
|
7690
7944
|
if (comp) {
|
|
@@ -7697,7 +7951,7 @@ class ChatMessageAccumulator {
|
|
|
7697
7951
|
}
|
|
7698
7952
|
}
|
|
7699
7953
|
// 2. Pending message injection
|
|
7700
|
-
if (pm
|
|
7954
|
+
if (pm) {
|
|
7701
7955
|
const { injected, claimed } = await drainSteeringQueue(pm, resultMessages ?? messages, steps, queue);
|
|
7702
7956
|
await this.absorbSteering(claimed, injected);
|
|
7703
7957
|
if (injected.length > 0) {
|
|
@@ -7705,12 +7959,14 @@ class ChatMessageAccumulator {
|
|
|
7705
7959
|
}
|
|
7706
7960
|
}
|
|
7707
7961
|
return resultMessages ? { messages: resultMessages } : undefined;
|
|
7708
|
-
};
|
|
7962
|
+
});
|
|
7709
7963
|
}
|
|
7710
7964
|
/**
|
|
7711
7965
|
* Run outer-loop compaction if needed. Call after adding the response
|
|
7712
|
-
* and capturing usage.
|
|
7713
|
-
*
|
|
7966
|
+
* and capturing usage. Pass the LAST step's usage (`result.usage`), which is
|
|
7967
|
+
* the context the model held on its final call; `result.totalUsage` sums every
|
|
7968
|
+
* step of a tool-using turn and belongs in `context.turnUsage`. Applies
|
|
7969
|
+
* `compactModelMessages` and `compactUIMessages` callbacks if configured.
|
|
7714
7970
|
*
|
|
7715
7971
|
* @returns `true` if compaction was performed, `false` otherwise.
|
|
7716
7972
|
*/
|
|
@@ -7723,6 +7979,7 @@ class ChatMessageAccumulator {
|
|
|
7723
7979
|
inputTokens: usage.inputTokens,
|
|
7724
7980
|
outputTokens: usage.outputTokens,
|
|
7725
7981
|
usage,
|
|
7982
|
+
turnUsage: context?.turnUsage,
|
|
7726
7983
|
totalUsage: context?.totalUsage,
|
|
7727
7984
|
chatId: context?.chatId,
|
|
7728
7985
|
turn: context?.turn,
|
|
@@ -7955,8 +8212,8 @@ function createChatSession(payload, options) {
|
|
|
7955
8212
|
idleTimeoutInSeconds: sessionIdleTimeoutOpt ?? currentPayload.idleTimeoutInSeconds ?? 30,
|
|
7956
8213
|
timeout,
|
|
7957
8214
|
spanName: currentPayload.trigger === "preload"
|
|
7958
|
-
? "
|
|
7959
|
-
: "
|
|
8215
|
+
? "first message"
|
|
8216
|
+
: "first message (continuation)",
|
|
7960
8217
|
});
|
|
7961
8218
|
if (!result.ok || runSignal.aborted) {
|
|
7962
8219
|
stop.cleanup();
|
|
@@ -7993,7 +8250,7 @@ function createChatSession(payload, options) {
|
|
|
7993
8250
|
const next = await messagesInput.waitWithIdleTimeout({
|
|
7994
8251
|
idleTimeoutInSeconds,
|
|
7995
8252
|
timeout,
|
|
7996
|
-
spanName: "
|
|
8253
|
+
spanName: "next message",
|
|
7997
8254
|
});
|
|
7998
8255
|
if (!next.ok || runSignal.aborted) {
|
|
7999
8256
|
stop.cleanup();
|
|
@@ -8014,7 +8271,9 @@ function createChatSession(payload, options) {
|
|
|
8014
8271
|
// Reset stop signal for this turn
|
|
8015
8272
|
stop.reset();
|
|
8016
8273
|
// Reset per-turn state
|
|
8017
|
-
locals_js_1.locals.
|
|
8274
|
+
await locals_js_1.locals.get(chatManagedResponseKey)?.close();
|
|
8275
|
+
locals_js_1.locals.set(chatManagedResponseKey, undefined);
|
|
8276
|
+
locals_js_1.locals.set(chatManagedResponseActiveKey, true);
|
|
8018
8277
|
// Set up steering queue and pending messages config in locals
|
|
8019
8278
|
// so toStreamTextOptions() auto-injects prepareStep for steering
|
|
8020
8279
|
const turnSteeringQueue = [];
|
|
@@ -8159,11 +8418,6 @@ function createChatSession(payload, options) {
|
|
|
8159
8418
|
if (captured.status === "error") {
|
|
8160
8419
|
if (captured.message) {
|
|
8161
8420
|
const partial = cleanupAbortedParts(captured.message);
|
|
8162
|
-
const queuedParts = locals_js_1.locals.get(chatResponsePartsKey);
|
|
8163
|
-
if (queuedParts && queuedParts.length > 0) {
|
|
8164
|
-
partial.parts = [...(partial.parts ?? []), ...queuedParts];
|
|
8165
|
-
locals_js_1.locals.set(chatResponsePartsKey, []);
|
|
8166
|
-
}
|
|
8167
8421
|
await accumulator.addResponse(partial);
|
|
8168
8422
|
}
|
|
8169
8423
|
throw captured.error;
|
|
@@ -8179,31 +8433,14 @@ function createChatSession(payload, options) {
|
|
|
8179
8433
|
const cleaned = stop.signal.aborted && !runSignal.aborted
|
|
8180
8434
|
? cleanupAbortedParts(response)
|
|
8181
8435
|
: response;
|
|
8182
|
-
// Append any non-transient data parts queued via chat.response or writer.write()
|
|
8183
|
-
const queuedParts = locals_js_1.locals.get(chatResponsePartsKey);
|
|
8184
|
-
if (queuedParts && queuedParts.length > 0) {
|
|
8185
|
-
cleaned.parts = [...(cleaned.parts ?? []), ...queuedParts];
|
|
8186
|
-
locals_js_1.locals.set(chatResponsePartsKey, []);
|
|
8187
|
-
}
|
|
8188
8436
|
await accumulator.addResponse(cleaned);
|
|
8189
8437
|
}
|
|
8190
|
-
else {
|
|
8191
|
-
// No response (manual pipe mode) but there are queued data parts
|
|
8192
|
-
const queuedParts = locals_js_1.locals.get(chatResponsePartsKey);
|
|
8193
|
-
if (queuedParts && queuedParts.length > 0) {
|
|
8194
|
-
await accumulator.addResponse({
|
|
8195
|
-
id: (0, ai_runtime_js_1.generateId)(),
|
|
8196
|
-
role: "assistant",
|
|
8197
|
-
parts: queuedParts,
|
|
8198
|
-
});
|
|
8199
|
-
locals_js_1.locals.set(chatResponsePartsKey, []);
|
|
8200
|
-
}
|
|
8201
|
-
}
|
|
8202
8438
|
// Capture token usage from the streamText result. Race with a 2s
|
|
8203
8439
|
// timeout — on stop-abort the AI SDK's totalUsage promise can hang
|
|
8204
8440
|
// indefinitely, which would wedge the turn loop (same guard as
|
|
8205
8441
|
// chat.agent's turn loop).
|
|
8206
8442
|
let turnUsage;
|
|
8443
|
+
let lastStepUsage;
|
|
8207
8444
|
if (typeof source.totalUsage?.then === "function") {
|
|
8208
8445
|
try {
|
|
8209
8446
|
const usage = (await Promise.race([
|
|
@@ -8220,14 +8457,28 @@ function createChatSession(payload, options) {
|
|
|
8220
8457
|
/* non-fatal */
|
|
8221
8458
|
}
|
|
8222
8459
|
}
|
|
8460
|
+
const lastStepUsagePromise = source.usage;
|
|
8461
|
+
if (typeof lastStepUsagePromise?.then === "function") {
|
|
8462
|
+
try {
|
|
8463
|
+
lastStepUsage = (await Promise.race([
|
|
8464
|
+
lastStepUsagePromise,
|
|
8465
|
+
new Promise((r) => setTimeout(() => r(undefined), 2_000)),
|
|
8466
|
+
]));
|
|
8467
|
+
}
|
|
8468
|
+
catch {
|
|
8469
|
+
/* non-fatal */
|
|
8470
|
+
}
|
|
8471
|
+
}
|
|
8223
8472
|
// Outer-loop compaction (same logic as chat.agent)
|
|
8224
8473
|
if (sessionCompaction && turnUsage && !turnObj.stopped) {
|
|
8474
|
+
const contextUsage = lastStepUsage ?? turnUsage;
|
|
8225
8475
|
const shouldTrigger = await sessionCompaction.shouldCompact({
|
|
8226
8476
|
messages: accumulator.modelMessages,
|
|
8227
|
-
totalTokens:
|
|
8228
|
-
inputTokens:
|
|
8229
|
-
outputTokens:
|
|
8230
|
-
usage:
|
|
8477
|
+
totalTokens: contextUsage.totalTokens,
|
|
8478
|
+
inputTokens: contextUsage.inputTokens,
|
|
8479
|
+
outputTokens: contextUsage.outputTokens,
|
|
8480
|
+
usage: contextUsage,
|
|
8481
|
+
turnUsage,
|
|
8231
8482
|
totalUsage: cumulativeUsage,
|
|
8232
8483
|
chatId: currentPayload.chatId,
|
|
8233
8484
|
turn,
|
|
@@ -8274,15 +8525,6 @@ function createChatSession(payload, options) {
|
|
|
8274
8525
|
return response;
|
|
8275
8526
|
},
|
|
8276
8527
|
async addResponse(response) {
|
|
8277
|
-
// Append any non-transient data parts queued via chat.response or writer.write()
|
|
8278
|
-
const queuedParts = locals_js_1.locals.get(chatResponsePartsKey);
|
|
8279
|
-
if (queuedParts && queuedParts.length > 0) {
|
|
8280
|
-
response = {
|
|
8281
|
-
...response,
|
|
8282
|
-
parts: [...(response.parts ?? []), ...queuedParts],
|
|
8283
|
-
};
|
|
8284
|
-
locals_js_1.locals.set(chatResponsePartsKey, []);
|
|
8285
|
-
}
|
|
8286
8528
|
await accumulator.addResponse(response);
|
|
8287
8529
|
},
|
|
8288
8530
|
async done() {
|
|
@@ -8294,7 +8536,7 @@ function createChatSession(payload, options) {
|
|
|
8294
8536
|
const hasPending = !!sessionPendingMessages;
|
|
8295
8537
|
if (!hasCompaction && !hasPending)
|
|
8296
8538
|
return undefined;
|
|
8297
|
-
return async ({ messages: stepMsgs, steps, }) => {
|
|
8539
|
+
return (0, steeringContext_js_1.retainStepMessages)(async ({ messages: stepMsgs, steps, }) => {
|
|
8298
8540
|
let resultMessages;
|
|
8299
8541
|
if (sessionCompaction) {
|
|
8300
8542
|
const compactResult = await chatCompact(stepMsgs, steps, {
|
|
@@ -8313,7 +8555,7 @@ function createChatSession(payload, options) {
|
|
|
8313
8555
|
}
|
|
8314
8556
|
}
|
|
8315
8557
|
return resultMessages ? { messages: resultMessages } : undefined;
|
|
8316
|
-
};
|
|
8558
|
+
});
|
|
8317
8559
|
},
|
|
8318
8560
|
};
|
|
8319
8561
|
return { done: false, value: turnObj };
|
|
@@ -8581,6 +8823,10 @@ function createChatStartSessionAction(taskId, options) {
|
|
|
8581
8823
|
const clientDataMetadata = params.clientData !== undefined ? { metadata: params.clientData } : {};
|
|
8582
8824
|
const maxAttempts = params.triggerConfig?.maxAttempts ?? options?.triggerConfig?.maxAttempts;
|
|
8583
8825
|
const maxDuration = params.triggerConfig?.maxDuration ?? options?.triggerConfig?.maxDuration;
|
|
8826
|
+
const concurrency = params.triggerConfig?.concurrency !== undefined
|
|
8827
|
+
? params.triggerConfig.concurrency
|
|
8828
|
+
: options?.triggerConfig?.concurrency;
|
|
8829
|
+
const concurrencyKey = params.triggerConfig?.concurrencyKey ?? options?.triggerConfig?.concurrencyKey;
|
|
8584
8830
|
const idleTimeoutInSeconds = params.triggerConfig?.idleTimeoutInSeconds ?? options?.triggerConfig?.idleTimeoutInSeconds;
|
|
8585
8831
|
// Only `undefined` means "not supplied": a per-call `null` (opt out) has to beat a pinning
|
|
8586
8832
|
// action default, which neither truthiness nor `??` would allow.
|
|
@@ -8602,6 +8848,8 @@ function createChatStartSessionAction(taskId, options) {
|
|
|
8602
8848
|
...(options?.triggerConfig?.queue || params.triggerConfig?.queue
|
|
8603
8849
|
? { queue: params.triggerConfig?.queue ?? options?.triggerConfig?.queue }
|
|
8604
8850
|
: {}),
|
|
8851
|
+
...(concurrency !== undefined ? (0, concurrency_shared_js_1.triggerConcurrencyBody)(concurrency) : {}),
|
|
8852
|
+
...(concurrencyKey !== undefined ? { concurrencyKey } : {}),
|
|
8605
8853
|
tags,
|
|
8606
8854
|
...(maxAttempts !== undefined ? { maxAttempts } : {}),
|
|
8607
8855
|
...(maxDuration !== undefined ? { maxDuration } : {}),
|
|
@@ -8839,6 +9087,7 @@ exports.chat = {
|
|
|
8839
9087
|
cleanupAbortedParts,
|
|
8840
9088
|
/** Register background work that runs in parallel with streaming. See {@link chatDefer}. */
|
|
8841
9089
|
defer: chatDefer,
|
|
9090
|
+
deferBeforeOutput: chatDeferBeforeOutput,
|
|
8842
9091
|
/** Queue model messages for injection at the next `prepareStep` boundary. See {@link injectBackgroundContext}. */
|
|
8843
9092
|
inject: injectBackgroundContext,
|
|
8844
9093
|
/** Typed chat output stream for writing custom chunks or piping from subtasks. */
|
|
@@ -8948,6 +9197,10 @@ exports.chat = {
|
|
|
8948
9197
|
* @internal
|
|
8949
9198
|
*/
|
|
8950
9199
|
async function writeTurnCompleteChunk(_chatId, publicAccessToken) {
|
|
9200
|
+
locals_js_1.locals.set(chatManagedResponseActiveKey, false);
|
|
9201
|
+
const response = locals_js_1.locals.get(chatManagedResponseKey);
|
|
9202
|
+
if (response)
|
|
9203
|
+
await response.close();
|
|
8951
9204
|
const session = getChatSession();
|
|
8952
9205
|
// A handover-prepare boot claims the handover kinds so a signal arriving
|
|
8953
9206
|
// before `waitForHandover` attaches is not drained. Released here rather than
|