@tanstack/ai
Version:
Type-safe TypeScript AI SDK for streaming chat, tool calling, agents, structured outputs, and multimodal generation.
63 lines (62 loc) • 3.42 kB
TypeScript
/**
* The one-way channel from the durable DELIVERY sink back to the chat run: "your
* viewer is gone, but you are not."
*
* This is the MIRROR of `./delivery-detach.ts`. That module answers a question the
* sink asks the run (was this abort a detach?); this one delivers a fact the sink
* learns first and the run cannot observe at all — the response body was
* cancelled. Both are keyed weakly by the exact stream object `chat()` returned,
* for the same reason: passing that object to
* `toServerSentEventsResponse(stream, { durability })` is already mandatory, so it
* is the one seam an application cannot forget to wire.
*
* WHY THE RUN CANNOT JUST WATCH AN ABORT SIGNAL. Before this channel existed, the
* only way a disconnect reached a run's middleware was for the application to
* mirror `request.signal` into `chat()`'s `abortController` — which ABORTS THE RUN.
* For a durable run that is precisely wrong, and wrong in the most expensive
* direction: `chat()` returns at its `isCancelled()` check immediately after
* middleware setup, so the harness adapter's `chatStream` is never called and the
* agent in the freshly-created sandbox is NEVER LAUNCHED. The user re-attaches to
* an empty log belonging to a run that did nothing, and no takeover can recover it
* because there is no journal to replay. The application was forced to choose
* between "the middleware learns about the disconnect" and "the run survives it".
*
* So the disconnect travels as a NOTIFICATION rather than a cancellation. The run
* hands it to whatever subscribed through `RunDisconnectCapability` — bookkeeping
* only, no teardown — and keeps producing into its still-open durable log, which
* is exactly what a re-attaching client tails to catch up.
*
* DISPATCHED FROM THE TRANSPORT, NOT FROM THE RUN'S UNWINDING. That is what makes
* it prompt. A run suspended inside a minutes-wide `setup` (cloning a repo into a
* sandbox) cannot dispatch anything from its own `finally`, because the `finally`
* is reached only once the generator unwinds — which is what made `detachedSince`
* land three minutes late instead of immediately.
*/
/**
* Publish `stream`'s disconnect handler. Called by `chat()` on the object it hands
* back, once per stream.
*
* Wired on BOTH streaming paths — `runStreamingText` and
* `runStreamingStructuredOutput` — since either can be handed to a durable
* transport helper, matching `publishRunDetachedSignal`.
*
* @internal
*/
export declare function publishRunDisconnectHandler(stream: object, onDisconnect: () => void): void;
/**
* Tell the run behind `stream` that its delivery socket closed.
*
* A NO-OP for anything that never published a handler — a hand-rolled iterable, a
* `chat()` from an older build, a non-object source — so an unknown stream simply
* keeps today's behavior.
*
* NEVER THROWS and never returns a promise to await. This is called from
* `ReadableStream.cancel()`, whose rejection would surface as an unhandled error
* on a path where the consumer has already gone away; and the handler's own work
* is tracked by the run (which awaits it before finishing), not by this call. A
* throwing handler is swallowed for the same reason `wasRunDetached` swallows: the
* disconnect path has no second channel to report on.
*
* @internal
*/
export declare function notifyRunDisconnected(stream: unknown): void;