-
-
Notifications
You must be signed in to change notification settings - Fork 1.4k
feat(chat): expose endAndContinue to custom agents #4647
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 2 commits
fcefcf5
da59647
03b84d1
e1a416e
adf37ab
b729865
5839cc3
31810b7
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| --- | ||
| "@trigger.dev/sdk": patch | ||
| --- | ||
|
|
||
| Allow custom chat agents to hand a conversation off to a fresh run on the latest deployed task version while preserving unconsumed Session input. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2538,6 +2538,8 @@ const chatOnCompactedKey = | |
| locals.create<(event: CompactedEvent) => Promise<void> | void>("chat.onCompacted"); | ||
| /** @internal Full task `ctx` for the active `chat.agent` run (for hooks invoked from nested compaction). */ | ||
| const chatAgentRunContextKey = locals.create<TaskRunContext>("chat.agentRunContext"); | ||
| /** @internal Marks the root run created by `chat.customAgent()`. */ | ||
| const chatCustomAgentRunKey = locals.create<boolean>("chat.customAgentRun"); | ||
| const chatPrepareMessagesKey = | ||
| locals.create<(event: PrepareMessagesEvent<unknown>) => ModelMessage[] | Promise<ModelMessage[]>>( | ||
| "chat.prepareMessages" | ||
|
|
@@ -5362,6 +5364,7 @@ function chatCustomAgent< | |
| locals.set(chatSessionHandleKey, sessions.open(payload.chatId)); | ||
| locals.set(chatExternalIdKey, payload.chatId); | ||
| locals.set(chatAgentRunContextKey, runOptions.ctx); | ||
| locals.set(chatCustomAgentRunKey, true); | ||
| // Initialize the turn-complete trim slot so `chat.writeTurnComplete` | ||
| // trims `session.out` back to the previous turn boundary. Without | ||
| // this the slot is undefined and the trim never runs, so `.out` | ||
|
|
@@ -5456,6 +5459,7 @@ function chatAgent< | |
| { signal: runSignal, ctx } | ||
| ) => { | ||
| locals.set(chatAgentRunContextKey, ctx); | ||
| locals.set(chatCustomAgentRunKey, false); | ||
|
|
||
| // On AI SDK 7, register the `@ai-sdk/otel` integration (once per process) | ||
| // so `experimental_telemetry` spans flow into the run trace. Awaited here | ||
|
|
@@ -8705,6 +8709,59 @@ function requestUpgrade(): void { | |
| locals.set(chatUpgradeRequestedKey, true); | ||
| } | ||
|
|
||
| /** | ||
| * Hand off the current custom agent Session to a fresh run. | ||
| * | ||
| * This is the low-level handoff for a fully hand-rolled | ||
| * `chat.customAgent()` loop. (Use {@link requestUpgrade} with | ||
| * `chat.createSession()` instead.) Call only between turns and after detaching | ||
| * input listeners for the old run. If the old run completed its current turn, | ||
| * persist its state and call {@link chatWriteTurnComplete} before handing off. | ||
| * Do not write a new turn boundary after receiving input that the continuation | ||
| * run should process: the boundary acknowledges the latest dispatched input. | ||
| * | ||
| * The server starts the continuation run but does not stop this run, so return | ||
| * from the task immediately after awaiting this function. The promise rejects | ||
| * if the server cannot complete the handoff. | ||
| * | ||
| * Pending Session input that the old run has not consumed remains on the | ||
| * durable `.in` stream and is delivered to the continuation run. | ||
| * | ||
| * @example | ||
| * ```ts | ||
| * messageSubscription.off(); | ||
| * await persistMessages(); | ||
| * await chat.writeTurnComplete(); | ||
| * await chat.endAndContinue(); | ||
| * return; | ||
| * ``` | ||
| */ | ||
| async function endAndContinue(): Promise<void> { | ||
| if (locals.get(chatCustomAgentRunKey) !== true) { | ||
| throw new Error( | ||
| "chat.endAndContinue() can only be called from inside a chat.customAgent() run" | ||
| ); | ||
| } | ||
|
|
||
| await performEndAndContinue(); | ||
| } | ||
|
|
||
| /** @internal Shared server handoff used by managed and custom agent loops. */ | ||
| async function performEndAndContinue(): Promise<void> { | ||
| const chatId = locals.get(chatExternalIdKey); | ||
| const callingRunId = locals.get(chatAgentRunContextKey)?.run.id; | ||
|
|
||
| if (!chatId || !callingRunId) { | ||
| throw new Error("Cannot end and continue without an active chat agent run"); | ||
| } | ||
|
|
||
| const apiClient = apiClientManager.clientOrThrow(); | ||
| await apiClient.endAndContinueSession(chatId, { | ||
| callingRunId, | ||
| reason: "upgrade", | ||
| }); | ||
| } | ||
|
|
||
| /** | ||
| * Exit the run after the current turn completes, without waiting for the | ||
| * next message. Unlike {@link requestUpgrade}, no upgrade-required signal | ||
|
|
@@ -10697,6 +10754,8 @@ export const chat = { | |
| isStopped, | ||
| /** Request that the run exits after the current turn so the next message starts on the latest version. See {@link requestUpgrade}. */ | ||
| requestUpgrade, | ||
| /** Hand off a custom agent Session to a fresh run. See {@link endAndContinue}. */ | ||
| endAndContinue, | ||
| /** Exit the run after the current turn completes, without any upgrade signal. See {@link endRun}. */ | ||
| endRun, | ||
| /** Clean up aborted parts from a UIMessage. See {@link cleanupAbortedParts}. */ | ||
|
|
@@ -10891,17 +10950,12 @@ async function writeTurnCompleteChunk( | |
| * @internal | ||
| */ | ||
| async function writeUpgradeRequiredChunk(): Promise<StreamWriteResult> { | ||
| const ctx = taskContext.ctx; | ||
| const chatId = ctx?.run.id ? getChatIdFromContext() : undefined; | ||
| const callingRunId = ctx?.run.id; | ||
| const chatId = locals.get(chatExternalIdKey); | ||
| const callingRunId = locals.get(chatAgentRunContextKey)?.run.id; | ||
|
Comment on lines
11056
to
+11058
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 📝 Info: chatId/callingRunId resolution swap is behaviour-preserving
Was this helpful? React with 👍 or 👎 to provide feedback. |
||
|
|
||
| if (chatId && callingRunId) { | ||
| const apiClient = apiClientManager.clientOrThrow(); | ||
| try { | ||
| await apiClient.endAndContinueSession(chatId, { | ||
| callingRunId, | ||
| reason: "upgrade", | ||
| }); | ||
| await performEndAndContinue(); | ||
| } catch (error) { | ||
| // Non-fatal: the next `.in/append` re-triggers via the probe. | ||
| // Swallow rather than throw so we still emit the chunk + exit. | ||
|
|
@@ -10917,17 +10971,6 @@ async function writeUpgradeRequiredChunk(): Promise<StreamWriteResult> { | |
| return session.out.writeControl(TRIGGER_CONTROL_SUBTYPE.UPGRADE_REQUIRED); | ||
| } | ||
|
|
||
| /** | ||
| * Resolves the current chat's `chatId` (used as session externalId) from | ||
| * the bound session handle. Returns `undefined` if no agent is bound — | ||
| * shouldn't happen at the call sites that invoke | ||
| * `writeUpgradeRequiredChunk`, but defensive against misuse. | ||
| * @internal | ||
| */ | ||
| function getChatIdFromContext(): string | undefined { | ||
| return locals.get(chatSessionHandleKey)?.id; | ||
| } | ||
|
|
||
| /** | ||
| * Extracts the text content of the last user message from a UIMessage array. | ||
| * Returns undefined if no user message is found. | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.