-
-
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
24
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.
Open
Changes from all commits
Commits
Show all changes
24 commits
Select commit
Hold shift + click to select a range
ac1635e
feat(sdk,core): close resumed chat streams promptly when caught up
ericallam 5343ec5
fix(webapp): stop the dev server sending duplicate CORS headers
ericallam 9e41506
fix(core): end caught-up tracking on all terminal stream paths
ericallam 43c237b
fix(core,sdk): settle resumed chat streams correctly on caught-up
ericallam 0cc305e
docs(ai-chat): document tail-on-ping and the client-side caught-up close
ericallam 1efb4b0
test(webapp): full-stack session-stream e2e over s2-lite
ericallam f7b0918
test(webapp): browser-level cross-origin session-stream e2e
ericallam 3fce5f8
test(webapp): make the browser session-stream leg CI-robust
ericallam 14edc3d
test(webapp): cover more session-stream scenarios
ericallam f36bbec
test(webapp): full-stack chat.agent e2e with the real turn loop
ericallam 099211e
test(webapp): agent e2e legs for validate rejection, action, and stop
ericallam 0843a20
test(webapp): agent e2e legs for tools, HITL, and snapshot restore
ericallam 3c1dc85
test(webapp): agent e2e legs for regenerate, tool approval, and sessi…
ericallam da78cd8
test(core,webapp): in-process waitpoint backend for run-lifecycle e2e
ericallam eff6fc6
test(webapp): suspend/resume, upgrade deferral, and multi-hop continu…
ericallam 2d9eff9
test(core,webapp): suspend-hook, OOM-retry, and idle-timeout e2e legs
ericallam 0dbeaa7
fix(sdk): recover the in-flight message on a preloaded chat.agent retry
ericallam 7d09766
fix(core): stop reconnecting a session stream after the consumer cancels
ericallam 3ace522
ci(e2e-webapp): pre-pull the MinIO image for the session-stream e2e
ericallam c23fcef
test(webapp): make the session-stream e2e reliable in CI
ericallam 406d7f2
test(core,webapp): terminate in-process agent e2e runs reliably
ericallam 4c1de6f
test(webapp): drain the session .out collector across reconnects
ericallam 614859f
fix(webapp): keep session .out writable when S2 access tokens are ski…
ericallam cab3b03
fix(sdk): request the caught-up settle on AgentChat.reconnect()
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 applies to the server-to-server `AgentChat` client's `reconnect()` too, not just the browser transport. |
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.