@trigger.dev/sdk 0.0.0-prerelease-20260908122921 → 0.0.0-prerelease-streamfix-20260909094302
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/imports/ai-runtime-cjs.cjs.map +1 -1
- package/dist/commonjs/imports/ai-runtime.js +0 -2
- package/dist/commonjs/v3/ai.d.ts +16 -199
- package/dist/commonjs/v3/ai.js +102 -983
- package/dist/commonjs/v3/ai.js.map +1 -1
- package/dist/commonjs/v3/chat-client.d.ts +2 -3
- package/dist/commonjs/v3/chat-client.js +5 -31
- package/dist/commonjs/v3/chat-client.js.map +1 -1
- package/dist/commonjs/v3/chat-react.d.ts +0 -34
- package/dist/commonjs/v3/chat-react.js +1 -47
- package/dist/commonjs/v3/chat-react.js.map +1 -1
- package/dist/commonjs/v3/chat-server.d.ts +6 -42
- package/dist/commonjs/v3/chat-server.js +7 -52
- package/dist/commonjs/v3/chat-server.js.map +1 -1
- package/dist/commonjs/v3/chat.d.ts +10 -81
- package/dist/commonjs/v3/chat.js +46 -292
- package/dist/commonjs/v3/chat.js.map +1 -1
- package/dist/commonjs/v3/sessions.d.ts +2 -15
- package/dist/commonjs/v3/sessions.js +1 -12
- package/dist/commonjs/v3/sessions.js.map +1 -1
- package/dist/commonjs/v3/shared.js +36 -30
- package/dist/commonjs/v3/shared.js.map +1 -1
- package/dist/commonjs/v3/test/mock-chat-agent.d.ts +0 -43
- package/dist/commonjs/v3/test/mock-chat-agent.js +0 -90
- package/dist/commonjs/v3/test/mock-chat-agent.js.map +1 -1
- package/dist/commonjs/v3/test/test-session-handle.js +0 -6
- package/dist/commonjs/v3/test/test-session-handle.js.map +1 -1
- package/dist/commonjs/version.js +1 -1
- package/dist/esm/imports/ai-runtime.d.ts +2 -2
- package/dist/esm/imports/ai-runtime.js +2 -2
- package/dist/esm/imports/ai-runtime.js.map +1 -1
- package/dist/esm/v3/ai.d.ts +16 -199
- package/dist/esm/v3/ai.js +103 -984
- package/dist/esm/v3/ai.js.map +1 -1
- package/dist/esm/v3/chat-client.d.ts +2 -3
- package/dist/esm/v3/chat-client.js +5 -31
- package/dist/esm/v3/chat-client.js.map +1 -1
- package/dist/esm/v3/chat-react.d.ts +0 -34
- package/dist/esm/v3/chat-react.js +1 -46
- package/dist/esm/v3/chat-react.js.map +1 -1
- package/dist/esm/v3/chat-server.d.ts +6 -42
- package/dist/esm/v3/chat-server.js +8 -53
- package/dist/esm/v3/chat-server.js.map +1 -1
- package/dist/esm/v3/chat.d.ts +10 -81
- package/dist/esm/v3/chat.js +47 -293
- package/dist/esm/v3/chat.js.map +1 -1
- package/dist/esm/v3/sessions.d.ts +2 -15
- package/dist/esm/v3/sessions.js +1 -11
- package/dist/esm/v3/sessions.js.map +1 -1
- package/dist/esm/v3/shared.js +23 -17
- package/dist/esm/v3/shared.js.map +1 -1
- package/dist/esm/v3/test/mock-chat-agent.d.ts +0 -43
- package/dist/esm/v3/test/mock-chat-agent.js +2 -92
- package/dist/esm/v3/test/mock-chat-agent.js.map +1 -1
- package/dist/esm/v3/test/test-session-handle.js +0 -6
- package/dist/esm/v3/test/test-session-handle.js.map +1 -1
- package/dist/esm/version.js +1 -1
- package/docs/ai-chat/actions.mdx +23 -55
- package/docs/ai-chat/anatomy.mdx +3 -3
- package/docs/ai-chat/backend.mdx +48 -125
- package/docs/ai-chat/background-injection.mdx +19 -67
- package/docs/ai-chat/client-protocol.mdx +4 -5
- package/docs/ai-chat/compaction.mdx +7 -11
- package/docs/ai-chat/custom-agents.mdx +0 -23
- package/docs/ai-chat/fast-starts.mdx +20 -27
- package/docs/ai-chat/frontend.mdx +14 -17
- package/docs/ai-chat/migrating-from-a-route-handler.mdx +14 -16
- package/docs/ai-chat/patterns/skills.mdx +10 -7
- package/docs/ai-chat/patterns/version-upgrades.mdx +6 -79
- package/docs/ai-chat/pending-messages.mdx +3 -3
- package/docs/ai-chat/prompt-caching.mdx +25 -23
- package/docs/ai-chat/quick-start.mdx +11 -11
- package/docs/ai-chat/reference.mdx +5 -12
- package/docs/ai-chat/sessions.mdx +1 -6
- package/docs/ai-chat/testing.mdx +1 -2
- package/docs/ai-chat/tools.mdx +13 -18
- package/docs/ai-chat/upgrade-guide.mdx +2 -2
- package/docs/apikeys.mdx +45 -27
- package/docs/deployment/overview.mdx +8 -4
- package/docs/deployment/preview-branches.mdx +4 -4
- package/docs/deployment/version-skew-protection.mdx +0 -62
- package/docs/manual-setup.mdx +7 -7
- package/docs/mcp-tools.mdx +0 -9
- package/docs/quick-start.mdx +3 -3
- package/docs/realtime/auth.mdx +1 -1
- package/docs/self-hosting/security.mdx +0 -5
- package/docs/tasks/scheduled.mdx +0 -24
- package/docs/triggering.mdx +1 -1
- package/package.json +4 -4
- package/skills/trigger-authoring-chat-agent/SKILL.md +27 -38
- package/skills/trigger-chat-agent-advanced/SKILL.md +12 -31
- package/dist/commonjs/v3/chatVersionSkew.d.ts +0 -12
- package/dist/commonjs/v3/chatVersionSkew.js +0 -30
- package/dist/commonjs/v3/chatVersionSkew.js.map +0 -1
- package/dist/commonjs/v3/externalDeploymentId.d.ts +0 -23
- package/dist/commonjs/v3/externalDeploymentId.js +0 -43
- package/dist/commonjs/v3/externalDeploymentId.js.map +0 -1
- package/dist/esm/v3/chatVersionSkew.d.ts +0 -12
- package/dist/esm/v3/chatVersionSkew.js +0 -27
- package/dist/esm/v3/chatVersionSkew.js.map +0 -1
- package/dist/esm/v3/externalDeploymentId.d.ts +0 -23
- package/dist/esm/v3/externalDeploymentId.js +0 -38
- package/dist/esm/v3/externalDeploymentId.js.map +0 -1
- package/docs/ai-chat/patterns/native-compaction.mdx +0 -310
- package/docs/reports.mdx +0 -157
- package/docs/troubleshooting-zod.mdx +0 -158
package/dist/commonjs/v3/ai.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.chat = exports.
|
|
3
|
+
exports.chat = exports.upsertIncomingMessage = exports.PENDING_MESSAGE_INJECTED_TYPE = exports.ai = void 0;
|
|
4
4
|
exports.__setReadChatSnapshotImplForTests = __setReadChatSnapshotImplForTests;
|
|
5
5
|
exports.__setWriteChatSnapshotImplForTests = __setWriteChatSnapshotImplForTests;
|
|
6
6
|
exports.__readChatSnapshotProductionPathForTests = __readChatSnapshotProductionPathForTests;
|
|
@@ -35,8 +35,6 @@ const metadata_js_1 = require("./metadata.js");
|
|
|
35
35
|
// pulled in transitively here never reach a client chunk.
|
|
36
36
|
const agentSkillsRuntime_js_1 = require("./agentSkillsRuntime.js");
|
|
37
37
|
const aiAutoTelemetry_js_1 = require("./aiAutoTelemetry.js");
|
|
38
|
-
const externalDeploymentId_js_1 = require("./externalDeploymentId.js");
|
|
39
|
-
const chatVersionSkew_js_1 = require("./chatVersionSkew.js");
|
|
40
38
|
const sessions_js_1 = require("./sessions.js");
|
|
41
39
|
const shared_js_1 = require("./shared.js");
|
|
42
40
|
const streams_js_1 = require("./streams.js");
|
|
@@ -1774,13 +1772,6 @@ async function installChatInputRouter(chatId, options) {
|
|
|
1774
1772
|
checkpoint.appliedThrough = Math.max(checkpoint.appliedThrough ?? replayWindowEnd, replayWindowEnd);
|
|
1775
1773
|
}
|
|
1776
1774
|
}
|
|
1777
|
-
// A boot that replayed `.in` itself has already answered everything up to
|
|
1778
|
-
// `recoveredThrough`, so the floor has to cover it before the tail opens.
|
|
1779
|
-
if (options?.recoveredThrough !== undefined) {
|
|
1780
|
-
const recovered = options.recoveredThrough;
|
|
1781
|
-
checkpoint.resumeFrom = Math.max(checkpoint.resumeFrom ?? recovered, recovered);
|
|
1782
|
-
checkpoint.appliedThrough = Math.max(checkpoint.appliedThrough ?? checkpoint.resumeFrom, checkpoint.resumeFrom);
|
|
1783
|
-
}
|
|
1784
1775
|
const router = entry.router;
|
|
1785
1776
|
router.restore(checkpoint);
|
|
1786
1777
|
const floor = router.resumeFrom();
|
|
@@ -1789,10 +1780,6 @@ async function installChatInputRouter(chatId, options) {
|
|
|
1789
1780
|
v3_1.sessionStreams.setLastDispatchedSeqNum(chatId, "in", floor);
|
|
1790
1781
|
}
|
|
1791
1782
|
v3_1.sessionStreams.onRecord(chatId, "in", (record) => {
|
|
1792
|
-
// The floor is the tail's `Last-Event-ID`, but a reconnect can still
|
|
1793
|
-
// re-deliver below it and a replayable route would re-queue it.
|
|
1794
|
-
if (floor !== undefined && record.seqNum <= floor)
|
|
1795
|
-
return true;
|
|
1796
1783
|
router.ingest(record);
|
|
1797
1784
|
return true;
|
|
1798
1785
|
});
|
|
@@ -1964,29 +1951,6 @@ function spliceHandoverPartial(modelMessages, uiMessages, signal) {
|
|
|
1964
1951
|
* @internal
|
|
1965
1952
|
*/
|
|
1966
1953
|
const chatBackgroundQueueKey = locals_js_1.locals.create("chat.backgroundQueue");
|
|
1967
|
-
/**
|
|
1968
|
-
* System-role context injected mid-conversation, held for the instructions lane.
|
|
1969
|
-
*
|
|
1970
|
-
* Kept apart from the message queue because ai@7 rejects a system message inside
|
|
1971
|
-
* `messages` for every provider — `standardizePrompt` throws upstream of any
|
|
1972
|
-
* provider call, and its own advice is to use the instructions option. Instructions
|
|
1973
|
-
* accept `Array<SystemModelMessage>`, so a system-role injection has a correct
|
|
1974
|
-
* home: appended as another system block rather than smuggled into the transcript.
|
|
1975
|
-
*
|
|
1976
|
-
* This is also the only way to inject *trusted* context. A message injected as
|
|
1977
|
-
* `user` is untrusted by construction, and a well-aligned model treats it that
|
|
1978
|
-
* way — it will say so, and re-derive the answer from tools instead.
|
|
1979
|
-
*/
|
|
1980
|
-
const chatInjectedInstructionsKey = locals_js_1.locals.create("chat.injectedInstructions");
|
|
1981
|
-
/**
|
|
1982
|
-
* What a turn already consumed from the instructions lane, so a second
|
|
1983
|
-
* `toStreamTextOptions()` call in the same turn sees the same blocks.
|
|
1984
|
-
*
|
|
1985
|
-
* Consumed blocks are moved here rather than left in the pending lane: leaving
|
|
1986
|
-
* them there means an injection made during the consumed turn sits behind them,
|
|
1987
|
-
* and clearing the lane on the next turn destroys both.
|
|
1988
|
-
*/
|
|
1989
|
-
const chatInstructionsConsumedKey = locals_js_1.locals.create("chat.injectedInstructionsConsumed");
|
|
1990
1954
|
/**
|
|
1991
1955
|
* Run-scoped pipe counter. Stored in locals so concurrent runs in the
|
|
1992
1956
|
* same worker don't share state.
|
|
@@ -2497,63 +2461,18 @@ const chatToolsOptionKey = locals_js_1.locals.create("chat.toolsOption");
|
|
|
2497
2461
|
const chatResolvedToolsKey = locals_js_1.locals.create("chat.resolvedTools");
|
|
2498
2462
|
/** @internal Flag set by `chat.requestUpgrade()` to exit the loop after the current turn. */
|
|
2499
2463
|
const chatUpgradeRequestedKey = locals_js_1.locals.create("chat.upgradeRequested");
|
|
2500
|
-
/** @internal Target for the upgrade handoff, set by `chat.requestUpgrade({ externalDeploymentId })`. */
|
|
2501
|
-
const chatUpgradeExternalDeploymentIdKey = locals_js_1.locals.create("chat.upgradeExternalDeploymentId");
|
|
2502
2464
|
/**
|
|
2503
2465
|
* @internal Flag set by `chat.endRun()` to exit the loop after the current
|
|
2504
2466
|
* turn completes, without any upgrade semantics. Checked at the same
|
|
2505
2467
|
* post-turn / pre-wait sites as `chatUpgradeRequestedKey`.
|
|
2506
2468
|
*/
|
|
2507
2469
|
const chatEndRunRequestedKey = locals_js_1.locals.create("chat.endRunRequested");
|
|
2508
|
-
/**
|
|
2509
|
-
* @internal Set by `chat.close()`. Holds the close request (and its reason)
|
|
2510
|
-
* for the rest of the run: the loop writes the terminal `session-closed`
|
|
2511
|
-
* record, closes the session row, and exits at the same post-turn /
|
|
2512
|
-
* pre-wait sites as `chatEndRunRequestedKey`.
|
|
2513
|
-
*/
|
|
2514
|
-
const chatCloseRequestedKey = locals_js_1.locals.create("chat.closeRequested");
|
|
2515
|
-
/** @internal Matches the server's `CloseSessionRequestBody.reason` cap. */
|
|
2516
|
-
const CHAT_CLOSE_REASON_MAX_LENGTH = 256;
|
|
2517
|
-
/** @internal Set once the session row is closed, so the close happens once. */
|
|
2518
|
-
const chatClosePerformedKey = locals_js_1.locals.create("chat.closePerformed");
|
|
2519
|
-
/**
|
|
2520
|
-
* @internal Set once the terminal `.out` record is written. Tracked apart from
|
|
2521
|
-
* {@link chatClosePerformedKey} so a retried close does not emit a second
|
|
2522
|
-
* client-visible event.
|
|
2523
|
-
*/
|
|
2524
|
-
const chatCloseRecordWrittenKey = locals_js_1.locals.create("chat.closeRecordWritten");
|
|
2525
2470
|
/** @internal */
|
|
2526
2471
|
const chatAgentCompactionKey = locals_js_1.locals.create("chat.agentCompaction");
|
|
2527
2472
|
/** @internal */
|
|
2528
2473
|
const chatPendingMessagesKey = locals_js_1.locals.create("chat.pendingMessages");
|
|
2529
2474
|
/** @internal */
|
|
2530
2475
|
const chatSteeringQueueKey = locals_js_1.locals.create("chat.steeringQueue");
|
|
2531
|
-
/**
|
|
2532
|
-
* This turn's new messages, as `onTurnComplete.newUIMessages` will see them.
|
|
2533
|
-
*
|
|
2534
|
-
* Held in locals because `drainSteeringQueue` runs outside the turn closure and
|
|
2535
|
-
* has to append the messages it injects. Without that, an injected message
|
|
2536
|
-
* reaches the model and the browser but no hook, so an app persisting from
|
|
2537
|
-
* `onTurnComplete` never learns it existed.
|
|
2538
|
-
*/
|
|
2539
|
-
const chatTurnNewUIMessagesKey = locals_js_1.locals.create("chat.turnNewUIMessages");
|
|
2540
|
-
/**
|
|
2541
|
-
* Steering messages a drain consumed that the model accumulator has not been
|
|
2542
|
-
* given yet.
|
|
2543
|
-
*
|
|
2544
|
-
* The two accumulators are maintained separately, and the model one is
|
|
2545
|
-
* normally advanced by appending each turn's delta. A drained message is
|
|
2546
|
-
* appended to the UI one but reaches the model only through the `prepareStep`
|
|
2547
|
-
* return value, which is per-step: without this the model lane never learns
|
|
2548
|
-
* the message exists and every later turn of the run answers without it,
|
|
2549
|
-
* while the browser, the snapshot and `chat.history.*` all still show it.
|
|
2550
|
-
*
|
|
2551
|
-
* Held as the messages rather than a "rebuild me" flag because the model lane
|
|
2552
|
-
* can only be appended to, never reconstructed. Compaction replaces it with a
|
|
2553
|
-
* summary and deliberately leaves the UI lane whole, so reconverting the UI
|
|
2554
|
-
* lane restores every message the summary replaced.
|
|
2555
|
-
*/
|
|
2556
|
-
const chatPendingSteerKey = locals_js_1.locals.create("chat.pendingSteer");
|
|
2557
2476
|
/** @internal — IDs of messages that were successfully injected via prepareStep */
|
|
2558
2477
|
const chatInjectedMessageIdsKey = locals_js_1.locals.create("chat.injectedMessageIds");
|
|
2559
2478
|
/** @internal — non-transient data parts queued via chat.response or writer.write() for accumulation into the response message */
|
|
@@ -2906,32 +2825,20 @@ function chatCompactionStep(options) {
|
|
|
2906
2825
|
return result.type === "skipped" ? undefined : result;
|
|
2907
2826
|
};
|
|
2908
2827
|
}
|
|
2909
|
-
|
|
2910
|
-
|
|
2911
|
-
|
|
2912
|
-
* each entry's own conversion is used. With it, `prepare` returned one list
|
|
2913
|
-
* for the whole batch, so the first claimed message carries all of it and the
|
|
2914
|
-
* rest carry none, which keeps the total exactly what the model received.
|
|
2915
|
-
*/
|
|
2916
|
-
function modelFormOf(m, batch, injected) {
|
|
2917
|
-
return batch[0] === m ? injected : [];
|
|
2918
|
-
}
|
|
2828
|
+
// ---------------------------------------------------------------------------
|
|
2829
|
+
// Steering queue drain — shared by toStreamTextOptions, session, accumulator
|
|
2830
|
+
// ---------------------------------------------------------------------------
|
|
2919
2831
|
/**
|
|
2920
2832
|
* Drain the steering queue as a batch. Calls `shouldInject` once with all
|
|
2921
2833
|
* pending messages. If it returns true, calls `prepareMessages` once to
|
|
2922
2834
|
* transform the batch, then clears the queue.
|
|
2923
|
-
* Returns the model messages to inject
|
|
2924
|
-
*
|
|
2925
|
-
* `claimed` is returned rather than only published to locals because each
|
|
2926
|
-
* surface files it somewhere different: `chat.agent` has an accumulator in
|
|
2927
|
-
* locals, while `chat.createSession` keeps its own. Publishing to locals alone
|
|
2928
|
-
* is silently a no-op for any surface that never set the key.
|
|
2835
|
+
* Returns the model messages to inject (empty if none).
|
|
2929
2836
|
* @internal
|
|
2930
2837
|
*/
|
|
2931
2838
|
async function drainSteeringQueue(config, messages, steps, queueOverride) {
|
|
2932
2839
|
const queue = queueOverride ?? locals_js_1.locals.get(chatSteeringQueueKey);
|
|
2933
2840
|
if (!queue || queue.length === 0)
|
|
2934
|
-
return
|
|
2841
|
+
return [];
|
|
2935
2842
|
const ctx = locals_js_1.locals.get(chatTurnContextKey);
|
|
2936
2843
|
const stepNumber = steps.length - 1;
|
|
2937
2844
|
/**
|
|
@@ -2954,7 +2861,7 @@ async function drainSteeringQueue(config, messages, steps, queueOverride) {
|
|
|
2954
2861
|
// Call shouldInject once for the whole batch
|
|
2955
2862
|
const shouldInject = config.shouldInject ? await config.shouldInject(batchEvent) : false;
|
|
2956
2863
|
if (!shouldInject)
|
|
2957
|
-
return
|
|
2864
|
+
return [];
|
|
2958
2865
|
const textOfUIMessage = (m) => (m.parts ?? [])
|
|
2959
2866
|
.filter((p) => p.type === "text")
|
|
2960
2867
|
.map((p) => p.text)
|
|
@@ -3000,7 +2907,7 @@ async function drainSteeringQueue(config, messages, steps, queueOverride) {
|
|
|
3000
2907
|
queue.splice(at, 1);
|
|
3001
2908
|
}
|
|
3002
2909
|
if (claimed.length === 0)
|
|
3003
|
-
return
|
|
2910
|
+
return [];
|
|
3004
2911
|
/**
|
|
3005
2912
|
* Give the claim back if the transform fails. `prepare` is caller code and
|
|
3006
2913
|
* can throw; the records have already left the router by this point, so
|
|
@@ -3030,37 +2937,6 @@ async function drainSteeringQueue(config, messages, steps, queueOverride) {
|
|
|
3030
2937
|
for (const m of claimedUIMessages)
|
|
3031
2938
|
injectedIds.add(m.id);
|
|
3032
2939
|
}
|
|
3033
|
-
// Record them as part of the conversation.
|
|
3034
|
-
//
|
|
3035
|
-
// The model has them and the browser has them; without this the
|
|
3036
|
-
// accumulator does not, so they reach neither `uiMessages` nor
|
|
3037
|
-
// `newUIMessages` on `onTurnComplete` and an app that persists from there
|
|
3038
|
-
// silently loses the instruction the answer was shaped by. Appending here
|
|
3039
|
-
// rather than at turn end keeps them in the order they happened: after the
|
|
3040
|
-
// message that started the turn, before the response that answers it.
|
|
3041
|
-
//
|
|
3042
|
-
// De-duplicated by id because a step boundary can drain more than once per
|
|
3043
|
-
// turn, and because a message that failed to inject falls back to becoming
|
|
3044
|
-
// its own turn, where it is accumulated the normal way.
|
|
3045
|
-
const currentUIMessages = locals_js_1.locals.get(chatCurrentUIMessagesKey);
|
|
3046
|
-
const turnNew = locals_js_1.locals.get(chatTurnNewUIMessagesKey);
|
|
3047
|
-
for (const m of claimedUIMessages) {
|
|
3048
|
-
if (currentUIMessages && !currentUIMessages.some((existing) => existing.id === m.id)) {
|
|
3049
|
-
currentUIMessages.push(m);
|
|
3050
|
-
}
|
|
3051
|
-
if (turnNew && !turnNew.some((existing) => existing.id === m.id)) {
|
|
3052
|
-
turnNew.push(m);
|
|
3053
|
-
}
|
|
3054
|
-
}
|
|
3055
|
-
if (claimedUIMessages.length > 0 && currentUIMessages) {
|
|
3056
|
-
const pendingSteer = locals_js_1.locals.get(chatPendingSteerKey) ?? [];
|
|
3057
|
-
for (const m of claimedUIMessages) {
|
|
3058
|
-
if (!pendingSteer.some((existing) => existing.ui.id === m.id)) {
|
|
3059
|
-
pendingSteer.push({ ui: m, model: modelFormOf(m, claimedUIMessages, injected) });
|
|
3060
|
-
}
|
|
3061
|
-
}
|
|
3062
|
-
locals_js_1.locals.set(chatPendingSteerKey, pendingSteer);
|
|
3063
|
-
}
|
|
3064
2940
|
// Write injection confirmation chunk to the stream so the frontend
|
|
3065
2941
|
// knows which messages were injected and where in the response.
|
|
3066
2942
|
if (injected.length > 0) {
|
|
@@ -3102,7 +2978,7 @@ async function drainSteeringQueue(config, messages, steps, queueOverride) {
|
|
|
3102
2978
|
/* non-fatal */
|
|
3103
2979
|
}
|
|
3104
2980
|
}
|
|
3105
|
-
return
|
|
2981
|
+
return injected;
|
|
3106
2982
|
}, {
|
|
3107
2983
|
attributes: {
|
|
3108
2984
|
[v3_1.SemanticInternalAttributes.STYLE_ICON]: "tabler-message-forward",
|
|
@@ -3328,149 +3204,32 @@ function buildSkillTools(skills) {
|
|
|
3328
3204
|
return { loadSkill, readFile, bash };
|
|
3329
3205
|
}
|
|
3330
3206
|
/**
|
|
3331
|
-
*
|
|
3332
|
-
*
|
|
3333
|
-
*
|
|
3334
|
-
*
|
|
3335
|
-
* because forgetting the spread silently drops the managed prompt, the skill
|
|
3336
|
-
* tools, telemetry, and the `prepareStep` that delivers steering, compaction and
|
|
3337
|
-
* conversational injection.
|
|
3338
|
-
*
|
|
3339
|
-
* Caller options win for everything the caller owns (model, messages, signal,
|
|
3340
|
-
* stopWhen). The three that would otherwise clobber managed behaviour are
|
|
3341
|
-
* merged instead of replaced:
|
|
3342
|
-
*
|
|
3343
|
-
* - `tools` are passed into the helper, so skill tools survive.
|
|
3344
|
-
* - `prepareStep` is composed after the managed one, so a caller's per-step
|
|
3345
|
-
* overrides apply on top of steering and compaction instead of disabling them.
|
|
3346
|
-
*
|
|
3347
|
-
* `system` may be set at the call site, on `chat.agent({ system })`, or
|
|
3348
|
-
* through `chat.prompt.set()`, but only in one of them. Setting it in two
|
|
3349
|
-
* places throws: no shape merges two system values on every supported
|
|
3350
|
-
* version, and dropping one silently is the failure this seam exists to
|
|
3351
|
-
* prevent. Injected instructions append to whichever one is in play.
|
|
3352
|
-
*/
|
|
3353
|
-
/**
|
|
3354
|
-
* The agent-level managed options (`registry`, `system`, `cacheControl`,
|
|
3355
|
-
* `systemProviderOptions`), published for the run so that
|
|
3356
|
-
* `chat.toStreamTextOptions()` applies them too. Without this only the bound
|
|
3357
|
-
* `streamText` saw them, and the documented spread form silently ran without
|
|
3358
|
-
* the agent's system prompt or model.
|
|
3359
|
-
*/
|
|
3360
|
-
const chatAgentManagedConfigKey = locals_js_1.locals.create("chat.agentManagedConfig");
|
|
3361
|
-
/**
|
|
3362
|
-
* The caller's `streamText` options merged with the agent's managed ones.
|
|
3207
|
+
* Returns an options object ready to spread into `streamText()`.
|
|
3208
|
+
*
|
|
3209
|
+
* Includes `system`, `experimental_telemetry`, and any config fields
|
|
3210
|
+
* (temperature, maxTokens, etc.) from the stored prompt.
|
|
3363
3211
|
*
|
|
3364
|
-
*
|
|
3365
|
-
* the
|
|
3366
|
-
*
|
|
3212
|
+
* When a `registry` is provided and the prompt has a `model` string,
|
|
3213
|
+
* the resolved `LanguageModel` is included as `model`.
|
|
3214
|
+
*
|
|
3215
|
+
* If no prompt has been set, returns `{}` (no-op spread).
|
|
3367
3216
|
*/
|
|
3368
|
-
function buildManagedStreamTextOptions(options, config) {
|
|
3369
|
-
const { registry, system: agentSystem, cacheControl, systemProviderOptions, tools: agentTools, } = config;
|
|
3370
|
-
/**
|
|
3371
|
-
* Only the three keys that collide are intercepted. Everything else, telemetry
|
|
3372
|
-
* included, stays in `rest` and reaches `streamText` untouched, with the
|
|
3373
|
-
* caller's value winning because `rest` is spread after `managed`. Pulling a
|
|
3374
|
-
* key out to "handle" it is how a caller's option gets silently dropped.
|
|
3375
|
-
*/
|
|
3376
|
-
const { tools, system: callerSystem, prepareStep: callerPrepareStep, ...rest } = options;
|
|
3377
|
-
const managed = toStreamTextOptions({
|
|
3378
|
-
registry,
|
|
3379
|
-
system: callerSystem ?? agentSystem,
|
|
3380
|
-
cacheControl,
|
|
3381
|
-
systemProviderOptions,
|
|
3382
|
-
/**
|
|
3383
|
-
* A call site that names `tools` replaces the agent's set rather than
|
|
3384
|
-
* adding to it, so narrowing the tools for one call still works. Omitting
|
|
3385
|
-
* `tools` falls back to the agent's, which is what an `onAction`
|
|
3386
|
-
* regenerate needs: without it a regenerated answer can call nothing.
|
|
3387
|
-
*/
|
|
3388
|
-
tools: (tools ?? agentTools),
|
|
3389
|
-
});
|
|
3390
|
-
const promptSystem = locals_js_1.locals.get(chatPromptKey)?.text;
|
|
3391
|
-
/**
|
|
3392
|
-
* Two managed sources conflict too, not only a caller against a managed one.
|
|
3393
|
-
* `toStreamTextOptions` resolves `prompt?.text || agentSystem`, so the prompt
|
|
3394
|
-
* would win and `chat.agent({ system })` would go nowhere.
|
|
3395
|
-
*/
|
|
3396
|
-
if (promptSystem && agentSystem) {
|
|
3397
|
-
throw new Error("chat.agent: `system` is set both on chat.agent({ system }) and by chat.prompt.set(), and only one " +
|
|
3398
|
-
"of them can apply. The prompt would win and the agent's `system` would go nowhere. Keep it in one " +
|
|
3399
|
-
"place, and add per-turn context with chat.inject({ role: 'system' }).");
|
|
3400
|
-
}
|
|
3401
|
-
const managedSystem = promptSystem || agentSystem;
|
|
3402
|
-
if (callerSystem !== undefined && managedSystem) {
|
|
3403
|
-
throw new Error("chat.agent: `system` is already set " +
|
|
3404
|
-
(promptSystem ? "by chat.prompt.set()" : "on chat.agent({ system })") +
|
|
3405
|
-
", so it cannot also be passed to the `streamText` given to run(). Set it in one place, and add " +
|
|
3406
|
-
"per-turn context with chat.inject({ role: 'system' }) rather than a second system value.");
|
|
3407
|
-
}
|
|
3408
|
-
const managedPrepareStep = managed.prepareStep;
|
|
3409
|
-
if (typeof callerPrepareStep === "function") {
|
|
3410
|
-
managed.prepareStep = async (arg) => {
|
|
3411
|
-
const first = managedPrepareStep ? await managedPrepareStep(arg) : undefined;
|
|
3412
|
-
const second = await callerPrepareStep({ ...arg, ...(first ?? {}) });
|
|
3413
|
-
return { ...(first ?? {}), ...(second ?? {}) };
|
|
3414
|
-
};
|
|
3415
|
-
}
|
|
3416
|
-
return { ...managed, ...rest };
|
|
3417
|
-
}
|
|
3418
|
-
/** @internal Test hook for {@link buildManagedStreamTextOptions}. */
|
|
3419
|
-
exports.__buildManagedStreamTextOptionsForTests = buildManagedStreamTextOptions;
|
|
3420
|
-
function createBoundStreamText(registry, agentSystem, agentCacheControl, agentSystemProviderOptions) {
|
|
3421
|
-
const bound = (options = {}) => (0, ai_runtime_js_1.streamText)(buildManagedStreamTextOptions(options, {
|
|
3422
|
-
registry,
|
|
3423
|
-
system: agentSystem,
|
|
3424
|
-
cacheControl: agentCacheControl,
|
|
3425
|
-
systemProviderOptions: agentSystemProviderOptions,
|
|
3426
|
-
/** Read per call, so per-turn tools resolved after binding are included. */
|
|
3427
|
-
tools: locals_js_1.locals.get(chatResolvedToolsKey),
|
|
3428
|
-
}));
|
|
3429
|
-
return bound;
|
|
3430
|
-
}
|
|
3431
3217
|
function toStreamTextOptions(options) {
|
|
3432
|
-
const agentDefaults = locals_js_1.locals.get(chatAgentManagedConfigKey);
|
|
3433
|
-
if (agentDefaults) {
|
|
3434
|
-
options = {
|
|
3435
|
-
registry: agentDefaults.registry,
|
|
3436
|
-
system: agentDefaults.system,
|
|
3437
|
-
cacheControl: agentDefaults.cacheControl,
|
|
3438
|
-
systemProviderOptions: agentDefaults.systemProviderOptions,
|
|
3439
|
-
...options,
|
|
3440
|
-
};
|
|
3441
|
-
}
|
|
3442
3218
|
const prompt = locals_js_1.locals.get(chatPromptKey);
|
|
3443
3219
|
const skills = locals_js_1.locals.get(chatSkillsKey);
|
|
3444
3220
|
const result = {};
|
|
3445
3221
|
// Build the combined system prompt: stored prompt + skills preamble.
|
|
3446
|
-
const
|
|
3447
|
-
const baseSystemText = typeof baseSystem === "string"
|
|
3448
|
-
? baseSystem
|
|
3449
|
-
: typeof baseSystem?.content === "string"
|
|
3450
|
-
? baseSystem.content
|
|
3451
|
-
: "";
|
|
3452
|
-
const promptText = prompt?.text || baseSystemText;
|
|
3222
|
+
const promptText = prompt?.text ?? "";
|
|
3453
3223
|
const skillsText = skills && skills.length > 0 ? buildSkillsSystemPrompt(skills) : "";
|
|
3454
3224
|
if (promptText || skillsText) {
|
|
3455
3225
|
const systemText = [promptText, skillsText].filter(Boolean).join("\n\n");
|
|
3456
|
-
|
|
3457
|
-
|
|
3458
|
-
|
|
3459
|
-
* `cacheControl` sugar, the ones carried on a structured `system` message,
|
|
3460
|
-
* then whatever `chat.prompt.set()` stored.
|
|
3461
|
-
*
|
|
3462
|
-
* A structured `system` counts only when its own text is the one being
|
|
3463
|
-
* sent. When `chat.prompt.set()` supplied the text, its provider options
|
|
3464
|
-
* are the ones that describe it.
|
|
3465
|
-
*/
|
|
3466
|
-
const baseSystemProviderOptions = !prompt?.text && baseSystem && typeof baseSystem !== "string"
|
|
3467
|
-
? baseSystem.providerOptions
|
|
3468
|
-
: undefined;
|
|
3226
|
+
// Resolve system-prompt provider options for caching. Precedence (most
|
|
3227
|
+
// specific wins, no deep merge): explicit `systemProviderOptions` →
|
|
3228
|
+
// `cacheControl` sugar → `providerOptions` stored on `chat.prompt.set()`.
|
|
3469
3229
|
const systemProviderOptions = options?.systemProviderOptions ??
|
|
3470
3230
|
(options?.cacheControl
|
|
3471
3231
|
? { anthropic: { cacheControl: options.cacheControl } }
|
|
3472
3232
|
: undefined) ??
|
|
3473
|
-
baseSystemProviderOptions ??
|
|
3474
3233
|
locals_js_1.locals.get(chatPromptProviderOptionsKey);
|
|
3475
3234
|
// A bare string stays a bare string (the unchanged default). With provider
|
|
3476
3235
|
// options, emit a structured `SystemModelMessage` so the provider can cache
|
|
@@ -3479,88 +3238,6 @@ function toStreamTextOptions(options) {
|
|
|
3479
3238
|
? { role: "system", content: systemText, providerOptions: systemProviderOptions }
|
|
3480
3239
|
: systemText;
|
|
3481
3240
|
}
|
|
3482
|
-
/**
|
|
3483
|
-
* Append anything injected as system context, in whichever shape the installed
|
|
3484
|
-
* AI SDK accepts.
|
|
3485
|
-
*
|
|
3486
|
-
* `system` widened over time: on ai@5 it is `string` only, and from ai@6 it is
|
|
3487
|
-
* `string | SystemModelMessage | Array<SystemModelMessage>`. This package's peer
|
|
3488
|
-
* range still spans all three, so emitting an array unconditionally would break
|
|
3489
|
-
* v5 consumers — for whom a system-role injection used to work, since v5 accepted
|
|
3490
|
-
* a system message inside `messages` that v7 rejects.
|
|
3491
|
-
*
|
|
3492
|
-
* So: concatenate into one string when the base is a plain string, which every
|
|
3493
|
-
* version accepts and which loses nothing (separate blocks only matter for
|
|
3494
|
-
* per-block `providerOptions`). Use the array form only when the base is already
|
|
3495
|
-
* a structured message — that path requires v6+ regardless, because it is how
|
|
3496
|
-
* prompt caching marks the system block, and flattening it would silently throw
|
|
3497
|
-
* the cache away.
|
|
3498
|
-
*
|
|
3499
|
-
* Either way the injected text goes last: the base prompt keeps its position for
|
|
3500
|
-
* caching, and the addition reads as a later amendment. A changed prefix does
|
|
3501
|
-
* cost the first call its cache hit, on turns that actually injected.
|
|
3502
|
-
*/
|
|
3503
|
-
/**
|
|
3504
|
-
* Consumed once per turn, not once per read, and moved out of the lane rather
|
|
3505
|
-
* than marked read in place.
|
|
3506
|
-
*
|
|
3507
|
-
* Per turn, because a `run()` that builds options twice (a cheap classifier
|
|
3508
|
-
* pass and then the answer) has to see the injection in both, and draining on
|
|
3509
|
-
* read hands it to whichever call ran first. Moved out, because blocks left in
|
|
3510
|
-
* the lane sit in front of anything injected during the same turn, and
|
|
3511
|
-
* clearing the lane on the next turn then destroys both. Outside a turn there
|
|
3512
|
-
* is no turn to scope the stash to, so the lane drains on read there.
|
|
3513
|
-
*/
|
|
3514
|
-
const injectedInstructions = locals_js_1.locals.get(chatInjectedInstructionsKey);
|
|
3515
|
-
const currentTurn = locals_js_1.locals.get(chatTurnContextKey)?.turn;
|
|
3516
|
-
const consumedThisTurn = currentTurn === undefined ? undefined : locals_js_1.locals.get(chatInstructionsConsumedKey);
|
|
3517
|
-
let injectedBlocks = [];
|
|
3518
|
-
if (consumedThisTurn && consumedThisTurn.turn === currentTurn) {
|
|
3519
|
-
injectedBlocks = consumedThisTurn.blocks;
|
|
3520
|
-
// Anything injected since the stash was taken joins it, so an instruction
|
|
3521
|
-
// added after an action read the lane still reaches the real turn that
|
|
3522
|
-
// shares the action's turn number, rather than the one after.
|
|
3523
|
-
if (injectedInstructions && injectedInstructions.length > 0) {
|
|
3524
|
-
injectedBlocks.push(...injectedInstructions.splice(0));
|
|
3525
|
-
}
|
|
3526
|
-
}
|
|
3527
|
-
else if (injectedInstructions && injectedInstructions.length > 0) {
|
|
3528
|
-
injectedBlocks = injectedInstructions.splice(0);
|
|
3529
|
-
if (currentTurn !== undefined) {
|
|
3530
|
-
locals_js_1.locals.set(chatInstructionsConsumedKey, { turn: currentTurn, blocks: injectedBlocks });
|
|
3531
|
-
}
|
|
3532
|
-
}
|
|
3533
|
-
if (injectedBlocks.length > 0) {
|
|
3534
|
-
const blocks = injectedBlocks;
|
|
3535
|
-
const injectedText = blocks
|
|
3536
|
-
.map((block) => (typeof block.content === "string" ? block.content : ""))
|
|
3537
|
-
.filter(Boolean)
|
|
3538
|
-
.join("\n\n");
|
|
3539
|
-
const base = result.system;
|
|
3540
|
-
if (base === undefined) {
|
|
3541
|
-
result.system = injectedText;
|
|
3542
|
-
}
|
|
3543
|
-
else if (typeof base === "string") {
|
|
3544
|
-
result.system = [base, injectedText].filter(Boolean).join("\n\n");
|
|
3545
|
-
}
|
|
3546
|
-
else {
|
|
3547
|
-
// Merged into the existing block rather than added as a second one. An array
|
|
3548
|
-
// of system blocks would keep the base block's cache entry, but ai@5 rejects
|
|
3549
|
-
// it outright ("Invalid prompt: system must be a string") while accepting a
|
|
3550
|
-
// single structured block, and this package's peer range still spans v5.
|
|
3551
|
-
// Choosing per version would mean resolving the installed version at runtime,
|
|
3552
|
-
// which is not something to build on: `import.meta.url` is illegal in this
|
|
3553
|
-
// package's CommonJS output, and a bundled task may have no resolvable `ai`
|
|
3554
|
-
// to read. One shape that works everywhere beats a cache hit.
|
|
3555
|
-
const baseBlock = base;
|
|
3556
|
-
result.system = {
|
|
3557
|
-
...baseBlock,
|
|
3558
|
-
content: [typeof baseBlock.content === "string" ? baseBlock.content : "", injectedText]
|
|
3559
|
-
.filter(Boolean)
|
|
3560
|
-
.join("\n\n"),
|
|
3561
|
-
};
|
|
3562
|
-
}
|
|
3563
|
-
}
|
|
3564
3241
|
// Prompt-related options (only if chat.prompt.set() was called)
|
|
3565
3242
|
if (prompt) {
|
|
3566
3243
|
// Resolve model via registry if both are present
|
|
@@ -3615,7 +3292,7 @@ function toStreamTextOptions(options) {
|
|
|
3615
3292
|
}
|
|
3616
3293
|
// 2. Pending message injection (steering)
|
|
3617
3294
|
if (taskPendingMessages) {
|
|
3618
|
-
const
|
|
3295
|
+
const injected = await drainSteeringQueue(taskPendingMessages, resultMessages ?? messages, steps);
|
|
3619
3296
|
if (injected.length > 0) {
|
|
3620
3297
|
resultMessages = [...(resultMessages ?? messages), ...injected];
|
|
3621
3298
|
}
|
|
@@ -3631,61 +3308,6 @@ function toStreamTextOptions(options) {
|
|
|
3631
3308
|
}
|
|
3632
3309
|
return result;
|
|
3633
3310
|
}
|
|
3634
|
-
const actionTurnBrand = Symbol.for("trigger.dev/chat/actionTurn");
|
|
3635
|
-
/**
|
|
3636
|
-
* Turn the current action into a turn.
|
|
3637
|
-
*
|
|
3638
|
-
* Return it from `onAction` after editing history. The action's own work is
|
|
3639
|
-
* finished first (the edit is applied and snapshotted), then a turn runs on the
|
|
3640
|
-
* result exactly as a message turn does: `onTurnStart`, `run()` with the edited
|
|
3641
|
-
* history, `onBeforeTurnComplete`, `onTurnComplete`, and the turn counter
|
|
3642
|
-
* advances. That gives the answer everything a turn has, the system prompt,
|
|
3643
|
-
* tools, steering, compaction, injected instructions and persistence, with no
|
|
3644
|
-
* action-specific handling.
|
|
3645
|
-
*
|
|
3646
|
-
* @example
|
|
3647
|
-
* ```ts
|
|
3648
|
-
* onAction: async ({ action }) => {
|
|
3649
|
-
* if (action.type === "regenerate") {
|
|
3650
|
-
* chat.history.slice(0, -1);
|
|
3651
|
-
* return chat.turn();
|
|
3652
|
-
* }
|
|
3653
|
-
* if (action.type === "undo") chat.history.slice(0, -2); // no turn
|
|
3654
|
-
* },
|
|
3655
|
-
* ```
|
|
3656
|
-
*/
|
|
3657
|
-
function chatTurn() {
|
|
3658
|
-
return { [actionTurnBrand]: true };
|
|
3659
|
-
}
|
|
3660
|
-
function isActionTurn(value) {
|
|
3661
|
-
return typeof value === "object" && value !== null && value[actionTurnBrand] === true;
|
|
3662
|
-
}
|
|
3663
|
-
/**
|
|
3664
|
-
* Replace, in a model lane, the run of messages one UI message contributed.
|
|
3665
|
-
*
|
|
3666
|
-
* Used when a UI message is replaced in place (a tool-approval continuation
|
|
3667
|
-
* merging onto the trailing assistant, a captured response reusing an existing
|
|
3668
|
-
* id, a partial replacing an existing message). Reconverting the whole lane
|
|
3669
|
-
* from the UI lane would also replace a compaction summary with the full
|
|
3670
|
-
* transcript and drop the model forms `pendingMessages.prepare` produced.
|
|
3671
|
-
*
|
|
3672
|
-
* The replaced message is the trailing one, so its run is the lane's tail,
|
|
3673
|
-
* before any steer forms appended after it this turn (`tailAfter`). If the
|
|
3674
|
-
* tail does not match the old message's conversion, nothing is changed and
|
|
3675
|
-
* `false` is returned so the caller can fall back to a full reconversion.
|
|
3676
|
-
*/
|
|
3677
|
-
async function replaceModelRun(lane, oldUi, newUi, tailAfter) {
|
|
3678
|
-
const oldRun = await toModelMessages([stripProviderMetadata(oldUi)]);
|
|
3679
|
-
const newRun = await toModelMessages([stripProviderMetadata(newUi)]);
|
|
3680
|
-
const end = lane.length - tailAfter;
|
|
3681
|
-
const start = end - oldRun.length;
|
|
3682
|
-
if (start < 0 || end > lane.length)
|
|
3683
|
-
return false;
|
|
3684
|
-
if (JSON.stringify(lane.slice(start, end)) !== JSON.stringify(oldRun))
|
|
3685
|
-
return false;
|
|
3686
|
-
lane.splice(start, oldRun.length, ...newRun);
|
|
3687
|
-
return true;
|
|
3688
|
-
}
|
|
3689
3311
|
function isUIMessageStreamable(value) {
|
|
3690
3312
|
return (typeof value === "object" &&
|
|
3691
3313
|
value !== null &&
|
|
@@ -3824,22 +3446,10 @@ function chatCustomAgent(options) {
|
|
|
3824
3446
|
await installChatInputRouter(payload.chatId, {
|
|
3825
3447
|
resuming: Boolean(payload.continuation),
|
|
3826
3448
|
});
|
|
3827
|
-
// A custom agent's loop is the customer's, so there is no exit site the
|
|
3828
|
-
// SDK controls. Perform a requested close when `run()` returns, whatever
|
|
3829
|
-
// shape the loop had. Idempotent, so the createSession iterator having
|
|
3830
|
-
// already closed on its own exit costs nothing.
|
|
3831
|
-
const withClose = async (result) => {
|
|
3832
|
-
try {
|
|
3833
|
-
return await result;
|
|
3834
|
-
}
|
|
3835
|
-
finally {
|
|
3836
|
-
await performChatClose();
|
|
3837
|
-
}
|
|
3838
|
-
};
|
|
3839
3449
|
// Keep the schema-free path identical to the original custom-agent
|
|
3840
3450
|
// wrapper, including when userRun starts executing.
|
|
3841
3451
|
if (!parseClientData) {
|
|
3842
|
-
return
|
|
3452
|
+
return userRun(payload, runOptions);
|
|
3843
3453
|
}
|
|
3844
3454
|
const isHandoverBoot = payload.trigger === "handover-prepare";
|
|
3845
3455
|
const isMessagelessBoot = payload.trigger === "preload" ||
|
|
@@ -3855,7 +3465,7 @@ function chatCustomAgent(options) {
|
|
|
3855
3465
|
writeErrorToStream: !isMessagelessBoot && !isHandoverBoot,
|
|
3856
3466
|
});
|
|
3857
3467
|
if (validated.ok) {
|
|
3858
|
-
return
|
|
3468
|
+
return userRun(validated.payload, runOptions);
|
|
3859
3469
|
}
|
|
3860
3470
|
if (isHandoverBoot) {
|
|
3861
3471
|
const signal = await waitForHandover({
|
|
@@ -3891,7 +3501,7 @@ function chatCustomAgent(options) {
|
|
|
3891
3501
|
sessionId: next.output.sessionId ?? payload.sessionId,
|
|
3892
3502
|
idleTimeoutInSeconds: next.output.idleTimeoutInSeconds ?? payload.idleTimeoutInSeconds,
|
|
3893
3503
|
};
|
|
3894
|
-
return
|
|
3504
|
+
return userRun(recoveredPayload, runOptions);
|
|
3895
3505
|
},
|
|
3896
3506
|
});
|
|
3897
3507
|
// Register clientDataSchema so the CLI converts it to JSONSchema
|
|
@@ -3904,7 +3514,7 @@ function chatCustomAgent(options) {
|
|
|
3904
3514
|
return task;
|
|
3905
3515
|
}
|
|
3906
3516
|
function chatAgent(options) {
|
|
3907
|
-
const { run: userRun, clientDataSchema, onBoot, onRecoveryBoot, onPreload, onChatStart, onValidateMessages, hydrateMessages, actionSchema, onAction, onTurnStart, onBeforeTurnComplete, onCompacted, compaction, pendingMessages: pendingMessagesConfig, prepareMessages, tools: toolsOption, onTurnComplete, maxTurns = 100, turnTimeout = "1h", idleTimeoutInSeconds = 30, chatAccessTokenTTL = "1h", preloadIdleTimeoutInSeconds, preloadTimeout, uiMessageStreamOptions, onChatSuspend, onChatResume, exitAfterPreloadIdle = false, oomMachine,
|
|
3517
|
+
const { run: userRun, clientDataSchema, onBoot, onRecoveryBoot, onPreload, onChatStart, onValidateMessages, hydrateMessages, actionSchema, onAction, onTurnStart, onBeforeTurnComplete, onCompacted, compaction, pendingMessages: pendingMessagesConfig, prepareMessages, tools: toolsOption, onTurnComplete, maxTurns = 100, turnTimeout = "1h", idleTimeoutInSeconds = 30, chatAccessTokenTTL = "1h", preloadIdleTimeoutInSeconds, preloadTimeout, uiMessageStreamOptions, onChatSuspend, onChatResume, exitAfterPreloadIdle = false, oomMachine, ...restOptions } = options;
|
|
3908
3518
|
const parseClientData = clientDataSchema ? (0, v3_1.getSchemaParseFn)(clientDataSchema) : undefined;
|
|
3909
3519
|
const parseAction = actionSchema ? (0, v3_1.getSchemaParseFn)(actionSchema) : undefined;
|
|
3910
3520
|
// chat.agent does not expose generic retry options (see docstring on
|
|
@@ -3987,24 +3597,6 @@ function chatAgent(options) {
|
|
|
3987
3597
|
// durable snapshot + `session.out` replay (or `hydrateMessages` if
|
|
3988
3598
|
// registered) — the wire is delta-only now, no longer a seed.
|
|
3989
3599
|
let accumulatedMessages = [];
|
|
3990
|
-
/**
|
|
3991
|
-
* Give the model accumulator the steering messages a drain consumed,
|
|
3992
|
-
* in the form the model actually received. Appended, never reconverted
|
|
3993
|
-
* from the UI lane, so a model-only compaction summary survives. Called
|
|
3994
|
-
* on both the success and the error path, before the response or the
|
|
3995
|
-
* partial joins the lane, so the order stays steer-then-answer.
|
|
3996
|
-
*/
|
|
3997
|
-
const reconcilePendingSteer = (options) => {
|
|
3998
|
-
const pending = locals_js_1.locals.get(chatPendingSteerKey);
|
|
3999
|
-
if (!pending || pending.length === 0)
|
|
4000
|
-
return [];
|
|
4001
|
-
locals_js_1.locals.set(chatPendingSteerKey, []);
|
|
4002
|
-
for (const entry of pending) {
|
|
4003
|
-
accumulatedMessages.push(...entry.model);
|
|
4004
|
-
options?.turnNew?.push(...entry.model);
|
|
4005
|
-
}
|
|
4006
|
-
return pending;
|
|
4007
|
-
};
|
|
4008
3600
|
// Accumulated UI messages for persistence. Mirrors the model accumulator
|
|
4009
3601
|
// but in frontend-friendly UIMessage format (with parts, id, etc.).
|
|
4010
3602
|
let accumulatedUIMessages = [];
|
|
@@ -4023,58 +3615,6 @@ function chatAgent(options) {
|
|
|
4023
3615
|
// swallow errors internally; the agent stays available either way.
|
|
4024
3616
|
const sessionIdForSnapshot = payload.sessionId ?? payload.chatId;
|
|
4025
3617
|
let bootSnapshot;
|
|
4026
|
-
/**
|
|
4027
|
-
* The `lastOutEventId` the most recent snapshot carried.
|
|
4028
|
-
*
|
|
4029
|
-
* A snapshot written outside a turn — after an action mutates history — has
|
|
4030
|
-
* no turn cursor of its own, and writing `undefined` there would drop the
|
|
4031
|
-
* resume point and make the next boot replay from further back. Retaining it
|
|
4032
|
-
* keeps an action's write cursor-neutral.
|
|
4033
|
-
*/
|
|
4034
|
-
let lastSnapshotOutEventId;
|
|
4035
|
-
/**
|
|
4036
|
-
* Persist the accumulator outside a turn.
|
|
4037
|
-
*
|
|
4038
|
-
* An action is not a turn, so it never reaches the turn-complete path where
|
|
4039
|
-
* the snapshot is normally written — but it can change the conversation in
|
|
4040
|
-
* two ways: a `chat.history` mutation, and a response streamed back from
|
|
4041
|
-
* `onAction`. Both have to survive, and one write at the end of the action
|
|
4042
|
-
* covers both rather than writing twice for a regenerate that does both.
|
|
4043
|
-
*
|
|
4044
|
-
* Cursor-neutral: an action has no turn cursor of its own, and writing
|
|
4045
|
-
* `undefined` would drop the resume point the last turn established and make
|
|
4046
|
-
* the next boot replay from further back.
|
|
4047
|
-
*/
|
|
4048
|
-
const writeSnapshotOutsideTurn = async (reason) => {
|
|
4049
|
-
if (hydrateMessages)
|
|
4050
|
-
return;
|
|
4051
|
-
try {
|
|
4052
|
-
await tracer_js_1.tracer.startActiveSpan("snapshot.write", async () => {
|
|
4053
|
-
const snapshotInCursor = chatInputRouter().resumeFloor();
|
|
4054
|
-
await writeChatSnapshot(sessionIdForSnapshot, {
|
|
4055
|
-
version: 1,
|
|
4056
|
-
savedAt: Date.now(),
|
|
4057
|
-
messages: accumulatedUIMessages,
|
|
4058
|
-
lastOutEventId: lastSnapshotOutEventId,
|
|
4059
|
-
lastInEventId: snapshotInCursor !== undefined ? String(snapshotInCursor) : undefined,
|
|
4060
|
-
});
|
|
4061
|
-
}, {
|
|
4062
|
-
attributes: {
|
|
4063
|
-
[v3_1.SemanticInternalAttributes.STYLE_ICON]: "task-hook-onStart",
|
|
4064
|
-
[v3_1.SemanticInternalAttributes.COLLAPSED]: true,
|
|
4065
|
-
"chat.snapshot.reason": reason,
|
|
4066
|
-
"chat.messages.count": accumulatedUIMessages.length,
|
|
4067
|
-
},
|
|
4068
|
-
});
|
|
4069
|
-
}
|
|
4070
|
-
catch (error) {
|
|
4071
|
-
v3_1.logger.warn("chat.agent: snapshot write outside a turn failed; the change may not survive a continuation", {
|
|
4072
|
-
error: error instanceof Error ? error.message : String(error),
|
|
4073
|
-
sessionId: sessionIdForSnapshot,
|
|
4074
|
-
reason,
|
|
4075
|
-
});
|
|
4076
|
-
}
|
|
4077
|
-
};
|
|
4078
3618
|
let replayedSettled = [];
|
|
4079
3619
|
let replayedPartial;
|
|
4080
3620
|
let replayedPartialRaw;
|
|
@@ -4117,7 +3657,6 @@ function chatAgent(options) {
|
|
|
4117
3657
|
// Without seeding, the new worker would emit no trim on its first
|
|
4118
3658
|
// turn (chain self-bootstraps from turn 2), so this is purely an
|
|
4119
3659
|
// optimization to keep continuation runs bounded from the first turn.
|
|
4120
|
-
lastSnapshotOutEventId = bootSnapshot?.lastOutEventId;
|
|
4121
3660
|
if (bootSnapshot?.lastOutEventId !== undefined) {
|
|
4122
3661
|
const seeded = Number.parseInt(bootSnapshot.lastOutEventId, 10);
|
|
4123
3662
|
if (Number.isFinite(seeded)) {
|
|
@@ -4212,13 +3751,9 @@ function chatAgent(options) {
|
|
|
4212
3751
|
// Reads the turn boundary and subscribes in one call. `bootInCursor` is
|
|
4213
3752
|
// only a fallback: the boot block above may already have resolved a
|
|
4214
3753
|
// cursor from the snapshot, which is used when the boundary itself
|
|
4215
|
-
// carries none.
|
|
4216
|
-
// `bootInjectedQueue` below, so it goes into the floor here — folded in
|
|
4217
|
-
// after the subscription opens, the live tail re-delivers it as a turn.
|
|
4218
|
-
const lastRecoveredInSeq = replayedInTail.length > 0 ? replayedInTail[replayedInTail.length - 1].seqNum : undefined;
|
|
3754
|
+
// carries none.
|
|
4219
3755
|
await installChatInputRouter(payload.chatId, {
|
|
4220
3756
|
fallbackResumeFrom: bootInCursorResolved ? bootInCursor : undefined,
|
|
4221
|
-
recoveredThrough: lastRecoveredInSeq,
|
|
4222
3757
|
resuming: Boolean(payload.continuation) || ctx.attempt.number > 1,
|
|
4223
3758
|
});
|
|
4224
3759
|
// ── Recovery boot + chain reconstruction ────────────────────────
|
|
@@ -4326,6 +3861,16 @@ function chatAgent(options) {
|
|
|
4326
3861
|
if (hookBeforeBoot) {
|
|
4327
3862
|
await hookBeforeBoot();
|
|
4328
3863
|
}
|
|
3864
|
+
// Advance the session.in cursor past every recovered user so
|
|
3865
|
+
// the live subscription doesn't re-deliver them.
|
|
3866
|
+
if (replayedInTail.length > 0) {
|
|
3867
|
+
const lastRecoveredSeq = replayedInTail[replayedInTail.length - 1].seqNum;
|
|
3868
|
+
const currentCursor = v3_1.sessionStreams.lastSeqNum(payload.chatId, "in");
|
|
3869
|
+
if (currentCursor === undefined || lastRecoveredSeq > currentCursor) {
|
|
3870
|
+
v3_1.sessionStreams.setLastSeqNum(payload.chatId, "in", lastRecoveredSeq);
|
|
3871
|
+
v3_1.sessionStreams.setLastDispatchedSeqNum(payload.chatId, "in", lastRecoveredSeq);
|
|
3872
|
+
}
|
|
3873
|
+
}
|
|
4329
3874
|
// Synthesize wire payloads for each recoveredTurn. The turn-loop
|
|
4330
3875
|
// pops these ahead of `messagesInput.waitWithIdleTimeout` so they
|
|
4331
3876
|
// dispatch as normal turns with the existing hook stack.
|
|
@@ -4418,12 +3963,6 @@ function chatAgent(options) {
|
|
|
4418
3963
|
// before any hook (`onChatStart`, `onTurnStart`, etc.) fires.
|
|
4419
3964
|
locals_js_1.locals.set(chatCurrentUIMessagesKey, accumulatedUIMessages);
|
|
4420
3965
|
}
|
|
4421
|
-
locals_js_1.locals.set(chatAgentManagedConfigKey, {
|
|
4422
|
-
registry: promptRegistry,
|
|
4423
|
-
system: agentSystem,
|
|
4424
|
-
cacheControl: agentCacheControl,
|
|
4425
|
-
systemProviderOptions: agentSystemProviderOptions,
|
|
4426
|
-
});
|
|
4427
3966
|
// Token usage tracking across turns
|
|
4428
3967
|
let previousTurnUsage;
|
|
4429
3968
|
let cumulativeUsage = emptyUsage();
|
|
@@ -4919,10 +4458,6 @@ function chatAgent(options) {
|
|
|
4919
4458
|
// Track new messages for this turn (user input + assistant response).
|
|
4920
4459
|
const turnNewModelMessages = [];
|
|
4921
4460
|
const turnNewUIMessages = [];
|
|
4922
|
-
locals_js_1.locals.set(chatTurnNewUIMessagesKey, turnNewUIMessages);
|
|
4923
|
-
// A head-start handover deliberately resumes from an assistant
|
|
4924
|
-
// message it spliced in, so it isn't a no-op turn.
|
|
4925
|
-
let splicedHandoverPartial = false;
|
|
4926
4461
|
// ── Action handling ──────────────────────────────────────
|
|
4927
4462
|
// Actions arrive on the same input stream but with
|
|
4928
4463
|
// trigger === "action". They are NOT turns — only
|
|
@@ -4933,16 +4468,7 @@ function chatAgent(options) {
|
|
|
4933
4468
|
// an action, return a `StreamTextResult` (auto-piped),
|
|
4934
4469
|
// string, or UIMessage from `onAction`. Turn counter
|
|
4935
4470
|
// does not advance.
|
|
4936
|
-
let
|
|
4937
|
-
/** Set when `onAction` returned `chat.turn()`: the turn block below runs. */
|
|
4938
|
-
let actionTurn = false;
|
|
4939
|
-
/**
|
|
4940
|
-
* Whether this action changed the conversation, by rolling history
|
|
4941
|
-
* back or by streaming a response. Drives the single snapshot write
|
|
4942
|
-
* at the end — an action never reaches the turn-complete path that
|
|
4943
|
-
* normally does it.
|
|
4944
|
-
*/
|
|
4945
|
-
let actionChangedHistory = false;
|
|
4471
|
+
let actionStreamResult = undefined;
|
|
4946
4472
|
if (isAction) {
|
|
4947
4473
|
// Parse and validate the action payload
|
|
4948
4474
|
const parsedAction = parseAction
|
|
@@ -4976,7 +4502,7 @@ function chatAgent(options) {
|
|
|
4976
4502
|
// Fire onAction — handler may mutate state via
|
|
4977
4503
|
// `chat.history.*` and / or return a model response.
|
|
4978
4504
|
if (onAction) {
|
|
4979
|
-
|
|
4505
|
+
actionStreamResult = await tracer_js_1.tracer.startActiveSpan("onAction()", async () => {
|
|
4980
4506
|
return await onAction({
|
|
4981
4507
|
action: parsedAction,
|
|
4982
4508
|
chatId: currentWirePayload.chatId,
|
|
@@ -5002,7 +4528,6 @@ function chatAgent(options) {
|
|
|
5002
4528
|
accumulatedUIMessages = [...actionOverride];
|
|
5003
4529
|
accumulatedMessages = await toModelMessages(actionOverride);
|
|
5004
4530
|
locals_js_1.locals.set(chatCurrentUIMessagesKey, accumulatedUIMessages);
|
|
5005
|
-
actionChangedHistory = true;
|
|
5006
4531
|
}
|
|
5007
4532
|
}
|
|
5008
4533
|
else {
|
|
@@ -5171,7 +4696,6 @@ function chatAgent(options) {
|
|
|
5171
4696
|
// where AI SDK regenerates the id (TRI-9137) still
|
|
5172
4697
|
// applies via `rewriteIncomingIdViaToolCallMap`.
|
|
5173
4698
|
let replaced = false;
|
|
5174
|
-
const replacedPairs = [];
|
|
5175
4699
|
for (const raw of cleanedUIMessages) {
|
|
5176
4700
|
let incoming = raw;
|
|
5177
4701
|
let idx = accumulatedUIMessages.findIndex((m) => m.id === incoming.id);
|
|
@@ -5183,9 +4707,7 @@ function chatAgent(options) {
|
|
|
5183
4707
|
}
|
|
5184
4708
|
}
|
|
5185
4709
|
if (idx !== -1) {
|
|
5186
|
-
|
|
5187
|
-
accumulatedUIMessages[idx] = mergeIncomingIntoHydrated(previous, incoming);
|
|
5188
|
-
replacedPairs.push({ previous, merged: accumulatedUIMessages[idx] });
|
|
4710
|
+
accumulatedUIMessages[idx] = mergeIncomingIntoHydrated(accumulatedUIMessages[idx], incoming);
|
|
5189
4711
|
replaced = true;
|
|
5190
4712
|
}
|
|
5191
4713
|
else {
|
|
@@ -5195,17 +4717,9 @@ function chatAgent(options) {
|
|
|
5195
4717
|
recordToolCallIdsFromMessage(incoming);
|
|
5196
4718
|
}
|
|
5197
4719
|
if (replaced) {
|
|
5198
|
-
|
|
5199
|
-
|
|
5200
|
-
|
|
5201
|
-
inPlace = false;
|
|
5202
|
-
break;
|
|
5203
|
-
}
|
|
5204
|
-
}
|
|
5205
|
-
if (!inPlace) {
|
|
5206
|
-
v3_1.logger.warn("chat.agent: replaced message not found at the model lane tail; reconverting the lane");
|
|
5207
|
-
accumulatedMessages = await toModelMessages(accumulatedUIMessages);
|
|
5208
|
-
}
|
|
4720
|
+
// Replacement changes structure — reconvert all model
|
|
4721
|
+
// messages instead of appending.
|
|
4722
|
+
accumulatedMessages = await toModelMessages(accumulatedUIMessages);
|
|
5209
4723
|
}
|
|
5210
4724
|
else {
|
|
5211
4725
|
const incomingModelMessages = await toModelMessages(cleanedUIMessages);
|
|
@@ -5248,33 +4762,10 @@ function chatAgent(options) {
|
|
|
5248
4762
|
messageId: locals_js_1.locals.get(chatHandoverMessageIdKey),
|
|
5249
4763
|
});
|
|
5250
4764
|
locals_js_1.locals.set(chatHandoverPartialKey, []); // consume once
|
|
5251
|
-
splicedHandoverPartial = true;
|
|
5252
4765
|
}
|
|
5253
4766
|
}
|
|
5254
4767
|
locals_js_1.locals.set(chatCurrentUIMessagesKey, accumulatedUIMessages);
|
|
5255
4768
|
} // end if (trigger !== "action")
|
|
5256
|
-
// ── No-op turn ──────────────────────────────────────────
|
|
5257
|
-
//
|
|
5258
|
-
// A submit that added no new user message and leaves the model
|
|
5259
|
-
// chain ending on an assistant message has nothing to answer —
|
|
5260
|
-
// calling the model would prefill its own last reply. Keyed on
|
|
5261
|
-
// the model tail, so a `tool`-terminated chain (a merged tool
|
|
5262
|
-
// approval) still runs.
|
|
5263
|
-
const isNoOpTurn = !isAction &&
|
|
5264
|
-
!splicedHandoverPartial &&
|
|
5265
|
-
currentWirePayload.trigger === "submit-message" &&
|
|
5266
|
-
turnNewUIMessages.length === 0 &&
|
|
5267
|
-
accumulatedMessages[accumulatedMessages.length - 1]?.role === "assistant";
|
|
5268
|
-
if (isNoOpTurn) {
|
|
5269
|
-
msgSub?.off();
|
|
5270
|
-
v3_1.logger.warn("chat.agent: turn added no new user message; skipping the model", {
|
|
5271
|
-
chatId: currentWirePayload.chatId,
|
|
5272
|
-
messageId: currentWirePayload.messageId,
|
|
5273
|
-
});
|
|
5274
|
-
await writeTurnCompleteChunk(currentWirePayload.chatId);
|
|
5275
|
-
// Not a turn — don't consume an iteration.
|
|
5276
|
-
turn--;
|
|
5277
|
-
}
|
|
5278
4769
|
// ── Action result handling ──────────────────────────────
|
|
5279
4770
|
// For action turns, skip the turn machinery entirely.
|
|
5280
4771
|
// If `onAction` returned a stream / string / UIMessage,
|
|
@@ -5284,34 +4775,34 @@ function chatAgent(options) {
|
|
|
5284
4775
|
// The turn counter is decremented so the next iteration
|
|
5285
4776
|
// sees the same `turn` value — actions don't count.
|
|
5286
4777
|
if (isAction) {
|
|
5287
|
-
|
|
5288
|
-
|
|
5289
|
-
|
|
5290
|
-
|
|
5291
|
-
|
|
5292
|
-
|
|
5293
|
-
|
|
4778
|
+
msgSub?.off();
|
|
4779
|
+
if ((locals_js_1.locals.get(chatPipeCountKey) ?? 0) === 0 &&
|
|
4780
|
+
isUIMessageStreamable(actionStreamResult)) {
|
|
4781
|
+
try {
|
|
4782
|
+
const resolvedOptions = resolveUIMessageStreamOptions();
|
|
4783
|
+
const uiStream = actionStreamResult.toUIMessageStream({
|
|
4784
|
+
...resolvedOptions,
|
|
4785
|
+
generateMessageId: resolvedOptions.generateMessageId ?? ai_runtime_js_1.generateId,
|
|
4786
|
+
});
|
|
4787
|
+
await pipeChat(uiStream, {
|
|
4788
|
+
signal: combinedSignal,
|
|
4789
|
+
spanName: "stream response",
|
|
4790
|
+
});
|
|
5294
4791
|
}
|
|
5295
|
-
|
|
5296
|
-
|
|
5297
|
-
|
|
5298
|
-
|
|
5299
|
-
|
|
5300
|
-
|
|
5301
|
-
|
|
5302
|
-
else {
|
|
5303
|
-
msgSub?.off();
|
|
5304
|
-
if (actionChangedHistory) {
|
|
5305
|
-
await writeSnapshotOutsideTurn("action");
|
|
4792
|
+
catch (error) {
|
|
4793
|
+
if (error instanceof Error &&
|
|
4794
|
+
error.name === "AbortError" &&
|
|
4795
|
+
runSignal.aborted) {
|
|
4796
|
+
return "exit";
|
|
4797
|
+
}
|
|
4798
|
+
throw error;
|
|
5306
4799
|
}
|
|
5307
|
-
await writeTurnCompleteChunk(currentWirePayload.chatId);
|
|
5308
|
-
// Don't consume a turn iteration — actions aren't turns.
|
|
5309
|
-
turn--;
|
|
5310
4800
|
}
|
|
4801
|
+
await writeTurnCompleteChunk(currentWirePayload.chatId);
|
|
4802
|
+
// Don't consume a turn iteration — actions aren't turns.
|
|
4803
|
+
turn--;
|
|
5311
4804
|
}
|
|
5312
|
-
|
|
5313
|
-
// there is nothing to answer, so nothing to hand over.
|
|
5314
|
-
if ((!isAction || actionTurn) && !isNoOpTurn) {
|
|
4805
|
+
if (!isAction) {
|
|
5315
4806
|
// Mint a scoped public access token once per turn, reused for
|
|
5316
4807
|
// onChatStart, onTurnStart, onTurnComplete, and the turn-complete chunk.
|
|
5317
4808
|
const currentRunId = ctx.run.id;
|
|
@@ -5416,11 +4907,9 @@ function chatAgent(options) {
|
|
|
5416
4907
|
},
|
|
5417
4908
|
});
|
|
5418
4909
|
}
|
|
5419
|
-
await followSessionPin(currentWirePayload.chatId, versionSkew);
|
|
5420
4910
|
// chat.requestUpgrade() called in onTurnStart (or onValidateMessages) —
|
|
5421
|
-
// skip run() and
|
|
5422
|
-
//
|
|
5423
|
-
// keeps reading.
|
|
4911
|
+
// skip run() and signal the transport to re-trigger the same message
|
|
4912
|
+
// on the new version.
|
|
5424
4913
|
if (locals_js_1.locals.get(chatUpgradeRequestedKey)) {
|
|
5425
4914
|
await writeUpgradeRequiredChunk();
|
|
5426
4915
|
return "exit";
|
|
@@ -5479,9 +4968,6 @@ function chatAgent(options) {
|
|
|
5479
4968
|
const preparedMessages = await applyPrepareMessages(accumulatedMessages, "run");
|
|
5480
4969
|
runResult = await userRun({
|
|
5481
4970
|
...restWire,
|
|
5482
|
-
// A turn requested by chat.turn() is not the action itself:
|
|
5483
|
-
// a run() that short-circuits on "action" must still answer.
|
|
5484
|
-
...(actionTurn ? { trigger: "action-turn" } : {}),
|
|
5485
4971
|
messages: preparedMessages,
|
|
5486
4972
|
clientData,
|
|
5487
4973
|
continuation,
|
|
@@ -5494,7 +4980,6 @@ function chatAgent(options) {
|
|
|
5494
4980
|
signal: combinedSignal,
|
|
5495
4981
|
cancelSignal,
|
|
5496
4982
|
stopSignal,
|
|
5497
|
-
streamText: createBoundStreamText(promptRegistry, agentSystem, agentCacheControl, agentSystemProviderOptions),
|
|
5498
4983
|
});
|
|
5499
4984
|
}
|
|
5500
4985
|
// Auto-pipe if the run function returned a StreamTextResult or similar,
|
|
@@ -5608,19 +5093,7 @@ function chatAgent(options) {
|
|
|
5608
5093
|
if (runOverride) {
|
|
5609
5094
|
locals_js_1.locals.set(chatOverrideMessagesKey, undefined);
|
|
5610
5095
|
accumulatedUIMessages = [...runOverride];
|
|
5611
|
-
|
|
5612
|
-
* Steers the drain consumed are left out of the rebuild and
|
|
5613
|
-
* appended by the reconciliation below instead, so the lane
|
|
5614
|
-
* gets the form the model actually received rather than a
|
|
5615
|
-
* reconversion of the UI message, and gets it once. A steer
|
|
5616
|
-
* the edit removed is dropped from the pending list too, so
|
|
5617
|
-
* the edit is honoured.
|
|
5618
|
-
*/
|
|
5619
|
-
const overrideIds = new Set(runOverride.map((m) => m.id));
|
|
5620
|
-
const pending = (locals_js_1.locals.get(chatPendingSteerKey) ?? []).filter((e) => overrideIds.has(e.ui.id));
|
|
5621
|
-
locals_js_1.locals.set(chatPendingSteerKey, pending);
|
|
5622
|
-
const pendingIds = new Set(pending.map((e) => e.ui.id));
|
|
5623
|
-
accumulatedMessages = await toModelMessages(runOverride.filter((m) => !pendingIds.has(m.id)));
|
|
5096
|
+
accumulatedMessages = await toModelMessages(runOverride);
|
|
5624
5097
|
locals_js_1.locals.set(chatCurrentUIMessagesKey, accumulatedUIMessages);
|
|
5625
5098
|
}
|
|
5626
5099
|
// Check if compaction set a model-only override (preserves UI messages).
|
|
@@ -5650,15 +5123,6 @@ function chatAgent(options) {
|
|
|
5650
5123
|
}
|
|
5651
5124
|
// Determine if the user stopped generation this turn (not a full run cancel).
|
|
5652
5125
|
const wasStopped = stopController.signal.aborted && !runSignal.aborted;
|
|
5653
|
-
// Give the model accumulator the steering messages the drain
|
|
5654
|
-
// consumed. Appended, never reconverted from the UI lane, so a
|
|
5655
|
-
// model-only compaction summary set just above survives; and done
|
|
5656
|
-
// before the response is appended so the order stays
|
|
5657
|
-
// steer-then-answer. Outside the `capturedResponseMessage`
|
|
5658
|
-
// branches below, so a turn that captured no response is covered.
|
|
5659
|
-
const steerTailThisTurn = reconcilePendingSteer({
|
|
5660
|
-
turnNew: turnNewModelMessages,
|
|
5661
|
-
}).reduce((n, e) => n + e.model.length, 0);
|
|
5662
5126
|
// Append the assistant's response (partial or complete) to the accumulator.
|
|
5663
5127
|
// The onFinish callback fires even on abort/stop, so partial responses
|
|
5664
5128
|
// from stopped generation are captured correctly.
|
|
@@ -5696,7 +5160,6 @@ function chatAgent(options) {
|
|
|
5696
5160
|
const existingIdx = capturedResponseMessage.id
|
|
5697
5161
|
? accumulatedUIMessages.findIndex((m) => m.id === capturedResponseMessage.id)
|
|
5698
5162
|
: -1;
|
|
5699
|
-
const previousAtIdx = existingIdx !== -1 ? accumulatedUIMessages[existingIdx] : undefined;
|
|
5700
5163
|
if (existingIdx !== -1) {
|
|
5701
5164
|
accumulatedUIMessages[existingIdx] = capturedResponseMessage;
|
|
5702
5165
|
}
|
|
@@ -5716,12 +5179,8 @@ function chatAgent(options) {
|
|
|
5716
5179
|
stripProviderMetadata(capturedResponseMessage),
|
|
5717
5180
|
]);
|
|
5718
5181
|
if (existingIdx !== -1) {
|
|
5719
|
-
|
|
5720
|
-
|
|
5721
|
-
if (!ok) {
|
|
5722
|
-
v3_1.logger.warn("chat.agent: replaced response not found at the model lane tail; reconverting the lane");
|
|
5723
|
-
accumulatedMessages = await toModelMessages(accumulatedUIMessages);
|
|
5724
|
-
}
|
|
5182
|
+
// Reconvert all model messages since we replaced rather than appended
|
|
5183
|
+
accumulatedMessages = await toModelMessages(accumulatedUIMessages);
|
|
5725
5184
|
}
|
|
5726
5185
|
else {
|
|
5727
5186
|
accumulatedMessages.push(...responseModelMessages);
|
|
@@ -6018,13 +5477,11 @@ function chatAgent(options) {
|
|
|
6018
5477
|
try {
|
|
6019
5478
|
await tracer_js_1.tracer.startActiveSpan("snapshot.write", async () => {
|
|
6020
5479
|
const snapshotInCursor = chatInputRouter().resumeFloor();
|
|
6021
|
-
lastSnapshotOutEventId =
|
|
6022
|
-
turnCompleteResult?.lastEventId ?? lastSnapshotOutEventId;
|
|
6023
5480
|
await writeChatSnapshot(sessionIdForSnapshot, {
|
|
6024
5481
|
version: 1,
|
|
6025
5482
|
savedAt: Date.now(),
|
|
6026
5483
|
messages: accumulatedUIMessages,
|
|
6027
|
-
lastOutEventId:
|
|
5484
|
+
lastOutEventId: turnCompleteResult?.lastEventId,
|
|
6028
5485
|
lastInEventId: snapshotInCursor !== undefined ? String(snapshotInCursor) : undefined,
|
|
6029
5486
|
});
|
|
6030
5487
|
}, {
|
|
@@ -6058,17 +5515,10 @@ function chatAgent(options) {
|
|
|
6058
5515
|
currentWirePayload = bootInjectedQueue.shift();
|
|
6059
5516
|
return "continue";
|
|
6060
5517
|
}
|
|
6061
|
-
// chat.requestUpgrade() was called — exit the loop
|
|
6062
|
-
//
|
|
5518
|
+
// chat.requestUpgrade() was called — exit the loop so the
|
|
5519
|
+
// transport triggers a new run on the latest version.
|
|
6063
5520
|
// chat.endRun() — same exit, no upgrade semantics.
|
|
6064
|
-
if (locals_js_1.locals.get(chatCloseRequestedKey)) {
|
|
6065
|
-
await performChatClose();
|
|
6066
|
-
return "exit";
|
|
6067
|
-
}
|
|
6068
5521
|
if (locals_js_1.locals.get(chatUpgradeRequestedKey) || locals_js_1.locals.get(chatEndRunRequestedKey)) {
|
|
6069
|
-
if (locals_js_1.locals.get(chatUpgradeRequestedKey)) {
|
|
6070
|
-
await persistUpgradeHandoff();
|
|
6071
|
-
}
|
|
6072
5522
|
return "exit";
|
|
6073
5523
|
}
|
|
6074
5524
|
// Wait for the next message — stay idle briefly, then suspend
|
|
@@ -6175,10 +5625,6 @@ function chatAgent(options) {
|
|
|
6175
5625
|
});
|
|
6176
5626
|
// Signal turn complete so the client knows this turn is done
|
|
6177
5627
|
errorTurnCompleteResult = await writeTurnCompleteChunk(currentWirePayload.chatId);
|
|
6178
|
-
// A later action's snapshot reuses this cursor, so it has to move
|
|
6179
|
-
// here too or that snapshot resumes from before the failed turn.
|
|
6180
|
-
lastSnapshotOutEventId =
|
|
6181
|
-
errorTurnCompleteResult?.lastEventId ?? lastSnapshotOutEventId;
|
|
6182
5628
|
}
|
|
6183
5629
|
catch {
|
|
6184
5630
|
// Best-effort — if stream write fails, let the run continue anyway
|
|
@@ -6223,46 +5669,15 @@ function chatAgent(options) {
|
|
|
6223
5669
|
: partialIdx === -1
|
|
6224
5670
|
? [...erroredUIMessages, partialResponse]
|
|
6225
5671
|
: erroredUIMessages.map((m, i) => i === partialIdx ? partialResponse : m);
|
|
6226
|
-
|
|
6227
|
-
|
|
6228
|
-
|
|
6229
|
-
|
|
6230
|
-
* instruction whenever the turn it steered went on to fail.
|
|
6231
|
-
*/
|
|
6232
|
-
const buildErroredNew = () => {
|
|
6233
|
-
const out = [];
|
|
6234
|
-
const addUnique = (m) => {
|
|
6235
|
-
if (m && !out.some((existing) => existing.id === m.id))
|
|
6236
|
-
out.push(m);
|
|
6237
|
-
};
|
|
6238
|
-
addUnique(erroredWireMessage);
|
|
6239
|
-
for (const m of (locals_js_1.locals.get(chatTurnNewUIMessagesKey) ?? [])) {
|
|
6240
|
-
addUnique(m);
|
|
6241
|
-
}
|
|
6242
|
-
if (includePartial)
|
|
6243
|
-
addUnique(partialResponse);
|
|
6244
|
-
return out;
|
|
6245
|
-
};
|
|
6246
|
-
let erroredNewUIMessages = buildErroredNew();
|
|
5672
|
+
let erroredNewUIMessages = erroredWireMessage ? [erroredWireMessage] : [];
|
|
5673
|
+
if (includePartial) {
|
|
5674
|
+
erroredNewUIMessages.push(partialResponse);
|
|
5675
|
+
}
|
|
6247
5676
|
let erroredNewModelMessages = [];
|
|
6248
|
-
const reconciledSteer = reconcilePendingSteer();
|
|
6249
5677
|
if (!responseCommitted) {
|
|
6250
5678
|
try {
|
|
6251
5679
|
if (erroredNewUIMessages.length > 0) {
|
|
6252
|
-
|
|
6253
|
-
* Built in order from the recorded forms rather than by
|
|
6254
|
-
* converting the UI list, so a steer appears in the delta as
|
|
6255
|
-
* the model received it (what `prepare` produced), matching the
|
|
6256
|
-
* lane. The wire message and partial are converted as before.
|
|
6257
|
-
*/
|
|
6258
|
-
const steerModelById = new Map(reconciledSteer.map((e) => [e.ui.id, e.model]));
|
|
6259
|
-
for (const m of erroredNewUIMessages) {
|
|
6260
|
-
const recorded = steerModelById.get(m.id);
|
|
6261
|
-
if (recorded)
|
|
6262
|
-
erroredNewModelMessages.push(...recorded);
|
|
6263
|
-
else
|
|
6264
|
-
erroredNewModelMessages.push(...(await toModelMessages([stripProviderMetadata(m)])));
|
|
6265
|
-
}
|
|
5680
|
+
erroredNewModelMessages = await toModelMessages(erroredNewUIMessages.map((m) => stripProviderMetadata(m)));
|
|
6266
5681
|
}
|
|
6267
5682
|
if (erroredUIMessagesWithPartial !== accumulatedUIMessages) {
|
|
6268
5683
|
if (partialIdx === -1) {
|
|
@@ -6270,11 +5685,7 @@ function chatAgent(options) {
|
|
|
6270
5685
|
accumulatedMessages.push(...(await toModelMessages(appended.map((m) => stripProviderMetadata(m)))));
|
|
6271
5686
|
}
|
|
6272
5687
|
else {
|
|
6273
|
-
|
|
6274
|
-
if (!ok) {
|
|
6275
|
-
v3_1.logger.warn("chat.agent: replaced partial not found at the model lane tail; reconverting the lane");
|
|
6276
|
-
accumulatedMessages = await toModelMessages(erroredUIMessagesWithPartial);
|
|
6277
|
-
}
|
|
5688
|
+
accumulatedMessages = await toModelMessages(erroredUIMessagesWithPartial);
|
|
6278
5689
|
}
|
|
6279
5690
|
accumulatedUIMessages = erroredUIMessagesWithPartial;
|
|
6280
5691
|
locals_js_1.locals.set(chatCurrentUIMessagesKey, accumulatedUIMessages);
|
|
@@ -6283,7 +5694,7 @@ function chatAgent(options) {
|
|
|
6283
5694
|
catch {
|
|
6284
5695
|
erroredNewModelMessages = [];
|
|
6285
5696
|
erroredUIMessagesWithPartial = erroredUIMessages;
|
|
6286
|
-
erroredNewUIMessages =
|
|
5697
|
+
erroredNewUIMessages = erroredWireMessage ? [erroredWireMessage] : [];
|
|
6287
5698
|
}
|
|
6288
5699
|
}
|
|
6289
5700
|
if (onTurnComplete) {
|
|
@@ -6353,15 +5764,8 @@ function chatAgent(options) {
|
|
|
6353
5764
|
});
|
|
6354
5765
|
}
|
|
6355
5766
|
}
|
|
6356
|
-
if (locals_js_1.locals.get(chatCloseRequestedKey)) {
|
|
6357
|
-
await performChatClose();
|
|
6358
|
-
return;
|
|
6359
|
-
}
|
|
6360
5767
|
// chat.requestUpgrade() / chat.endRun() — exit after error turn too
|
|
6361
5768
|
if (locals_js_1.locals.get(chatUpgradeRequestedKey) || locals_js_1.locals.get(chatEndRunRequestedKey)) {
|
|
6362
|
-
if (locals_js_1.locals.get(chatUpgradeRequestedKey)) {
|
|
6363
|
-
await persistUpgradeHandoff();
|
|
6364
|
-
}
|
|
6365
5769
|
return;
|
|
6366
5770
|
}
|
|
6367
5771
|
// Drain remaining recovered turns before idling — a thrown
|
|
@@ -6384,12 +5788,6 @@ function chatAgent(options) {
|
|
|
6384
5788
|
return; // Timed out — end run gracefully
|
|
6385
5789
|
}
|
|
6386
5790
|
currentWirePayload = next.output;
|
|
6387
|
-
// Same close check the success path makes. Without it a close
|
|
6388
|
-
// record that lands after a failed turn is consumed as if it were
|
|
6389
|
-
// a turn payload, and the loop runs on against a closed session.
|
|
6390
|
-
if (currentWirePayload.trigger === "close") {
|
|
6391
|
-
return;
|
|
6392
|
-
}
|
|
6393
5791
|
// Continue to next iteration of the for loop
|
|
6394
5792
|
}
|
|
6395
5793
|
finally {
|
|
@@ -6398,11 +5796,6 @@ function chatAgent(options) {
|
|
|
6398
5796
|
}
|
|
6399
5797
|
}
|
|
6400
5798
|
finally {
|
|
6401
|
-
// Safety net for a close requested on a path that exits without
|
|
6402
|
-
// reaching one of the loop's close checks (a turn timeout, an OOM
|
|
6403
|
-
// re-throw). `performChatClose` is idempotent, so the ordinary path
|
|
6404
|
-
// having already run it costs nothing here.
|
|
6405
|
-
await performChatClose();
|
|
6406
5799
|
// `stopSub` is registered post-preload so the close-during-preload
|
|
6407
5800
|
// early-return path may exit before it ever attached. Guard the
|
|
6408
5801
|
// cleanup so a missing subscription doesn't throw.
|
|
@@ -6740,22 +6133,15 @@ function isStopped() {
|
|
|
6740
6133
|
// Version upgrade
|
|
6741
6134
|
// ---------------------------------------------------------------------------
|
|
6742
6135
|
/**
|
|
6743
|
-
*
|
|
6744
|
-
*
|
|
6745
|
-
* The handover happens immediately and server-side: a successor run is created
|
|
6746
|
-
* and picks the conversation up from `session.in`. The transport keeps reading
|
|
6747
|
-
* the same session output, so no client action is needed and nothing waits for
|
|
6748
|
-
* the next message.
|
|
6749
|
-
*
|
|
6750
|
-
* Without a target the session's pin is cleared, so the successor lands on the
|
|
6751
|
-
* latest deployed version; with `externalDeploymentId` the session is re-pinned
|
|
6752
|
-
* to that deployment.
|
|
6136
|
+
* Request that the current run exits so the next message starts on the latest
|
|
6137
|
+
* deployed version (via the standard continuation mechanism).
|
|
6753
6138
|
*
|
|
6754
6139
|
* When called from `onTurnStart` or `onValidateMessages`, `run()` is skipped
|
|
6755
|
-
* entirely
|
|
6140
|
+
* entirely — the run exits immediately and the transport re-triggers the
|
|
6141
|
+
* same message on the new version.
|
|
6756
6142
|
*
|
|
6757
6143
|
* When called from `run()` or `chat.defer()`, the current turn completes
|
|
6758
|
-
* normally and the
|
|
6144
|
+
* normally and the run exits afterward instead of waiting for the next message.
|
|
6759
6145
|
*
|
|
6760
6146
|
* Call from `onTurnStart`, `onValidateMessages`, `onChatResume`, `run()`,
|
|
6761
6147
|
* or inside `chat.defer()`.
|
|
@@ -6775,37 +6161,8 @@ function isStopped() {
|
|
|
6775
6161
|
* });
|
|
6776
6162
|
* ```
|
|
6777
6163
|
*/
|
|
6778
|
-
function requestUpgrade(
|
|
6164
|
+
function requestUpgrade() {
|
|
6779
6165
|
locals_js_1.locals.set(chatUpgradeRequestedKey, true);
|
|
6780
|
-
// Without a target the handoff clears the session's pin; with one it re-pins to that deployment.
|
|
6781
|
-
const target = options?.externalDeploymentId?.trim();
|
|
6782
|
-
if (target)
|
|
6783
|
-
locals_js_1.locals.set(chatUpgradeExternalDeploymentIdKey, target);
|
|
6784
|
-
}
|
|
6785
|
-
/** @internal Requests a handoff when the session's pin no longer names this deployment. */
|
|
6786
|
-
async function followSessionPin(chatId, policy) {
|
|
6787
|
-
if (!chatId) {
|
|
6788
|
-
return;
|
|
6789
|
-
}
|
|
6790
|
-
const deployedExternalId = locals_js_1.locals.get(chatAgentRunContextKey)?.deployment?.externalId;
|
|
6791
|
-
if (policy !== "hold" && !deployedExternalId) {
|
|
6792
|
-
v3_1.logger.debug("chat.versionSkew: cannot follow the session pin", {
|
|
6793
|
-
chatId,
|
|
6794
|
-
reason: "the run context carries no deployment.externalId",
|
|
6795
|
-
});
|
|
6796
|
-
}
|
|
6797
|
-
const target = await (0, chatVersionSkew_js_1.resolvePinToFollow)({
|
|
6798
|
-
policy,
|
|
6799
|
-
deployedExternalId,
|
|
6800
|
-
upgradeAlreadyRequested: locals_js_1.locals.get(chatUpgradeRequestedKey) === true,
|
|
6801
|
-
readPin: async () => (await sessions_js_1.sessions.retrieve(chatId, { retry: { maxAttempts: 2, randomize: true } }))
|
|
6802
|
-
.triggerConfig,
|
|
6803
|
-
});
|
|
6804
|
-
if (!target) {
|
|
6805
|
-
return;
|
|
6806
|
-
}
|
|
6807
|
-
v3_1.logger.info("chat.versionSkew: following the session pin", { chatId, target });
|
|
6808
|
-
requestUpgrade({ externalDeploymentId: target });
|
|
6809
6166
|
}
|
|
6810
6167
|
/**
|
|
6811
6168
|
* Hand off the current custom agent Session to a fresh run.
|
|
@@ -6844,31 +6201,20 @@ async function endAndContinue() {
|
|
|
6844
6201
|
if ((locals_js_1.locals.get(chatActiveSessionIteratorsKey) ?? 0) > 0) {
|
|
6845
6202
|
throw new Error("chat.endAndContinue() cannot be called while a chat.createSession() iterator is active. Close the iterator, then call chat.endAndContinue().");
|
|
6846
6203
|
}
|
|
6847
|
-
await performEndAndContinue(
|
|
6204
|
+
await performEndAndContinue();
|
|
6848
6205
|
}
|
|
6849
6206
|
/** @internal Shared server handoff used by managed and custom agent loops. */
|
|
6850
|
-
async function performEndAndContinue(
|
|
6207
|
+
async function performEndAndContinue() {
|
|
6851
6208
|
const chatId = locals_js_1.locals.get(chatExternalIdKey);
|
|
6852
6209
|
const callingRunId = locals_js_1.locals.get(chatAgentRunContextKey)?.run.id;
|
|
6853
6210
|
if (!chatId || !callingRunId) {
|
|
6854
6211
|
throw new Error("Cannot end and continue without an active chat agent run");
|
|
6855
6212
|
}
|
|
6856
|
-
const externalDeploymentId = options.externalDeploymentId;
|
|
6857
6213
|
const apiClient = v3_1.apiClientManager.clientOrThrow();
|
|
6858
|
-
|
|
6214
|
+
await apiClient.endAndContinueSession(chatId, {
|
|
6859
6215
|
callingRunId,
|
|
6860
|
-
reason:
|
|
6861
|
-
...(externalDeploymentId ? { externalDeploymentId } : {}),
|
|
6216
|
+
reason: "upgrade",
|
|
6862
6217
|
});
|
|
6863
|
-
if (result?.pendingVersion !== true) {
|
|
6864
|
-
return;
|
|
6865
|
-
}
|
|
6866
|
-
// The successor parked. Say so on `.out` while this run still can — the transport's
|
|
6867
|
-
// subscription survives the swap, so the client learns without waiting for its next send.
|
|
6868
|
-
const [error] = await (0, v3_1.tryCatch)(getChatSession().out.writeControl(v3_1.TRIGGER_CONTROL_SUBTYPE.PENDING_VERSION));
|
|
6869
|
-
if (error) {
|
|
6870
|
-
v3_1.logger.warn("could not signal a parked handoff", { chatId, error });
|
|
6871
|
-
}
|
|
6872
6218
|
}
|
|
6873
6219
|
/**
|
|
6874
6220
|
* Exit the run after the current turn completes, without waiting for the
|
|
@@ -6899,124 +6245,6 @@ async function performEndAndContinue(options) {
|
|
|
6899
6245
|
function endRun() {
|
|
6900
6246
|
locals_js_1.locals.set(chatEndRunRequestedKey, true);
|
|
6901
6247
|
}
|
|
6902
|
-
/**
|
|
6903
|
-
* End the whole conversation, permanently. The session row is closed, further
|
|
6904
|
-
* appends are refused, and the run exits without scheduling a continuation.
|
|
6905
|
-
*
|
|
6906
|
-
* This is the session-level stop. {@link endRun} ends the current run and lets
|
|
6907
|
-
* the next message start a fresh one; `chat.close()` ends the session itself,
|
|
6908
|
-
* so there is no next message. Use it for a budget cap, a completed goal,
|
|
6909
|
-
* abuse detection, or a user signing out.
|
|
6910
|
-
*
|
|
6911
|
-
* In a `chat.agent`, call it from `run()`, `prepareStep`, or
|
|
6912
|
-
* `onBeforeTurnComplete`. Called mid-step, it aborts the in-flight
|
|
6913
|
-
* `streamText` the same way the stop signal does, so the partial response is
|
|
6914
|
-
* still captured and streamed. The turn then completes normally, a terminal
|
|
6915
|
-
* `session-closed` record carrying `reason` is written to the response stream,
|
|
6916
|
-
* and the loop exits.
|
|
6917
|
-
*
|
|
6918
|
-
* Prefer `onBeforeTurnComplete` over `onTurnComplete`. It carries the same
|
|
6919
|
-
* fields but still runs while the stream is open, so the closed state rides
|
|
6920
|
-
* out on the turn's final record. `onTurnComplete` runs after that record, so
|
|
6921
|
-
* a close decided there does not reach a reader that has already finished the
|
|
6922
|
-
* turn, and the user only finds out when their next message is refused.
|
|
6923
|
-
*
|
|
6924
|
-
* In a `chat.customAgent`, call it anywhere in your own loop. The close is
|
|
6925
|
-
* performed when `run()` returns, so it lands whether you break out of a
|
|
6926
|
-
* `chat.createSession` loop, return early, or hand-roll the loop entirely.
|
|
6927
|
-
*
|
|
6928
|
-
* Closing is one-way: a closed session cannot be reopened. Its transcript
|
|
6929
|
-
* stays readable.
|
|
6930
|
-
*
|
|
6931
|
-
* @example
|
|
6932
|
-
* ```ts
|
|
6933
|
-
* chat.agent({
|
|
6934
|
-
* id: "budgeted-agent",
|
|
6935
|
-
* onBeforeTurnComplete: async ({ usage }) => {
|
|
6936
|
-
* if (await overBudget(usage)) {
|
|
6937
|
-
* chat.close({ reason: "Monthly budget reached" });
|
|
6938
|
-
* }
|
|
6939
|
-
* },
|
|
6940
|
-
* });
|
|
6941
|
-
* ```
|
|
6942
|
-
*/
|
|
6943
|
-
function close(options) {
|
|
6944
|
-
if (!locals_js_1.locals.get(chatExternalIdKey)) {
|
|
6945
|
-
throw new Error("chat.close() can only be called from inside a chat.agent() or chat.customAgent() run");
|
|
6946
|
-
}
|
|
6947
|
-
// Bound the reason once, here. It goes out on S2 record headers as well as
|
|
6948
|
-
// the close API, and an oversized value would fail the turn-complete write
|
|
6949
|
-
// that carries the turn boundary, costing the client far more than the
|
|
6950
|
-
// reason text.
|
|
6951
|
-
// Trailing high surrogate: the cut landed between the two halves of an
|
|
6952
|
-
// astral character, and encoding the orphan to UTF-8 for a record header
|
|
6953
|
-
// yields a replacement character. Drop it rather than ship mojibake.
|
|
6954
|
-
const reason = options?.reason
|
|
6955
|
-
?.slice(0, CHAT_CLOSE_REASON_MAX_LENGTH)
|
|
6956
|
-
.replace(/[\uD800-\uDBFF]$/, "");
|
|
6957
|
-
locals_js_1.locals.set(chatCloseRequestedKey, reason ? { reason } : {});
|
|
6958
|
-
// Mid-step call: unblock the in-flight streamText exactly like the stop
|
|
6959
|
-
// signal, so the turn can reach its turn boundary instead of running the
|
|
6960
|
-
// model out to completion after the decision to close has been made.
|
|
6961
|
-
locals_js_1.locals.get(chatStopControllerKey)?.abort(reason ?? "closed");
|
|
6962
|
-
}
|
|
6963
|
-
/**
|
|
6964
|
-
* @internal Terminal close sequence, run once at whichever exit site observes
|
|
6965
|
-
* the close request. Writes the standalone `session-closed` record, then closes
|
|
6966
|
-
* the session row.
|
|
6967
|
-
*
|
|
6968
|
-
* The record lands after the turn's `turn-complete`, so a client reading that
|
|
6969
|
-
* turn's stream has already terminated on it and will not see this one. It is
|
|
6970
|
-
* there for a reconnect and for replay. What a live client reads is the
|
|
6971
|
-
* `session-closed` header stamped onto `turn-complete` itself by
|
|
6972
|
-
* `writeTurnCompleteChunk`, which fires whenever the close was decided before
|
|
6973
|
-
* the turn ended. A close decided from `onTurnComplete` is past that point, so
|
|
6974
|
-
* the client learns from the 409 on its next send.
|
|
6975
|
-
*/
|
|
6976
|
-
async function performChatClose() {
|
|
6977
|
-
const request = locals_js_1.locals.get(chatCloseRequestedKey);
|
|
6978
|
-
if (!request || locals_js_1.locals.get(chatClosePerformedKey))
|
|
6979
|
-
return;
|
|
6980
|
-
const reason = request.reason;
|
|
6981
|
-
// Two flags, not one. The record is a client-visible event and must not be
|
|
6982
|
-
// written twice, but the row close is the part that actually ends the
|
|
6983
|
-
// conversation: flagging it as done before it succeeds would let a transient
|
|
6984
|
-
// failure leave the session open with no later call willing to retry.
|
|
6985
|
-
if (!locals_js_1.locals.get(chatCloseRecordWrittenKey)) {
|
|
6986
|
-
locals_js_1.locals.set(chatCloseRecordWrittenKey, true);
|
|
6987
|
-
try {
|
|
6988
|
-
const session = getChatSession();
|
|
6989
|
-
await session.out.writeControl(v3_1.TRIGGER_CONTROL_SUBTYPE.SESSION_CLOSED, reason ? [[v3_1.SESSION_CLOSED_REASON_HEADER, reason]] : undefined);
|
|
6990
|
-
}
|
|
6991
|
-
catch (error) {
|
|
6992
|
-
v3_1.logger.warn("chat.close: failed to write the session-closed record", {
|
|
6993
|
-
error: error instanceof Error ? error.message : String(error),
|
|
6994
|
-
});
|
|
6995
|
-
}
|
|
6996
|
-
}
|
|
6997
|
-
const chatId = locals_js_1.locals.get(chatExternalIdKey);
|
|
6998
|
-
if (!chatId)
|
|
6999
|
-
return;
|
|
7000
|
-
try {
|
|
7001
|
-
await sessions_js_1.sessions.close(chatId, {
|
|
7002
|
-
...(reason ? { reason } : {}),
|
|
7003
|
-
...(locals_js_1.locals.get(chatAgentRunContextKey)?.run.id
|
|
7004
|
-
? { callingRunId: locals_js_1.locals.get(chatAgentRunContextKey).run.id }
|
|
7005
|
-
: {}),
|
|
7006
|
-
});
|
|
7007
|
-
locals_js_1.locals.set(chatClosePerformedKey, true);
|
|
7008
|
-
}
|
|
7009
|
-
catch (error) {
|
|
7010
|
-
// Deliberately NOT flagged as performed: the close API is idempotent, so a
|
|
7011
|
-
// later exit site on this run gets to retry it. Losing every retry to a
|
|
7012
|
-
// transient failure would leave the row open and the conversation alive.
|
|
7013
|
-
// Non-fatal either way — the run still exits.
|
|
7014
|
-
v3_1.logger.error("chat.close: failed to close the session", {
|
|
7015
|
-
chatId,
|
|
7016
|
-
error: error instanceof Error ? error.message : String(error),
|
|
7017
|
-
});
|
|
7018
|
-
}
|
|
7019
|
-
}
|
|
7020
6248
|
// ---------------------------------------------------------------------------
|
|
7021
6249
|
// Per-turn deferred work
|
|
7022
6250
|
// ---------------------------------------------------------------------------
|
|
@@ -7078,18 +6306,9 @@ function chatDefer(promiseOrFn) {
|
|
|
7078
6306
|
* ```
|
|
7079
6307
|
*/
|
|
7080
6308
|
function injectBackgroundContext(messages) {
|
|
7081
|
-
const
|
|
7082
|
-
|
|
7083
|
-
|
|
7084
|
-
const instructions = locals_js_1.locals.get(chatInjectedInstructionsKey) ?? [];
|
|
7085
|
-
instructions.push(...systemBlocks);
|
|
7086
|
-
locals_js_1.locals.set(chatInjectedInstructionsKey, instructions);
|
|
7087
|
-
}
|
|
7088
|
-
if (conversational.length > 0) {
|
|
7089
|
-
const queue = locals_js_1.locals.get(chatBackgroundQueueKey) ?? [];
|
|
7090
|
-
queue.push(...conversational);
|
|
7091
|
-
locals_js_1.locals.set(chatBackgroundQueueKey, queue);
|
|
7092
|
-
}
|
|
6309
|
+
const queue = locals_js_1.locals.get(chatBackgroundQueueKey) ?? [];
|
|
6310
|
+
queue.push(...messages);
|
|
6311
|
+
locals_js_1.locals.set(chatBackgroundQueueKey, queue);
|
|
7093
6312
|
}
|
|
7094
6313
|
// ---------------------------------------------------------------------------
|
|
7095
6314
|
// Aborted message cleanup
|
|
@@ -7502,12 +6721,10 @@ class ChatMessageAccumulator {
|
|
|
7502
6721
|
// a duplicate, mirroring the chat.agent accumulator.
|
|
7503
6722
|
const existingIdx = this.uiMessages.findIndex((m) => m.id === response.id);
|
|
7504
6723
|
if (existingIdx !== -1) {
|
|
7505
|
-
const previous = this.uiMessages[existingIdx];
|
|
7506
6724
|
this.uiMessages[existingIdx] = response;
|
|
7507
6725
|
try {
|
|
7508
|
-
|
|
7509
|
-
|
|
7510
|
-
}
|
|
6726
|
+
// Reconvert all model messages since we replaced rather than appended.
|
|
6727
|
+
this.modelMessages = await toModelMessages(this.uiMessages.map((m) => stripProviderMetadata(m)));
|
|
7511
6728
|
}
|
|
7512
6729
|
catch {
|
|
7513
6730
|
// Conversion failed — leave the existing model messages in place
|
|
@@ -7543,28 +6760,6 @@ class ChatMessageAccumulator {
|
|
|
7543
6760
|
const modelMsgs = await toModelMessages([message]);
|
|
7544
6761
|
this._steeringQueue.push({ uiMessage: message, modelMessages: modelMsgs });
|
|
7545
6762
|
}
|
|
7546
|
-
/**
|
|
7547
|
-
* Record the messages a steering drain consumed.
|
|
7548
|
-
*
|
|
7549
|
-
* The drain only puts them in this step's prompt, so without this they
|
|
7550
|
-
* shape one answer and then exist in neither lane: not in `uiMessages`,
|
|
7551
|
-
* which is what an app persists from, and not in `modelMessages`, which is
|
|
7552
|
-
* what every later turn sends.
|
|
7553
|
-
*
|
|
7554
|
-
* Both lanes are appended to. The model lane is never reconverted from the
|
|
7555
|
-
* UI lane, because `compactIfNeeded` replaces it with a summary and leaves
|
|
7556
|
-
* the UI lane whole: a reconversion would restore everything the summary
|
|
7557
|
-
* replaced.
|
|
7558
|
-
*/
|
|
7559
|
-
async absorbSteering(claimed, injected) {
|
|
7560
|
-
const fresh = claimed.filter((m) => !this.uiMessages.some((e) => e.id === m.id));
|
|
7561
|
-
if (fresh.length === 0)
|
|
7562
|
-
return;
|
|
7563
|
-
this.uiMessages.push(...fresh);
|
|
7564
|
-
// Record what the model received. Only when the whole batch is new is
|
|
7565
|
-
// `injected` known to describe exactly these messages.
|
|
7566
|
-
this.modelMessages.push(...(injected && fresh.length === claimed.length ? injected : await toModelMessages(fresh)));
|
|
7567
|
-
}
|
|
7568
6763
|
/**
|
|
7569
6764
|
* Get and clear unconsumed steering messages.
|
|
7570
6765
|
*/
|
|
@@ -7597,8 +6792,7 @@ class ChatMessageAccumulator {
|
|
|
7597
6792
|
}
|
|
7598
6793
|
// 2. Pending message injection
|
|
7599
6794
|
if (pm && queue.length > 0) {
|
|
7600
|
-
const
|
|
7601
|
-
await this.absorbSteering(claimed, injected);
|
|
6795
|
+
const injected = await drainSteeringQueue(pm, resultMessages ?? messages, steps, queue);
|
|
7602
6796
|
if (injected.length > 0) {
|
|
7603
6797
|
resultMessages = [...(resultMessages ?? messages), ...injected];
|
|
7604
6798
|
}
|
|
@@ -7787,7 +6981,7 @@ function trackActiveChatSessionIterator(iterator) {
|
|
|
7787
6981
|
* ```
|
|
7788
6982
|
*/
|
|
7789
6983
|
function createChatSession(payload, options) {
|
|
7790
|
-
const { signal: runSignal, idleTimeoutInSeconds: sessionIdleTimeoutOpt, timeout = "1h", maxTurns = 100, compaction: sessionCompaction, pendingMessages: sessionPendingMessages,
|
|
6984
|
+
const { signal: runSignal, idleTimeoutInSeconds: sessionIdleTimeoutOpt, timeout = "1h", maxTurns = 100, compaction: sessionCompaction, pendingMessages: sessionPendingMessages, } = options;
|
|
7791
6985
|
const idleTimeoutInSeconds = sessionIdleTimeoutOpt ?? 30;
|
|
7792
6986
|
return {
|
|
7793
6987
|
[Symbol.asyncIterator]() {
|
|
@@ -7876,16 +7070,8 @@ function createChatSession(payload, options) {
|
|
|
7876
7070
|
* without suspending.
|
|
7877
7071
|
*/
|
|
7878
7072
|
if (turn > 0) {
|
|
7879
|
-
if (locals_js_1.locals.get(chatCloseRequestedKey)) {
|
|
7880
|
-
await performChatClose();
|
|
7881
|
-
stop.cleanup();
|
|
7882
|
-
return { done: true, value: undefined };
|
|
7883
|
-
}
|
|
7884
7073
|
// chat.requestUpgrade() / chat.endRun() — exit before waiting
|
|
7885
7074
|
if (locals_js_1.locals.get(chatUpgradeRequestedKey) || locals_js_1.locals.get(chatEndRunRequestedKey)) {
|
|
7886
|
-
if (locals_js_1.locals.get(chatUpgradeRequestedKey)) {
|
|
7887
|
-
await persistUpgradeHandoff();
|
|
7888
|
-
}
|
|
7889
7075
|
stop.cleanup();
|
|
7890
7076
|
return { done: true, value: undefined };
|
|
7891
7077
|
}
|
|
@@ -7988,7 +7174,6 @@ function createChatSession(payload, options) {
|
|
|
7988
7174
|
}
|
|
7989
7175
|
accumulator.applyHandover(pendingHandoverSignal);
|
|
7990
7176
|
}
|
|
7991
|
-
await followSessionPin(currentPayload.chatId, sessionVersionSkew);
|
|
7992
7177
|
// chat.requestUpgrade() called before this turn — signal transport and exit
|
|
7993
7178
|
if (locals_js_1.locals.get(chatUpgradeRequestedKey)) {
|
|
7994
7179
|
await writeUpgradeRequiredChunk();
|
|
@@ -8205,8 +7390,7 @@ function createChatSession(payload, options) {
|
|
|
8205
7390
|
}
|
|
8206
7391
|
}
|
|
8207
7392
|
if (sessionPendingMessages) {
|
|
8208
|
-
const
|
|
8209
|
-
await accumulator.absorbSteering(claimed, injected);
|
|
7393
|
+
const injected = await drainSteeringQueue(sessionPendingMessages, resultMessages ?? stepMsgs, steps, turnSteeringQueue);
|
|
8210
7394
|
if (injected.length > 0) {
|
|
8211
7395
|
resultMessages = [...(resultMessages ?? stepMsgs), ...injected];
|
|
8212
7396
|
}
|
|
@@ -8220,11 +7404,6 @@ function createChatSession(payload, options) {
|
|
|
8220
7404
|
async return() {
|
|
8221
7405
|
activeMsgSub?.off();
|
|
8222
7406
|
activeMsgSub = undefined;
|
|
8223
|
-
// Reached when the consumer leaves the `for await` early (`break`,
|
|
8224
|
-
// `return`, a throw). A `chat.close()` from the loop body would
|
|
8225
|
-
// otherwise be dropped: the exit that performs it lives in `next()`,
|
|
8226
|
-
// and `next()` is never called again.
|
|
8227
|
-
await performChatClose();
|
|
8228
7407
|
// `stop` only exists once next() has booted the iterator.
|
|
8229
7408
|
stop?.cleanup();
|
|
8230
7409
|
return { done: true, value: undefined };
|
|
@@ -8481,11 +7660,6 @@ function createChatStartSessionAction(taskId, options) {
|
|
|
8481
7660
|
const maxAttempts = params.triggerConfig?.maxAttempts ?? options?.triggerConfig?.maxAttempts;
|
|
8482
7661
|
const maxDuration = params.triggerConfig?.maxDuration ?? options?.triggerConfig?.maxDuration;
|
|
8483
7662
|
const idleTimeoutInSeconds = params.triggerConfig?.idleTimeoutInSeconds ?? options?.triggerConfig?.idleTimeoutInSeconds;
|
|
8484
|
-
// Only `undefined` means "not supplied": a per-call `null` (opt out) has to beat a pinning
|
|
8485
|
-
// action default, which neither truthiness nor `??` would allow.
|
|
8486
|
-
const externalDeploymentId = params.triggerConfig?.externalDeploymentId !== undefined
|
|
8487
|
-
? params.triggerConfig.externalDeploymentId
|
|
8488
|
-
: options?.triggerConfig?.externalDeploymentId;
|
|
8489
7663
|
const triggerConfig = {
|
|
8490
7664
|
basePayload: {
|
|
8491
7665
|
messages: [],
|
|
@@ -8512,10 +7686,6 @@ function createChatStartSessionAction(taskId, options) {
|
|
|
8512
7686
|
lockToVersion: params.triggerConfig?.lockToVersion ?? options?.triggerConfig?.lockToVersion,
|
|
8513
7687
|
}
|
|
8514
7688
|
: {}),
|
|
8515
|
-
...(params.triggerConfig?.ttl !== undefined || options?.triggerConfig?.ttl !== undefined
|
|
8516
|
-
? { ttl: params.triggerConfig?.ttl ?? options?.triggerConfig?.ttl }
|
|
8517
|
-
: {}),
|
|
8518
|
-
...(externalDeploymentId !== undefined ? { externalDeploymentId } : {}),
|
|
8519
7689
|
...(idleTimeoutInSeconds !== undefined ? { idleTimeoutInSeconds } : {}),
|
|
8520
7690
|
};
|
|
8521
7691
|
const startBody = {
|
|
@@ -8560,7 +7730,6 @@ function createChatStartSessionAction(taskId, options) {
|
|
|
8560
7730
|
publicAccessToken,
|
|
8561
7731
|
runId: created.runId,
|
|
8562
7732
|
sessionId: created.id,
|
|
8563
|
-
...(created.pendingVersion ? { pendingVersion: true } : {}),
|
|
8564
7733
|
};
|
|
8565
7734
|
};
|
|
8566
7735
|
}
|
|
@@ -8594,8 +7763,7 @@ async function callSessionsCreateWithOverride(args) {
|
|
|
8594
7763
|
const init = {
|
|
8595
7764
|
method: "POST",
|
|
8596
7765
|
headers: overrideRequestHeaders(accessToken),
|
|
8597
|
-
|
|
8598
|
-
body: JSON.stringify((0, externalDeploymentId_js_1.withResolvedExternalDeploymentId)(args.body)),
|
|
7766
|
+
body: JSON.stringify(args.body),
|
|
8599
7767
|
};
|
|
8600
7768
|
const response = args.fetchOverride
|
|
8601
7769
|
? await args.fetchOverride(url, init, ctx)
|
|
@@ -8673,8 +7841,6 @@ exports.chat = {
|
|
|
8673
7841
|
createStartSessionAction: createChatStartSessionAction,
|
|
8674
7842
|
/** Pipe a stream to the chat transport. See {@link pipeChat}. */
|
|
8675
7843
|
pipe: pipeChat,
|
|
8676
|
-
/** Return from `onAction` to run a turn on the edited history. See {@link chatTurn}. */
|
|
8677
|
-
turn: chatTurn,
|
|
8678
7844
|
/** Create a per-run typed local. See {@link chatLocal}. */
|
|
8679
7845
|
local: chatLocal,
|
|
8680
7846
|
/** Create a public access token for a chat task. See {@link createChatAccessToken}. */
|
|
@@ -8695,8 +7861,6 @@ exports.chat = {
|
|
|
8695
7861
|
endAndContinue,
|
|
8696
7862
|
/** Exit the run after the current turn completes, without any upgrade signal. See {@link endRun}. */
|
|
8697
7863
|
endRun,
|
|
8698
|
-
/** End the conversation permanently: close the session and exit the run. See {@link close}. */
|
|
8699
|
-
close,
|
|
8700
7864
|
/** Clean up aborted parts from a UIMessage. See {@link cleanupAbortedParts}. */
|
|
8701
7865
|
cleanupAbortedParts,
|
|
8702
7866
|
/** Register background work that runs in parallel with streaming. See {@link chatDefer}. */
|
|
@@ -8844,16 +8008,6 @@ async function writeTurnCompleteChunk(_chatId, publicAccessToken) {
|
|
|
8844
8008
|
if (consumedCursor !== undefined) {
|
|
8845
8009
|
extraHeaders.push([v3_1.SESSION_IN_CONSUMED_ID_HEADER, String(consumedCursor)]);
|
|
8846
8010
|
}
|
|
8847
|
-
// A close decided before this turn ended rides out on turn-complete. Readers
|
|
8848
|
-
// terminate their stream on turn-complete, so a standalone record written
|
|
8849
|
-
// after it only reaches a reconnect — this header is what a live client sees.
|
|
8850
|
-
const pendingClose = locals_js_1.locals.get(chatCloseRequestedKey);
|
|
8851
|
-
if (pendingClose) {
|
|
8852
|
-
extraHeaders.push([v3_1.SESSION_CLOSED_HEADER, "true"]);
|
|
8853
|
-
if (pendingClose.reason) {
|
|
8854
|
-
extraHeaders.push([v3_1.SESSION_CLOSED_REASON_HEADER, pendingClose.reason]);
|
|
8855
|
-
}
|
|
8856
|
-
}
|
|
8857
8011
|
const result = await session.out.writeControl(v3_1.TRIGGER_CONTROL_SUBTYPE.TURN_COMPLETE, extraHeaders);
|
|
8858
8012
|
const T_N = result.lastEventId ? Number.parseInt(result.lastEventId, 10) : undefined;
|
|
8859
8013
|
// 2. Trim back to the previous turn-complete, if we have one. Skipping on
|
|
@@ -8911,47 +8065,12 @@ async function writeTurnCompleteChunk(_chatId, publicAccessToken) {
|
|
|
8911
8065
|
*
|
|
8912
8066
|
* @internal
|
|
8913
8067
|
*/
|
|
8914
|
-
/**
|
|
8915
|
-
* Persists an upgrade requested after the turn has already run.
|
|
8916
|
-
*
|
|
8917
|
-
* The pre-turn sites reach {@link performEndAndContinue} through
|
|
8918
|
-
* {@link writeUpgradeRequiredChunk}, which is what clears (or re-points) the
|
|
8919
|
-
* session's stored `externalDeploymentId`. The post-turn exits had no such path,
|
|
8920
|
-
* so a `chat.requestUpgrade()` from `run()` or `chat.defer()` left the pin intact
|
|
8921
|
-
* and every continuation re-pinned to the deployment the agent asked to leave.
|
|
8922
|
-
*
|
|
8923
|
-
* No `upgrade-required` chunk is written here: the turn already produced its
|
|
8924
|
-
* answer, so there is nothing for a client to be told about.
|
|
8925
|
-
*/
|
|
8926
|
-
async function persistUpgradeHandoff() {
|
|
8927
|
-
const chatId = locals_js_1.locals.get(chatExternalIdKey);
|
|
8928
|
-
const callingRunId = locals_js_1.locals.get(chatAgentRunContextKey)?.run.id;
|
|
8929
|
-
if (!chatId || !callingRunId) {
|
|
8930
|
-
return;
|
|
8931
|
-
}
|
|
8932
|
-
try {
|
|
8933
|
-
await performEndAndContinue({
|
|
8934
|
-
reason: "upgrade",
|
|
8935
|
-
externalDeploymentId: locals_js_1.locals.get(chatUpgradeExternalDeploymentIdKey),
|
|
8936
|
-
});
|
|
8937
|
-
}
|
|
8938
|
-
catch (error) {
|
|
8939
|
-
v3_1.logger.warn("upgrade handoff failed; session keeps its current version pin", {
|
|
8940
|
-
chatId,
|
|
8941
|
-
callingRunId,
|
|
8942
|
-
error,
|
|
8943
|
-
});
|
|
8944
|
-
}
|
|
8945
|
-
}
|
|
8946
8068
|
async function writeUpgradeRequiredChunk() {
|
|
8947
8069
|
const chatId = locals_js_1.locals.get(chatExternalIdKey);
|
|
8948
8070
|
const callingRunId = locals_js_1.locals.get(chatAgentRunContextKey)?.run.id;
|
|
8949
8071
|
if (chatId && callingRunId) {
|
|
8950
8072
|
try {
|
|
8951
|
-
await performEndAndContinue(
|
|
8952
|
-
reason: "upgrade",
|
|
8953
|
-
externalDeploymentId: locals_js_1.locals.get(chatUpgradeExternalDeploymentIdKey),
|
|
8954
|
-
});
|
|
8073
|
+
await performEndAndContinue();
|
|
8955
8074
|
}
|
|
8956
8075
|
catch (error) {
|
|
8957
8076
|
// Non-fatal: the next `.in/append` re-triggers via the probe.
|