-
-
Notifications
You must be signed in to change notification settings - Fork 1.4k
feat(sdk,core): close resumed chat streams promptly when caught up #4349
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
Open
ericallam
wants to merge
23
commits into
main
Choose a base branch
from
feat/s2-caught-up-reads
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+3,804
−49
Open
Changes from all commits
Commits
Show all changes
23 commits
Select commit
Hold shift + click to select a range
30b27b1
feat(sdk,core): close resumed chat streams promptly when caught up
ericallam 4fe81b9
fix(webapp): stop the dev server sending duplicate CORS headers
ericallam 77af1c2
fix(core): end caught-up tracking on all terminal stream paths
ericallam 2ca215b
fix(core,sdk): settle resumed chat streams correctly on caught-up
ericallam 8e20deb
docs(ai-chat): document tail-on-ping and the client-side caught-up close
ericallam 98f3f0f
test(webapp): full-stack session-stream e2e over s2-lite
ericallam 75c7990
test(webapp): browser-level cross-origin session-stream e2e
ericallam 9fb2a63
test(webapp): make the browser session-stream leg CI-robust
ericallam a9b53f6
test(webapp): cover more session-stream scenarios
ericallam 40f9640
test(webapp): full-stack chat.agent e2e with the real turn loop
ericallam 3ca2ad9
test(webapp): agent e2e legs for validate rejection, action, and stop
ericallam 659e769
test(webapp): agent e2e legs for tools, HITL, and snapshot restore
ericallam 3a69495
test(webapp): agent e2e legs for regenerate, tool approval, and sessi…
ericallam fb8367b
test(core,webapp): in-process waitpoint backend for run-lifecycle e2e
ericallam 5176b31
test(webapp): suspend/resume, upgrade deferral, and multi-hop continu…
ericallam 661ecd7
test(core,webapp): suspend-hook, OOM-retry, and idle-timeout e2e legs
ericallam 209e49b
fix(sdk): recover the in-flight message on a preloaded chat.agent retry
ericallam 19edf68
fix(core): stop reconnecting a session stream after the consumer cancels
ericallam 7055f22
ci(e2e-webapp): pre-pull the MinIO image for the session-stream e2e
ericallam e2f8bf0
test(webapp): make the session-stream e2e reliable in CI
ericallam de4c3c7
test(core,webapp): terminate in-process agent e2e runs reliably
ericallam 243289c
test(webapp): drain the session .out collector across reconnects
ericallam f202ccc
fix(webapp): keep session .out writable when S2 access tokens are ski…
ericallam File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| --- | ||
| "@trigger.dev/sdk": patch | ||
| --- | ||
|
|
||
| Fix a preloaded `chat.agent` run dropping an in-flight message when it retries after an out-of-memory error. The message being processed when the run hit the OOM is now recovered and re-run on the retry, instead of being skipped while the run waited for a new message. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| --- | ||
| "@trigger.dev/core": patch | ||
| "@trigger.dev/sdk": patch | ||
| --- | ||
|
|
||
| Chat sessions now close a resumed stream as soon as it has caught up to the latest output, instead of holding the connection open for the full long-poll window. Reloading or reconnecting to an idle chat settles faster. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,197 @@ | ||
| import { apiClientManager, resourceCatalog } from "@trigger.dev/core/v3"; | ||
| import type { LocalsKey } from "@trigger.dev/core/v3"; | ||
| import type { LanguageModel } from "ai"; | ||
| import { | ||
| installSessionWaitpointBackend, | ||
| runInMockTaskContext, | ||
| StandardSessionStreamManager, | ||
| } from "@trigger.dev/core/v3/test"; | ||
|
|
||
| export type RunRealChatAgentOptions = { | ||
| agentId: string; | ||
| baseUrl: string; | ||
| addressingKey: string; | ||
| /** | ||
| * The environment secret key. The agent writes `.out` and reads `.in` as the | ||
| * backend (PRIVATE auth) — the `.out` channel rejects client session tokens. | ||
| */ | ||
| secretKey: string; | ||
| model: LanguageModel; | ||
| modelLocal: LocalsKey<LanguageModel>; | ||
| runId?: string; | ||
| /** | ||
| * Boot as a continuation of a previous run for the same session. Gates the | ||
| * snapshot + `.out`/`.in` replay boot path, so the agent restores prior | ||
| * history instead of treating the chat as brand new. | ||
| */ | ||
| continuation?: boolean; | ||
| previousRunId?: string; | ||
| /** | ||
| * Idle window (seconds) before the turn loop falls through from the SSE | ||
| * once() to the suspending `session.in.wait()`. Set this low to force the | ||
| * suspend/resume path in a test. | ||
| */ | ||
| idleTimeoutInSeconds?: number; | ||
| /** | ||
| * `ctx.attempt.number`. A value greater than 1 makes the boot treat the run | ||
| * as a retry (`couldHavePriorState`), restoring from the snapshot + `.in` | ||
| * replay. Used to model an OOM retry re-dispatch. | ||
| */ | ||
| attemptNumber?: number; | ||
| }; | ||
|
|
||
| export type RunningAgent = { | ||
| done: Promise<void>; | ||
| close: () => Promise<void>; | ||
| }; | ||
|
|
||
| /** | ||
| * Run the real `chat.agent` turn loop in-process, wired to a running webapp: | ||
| * `apiClientManager` + a real `StandardSessionStreamManager` point the agent's | ||
| * `.in`/`.out` at the webapp's Session streams (real S2 + SSE), the model is | ||
| * injected via locals (so it survives without serialization), and turns are | ||
| * driven by appending to `.in` over HTTP. Callers keep each message inside the | ||
| * idle window and `close()` promptly so the run-engine suspend path is never | ||
| * reached. | ||
| */ | ||
| export function runRealChatAgent(opts: RunRealChatAgentOptions): RunningAgent { | ||
| apiClientManager.setGlobalAPIClientConfiguration({ | ||
| baseURL: opts.baseUrl, | ||
| accessToken: opts.secretKey, | ||
| }); | ||
| const apiClient = apiClientManager.clientOrThrow(); | ||
| const manager = new StandardSessionStreamManager(apiClient, opts.baseUrl); | ||
| const { backend, runtimeManager, restore } = installSessionWaitpointBackend(apiClient); | ||
|
|
||
| const taskEntry = resourceCatalog.getTask(opts.agentId); | ||
| if (!taskEntry) { | ||
| restore(); | ||
| throw new Error(`runRealChatAgent: agent "${opts.agentId}" is not registered`); | ||
| } | ||
| const runFn = taskEntry.fns.run as ( | ||
| payload: unknown, | ||
| params: { ctx: unknown; signal: AbortSignal } | ||
| ) => Promise<unknown>; | ||
|
|
||
| const runSignal = new AbortController(); | ||
| runSignal.signal.addEventListener("abort", () => { | ||
| try { | ||
| backend.disable(); | ||
| } catch {} | ||
| }); | ||
| const runId = opts.runId ?? `run_${opts.addressingKey}`; | ||
|
|
||
| const idle = | ||
| opts.idleTimeoutInSeconds !== undefined | ||
| ? { idleTimeoutInSeconds: opts.idleTimeoutInSeconds } | ||
| : {}; | ||
|
|
||
| const done = ( | ||
| runInMockTaskContext( | ||
| async (drivers) => { | ||
| drivers.locals.set(opts.modelLocal, opts.model); | ||
| const payload = opts.continuation | ||
| ? { | ||
| chatId: opts.addressingKey, | ||
| continuation: true, | ||
| metadata: {}, | ||
| ...idle, | ||
| ...(opts.previousRunId ? { previousRunId: opts.previousRunId } : {}), | ||
| } | ||
| : { chatId: opts.addressingKey, trigger: "preload", metadata: {}, ...idle }; | ||
| await runFn(payload, { ctx: drivers.ctx, signal: runSignal.signal }); | ||
| }, | ||
| { | ||
| ctx: { | ||
| run: { id: runId }, | ||
| ...(opts.attemptNumber !== undefined ? { attempt: { number: opts.attemptNumber } } : {}), | ||
| }, | ||
| sessionStreamManager: manager, | ||
| runtimeManager, | ||
| } | ||
| ) as Promise<void> | ||
| ).finally(restore); | ||
|
|
||
| return { | ||
| done, | ||
| close: async () => { | ||
| try { | ||
| await fetch( | ||
| `${opts.baseUrl}/realtime/v1/sessions/${encodeURIComponent(opts.addressingKey)}/in/append`, | ||
| { | ||
| method: "POST", | ||
| headers: { | ||
| Authorization: `Bearer ${opts.secretKey}`, | ||
| "Content-Type": "application/json", | ||
| "X-Part-Id": "close", | ||
| }, | ||
| body: JSON.stringify({ | ||
| kind: "message", | ||
| payload: { chatId: opts.addressingKey, trigger: "close" }, | ||
| }), | ||
| } | ||
| ); | ||
| } catch {} | ||
| runSignal.abort(); | ||
| await done.catch(() => {}); | ||
| }, | ||
| }; | ||
| } | ||
|
|
||
| export type ChatAgentSessionOptions = Omit< | ||
| RunRealChatAgentOptions, | ||
| "runId" | "continuation" | "previousRunId" | ||
| >; | ||
|
|
||
| export type ChatAgentSession = { | ||
| /** How many runs the session has spawned so far (1 fresh + N continuations). */ | ||
| runCount: () => number; | ||
| /** Close the currently-active run and stop spawning continuations. */ | ||
| close: () => Promise<void>; | ||
| }; | ||
|
|
||
| /** | ||
| * A session-scoped orchestrator that stands in for the run-engine's run | ||
| * lifecycle: it starts a run, and whenever that run exits on its own | ||
| * (`chat.endRun()` / `chat.requestUpgrade()`), spawns the next run as a | ||
| * continuation (new run id, `continuation: true`, `previousRunId` threaded) | ||
| * for the same session. That mirrors the server triggering a fresh run on the | ||
| * next append after the previous run went terminal, and lets each continuation | ||
| * restore prior history from the persisted snapshot. Runs never overlap: the | ||
| * next spawn is chained on the previous run's `done` (after its manager | ||
| * teardown), so the process-global managers are never installed twice at once. | ||
| */ | ||
| export function runChatAgentSession(opts: ChatAgentSessionOptions): ChatAgentSession { | ||
| let closed = false; | ||
| let index = 0; | ||
| let current: RunningAgent | undefined; | ||
| let previousRunId: string | undefined; | ||
|
|
||
| const spawn = () => { | ||
| index += 1; | ||
| const runId = `run_${opts.addressingKey}_${index}`; | ||
| current = runRealChatAgent({ | ||
| ...opts, | ||
| runId, | ||
| continuation: index > 1, | ||
| previousRunId, | ||
| }); | ||
| previousRunId = runId; | ||
| const settle = () => { | ||
| if (!closed) { | ||
| spawn(); | ||
| } | ||
| }; | ||
| current.done.then(settle, settle); | ||
| }; | ||
|
|
||
| spawn(); | ||
|
|
||
| return { | ||
| runCount: () => index, | ||
| close: async () => { | ||
| closed = true; | ||
| await current?.close(); | ||
| }, | ||
| }; | ||
| } |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.