Repository navigation
feat(agent): stream watchdog core and typed stream errors (DEV-723 1/3) - #136
Draft
LukasParke wants to merge 2 commits into
Draft
LukasParke wants to merge 2 commits into
LukasParke wants to merge 2 commits into
Conversation
Port of OpenRouterTeam/typescript-sdk#770 and #774. - stream-errors.ts: StreamStalledError (phase, timeoutMs, elapsedMs, receivedAnyContent, retryable) and StreamFailedError (kind, code, errorType, response, retryable). - stream-watchdog.ts: applyStreamWatchdog / applyResponsesStreamWatchdog with firstContentMs and contentIntervalMs deadlines, classified by parsed event type. Differs from the SDK version per its review threads: `error` events are neutral rather than terminal (an error followed by silence must still stall), `response.fusion_call.analysis.completed` counts as content, and the request's abort signal disarms the watchdog so a user abort is never reported as a stall. - response.failed, stream-level error events and truncated streams throw StreamFailedError on every consume path: consumeStreamForCompletion, extractCompletionFromBuffer, the initial-stream capture and the follow-up pipe. Error events are recorded and surfaced only when the stream ends without completing. - No-tools stream getters re-surface initial-stream failures after their consumer ends, instead of ending silently on a failed or truncated response. Not wired into callModel yet: that lands with the `timeout` option. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- Split applyStreamWatchdog into a small StreamWatchdog state class (cc 17 -> under the max_cc 15 ceiling). No behavior change. - Route ReusableReadableStream and StreamFailedError through a stream-runtime.ts re-export façade, following the async-tools.ts pattern. model-result.ts stays at 15 outbound edges, the gate's no_god_files ceiling. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This branch has not been deployed
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
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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.
Stack 1/3: stream watchdog core and typed stream errors
Linear: DEV-723. This ports the stall-watchdog work from typescript-sdk's legacy
callModelinto the agent engine, which is the source of truth.Stack: this PR (ports OpenRouterTeam/typescript-sdk#770 and OpenRouterTeam/typescript-sdk#774) → #137
streamTimeoutoption (ports OpenRouterTeam/typescript-sdk#773) → #138 stall retries (ports OpenRouterTeam/typescript-sdk#775). OpenRouterTeam/typescript-sdk#776 (public raw-stream helpers) is deliberately not ported. It belongs in the SDK.What this PR changes
stream-errors.tsStreamStalledErrorcarriesphase(first_content|between_content),timeoutMs,elapsedMsandreceivedAnyContent. Itsretryablegetter is true only when no content had arrived.StreamFailedErrorcarrieskind(response_failed|error_event|truncated),code,errorType,responseand aretryablehint.retryableis true forserver_error,server,timeout,rate_limit_exceeded,provider_overloadedandprovider_unavailable.kindis new compared with the SDK version. It replaces the bareError('Stream ended without completion event')with a typed error that callers can still tell apart.stream-watchdog.tsapplyStreamWatchdog/applyResponsesStreamWatchdogenforce two deadlines on the parsed event stream:firstContentMs: from stream start to the first content-bearing event.contentIntervalMs: the maximum gap between content events.callModeluses it yet. PR 2 wires it in.errorevent is neutral, not terminal. Devin and Perry both flagged that a terminalerrordisarms the watchdog. A stream that sendserrorand then goes quiet would hang forever, even though both consume paths keep waiting after anerrorevent.response.fusion_call.analysis.completedcounts as content. Perry flagged that otherwise a fusion stream with only an analysis result would raise a false stall.signal. An abort disarms the watchdog, so a user abort ortimeoutMsis never reported as a stall.The same errors on every consume path (from OpenRouterTeam/typescript-sdk#774)
StreamFailedError:consumeStreamForCompletionextractCompletionFromBuffercaptureInitialStreamEvent)pipeAndConsumeStream)errorevent is recorded rather than thrown. It surfaces only when the stream ends without completing. Anerrorfollowed byresponse.completedstill resolves.pipeAndConsumeStreamthrew onresponse.failedwith a meaningless message, andconsumeStreamForCompletionignorederrorevents entirely.Stream getters surface failures
getTextStream,getFullResponsesStream,getItemsStream,getReasoningStream,getToolStreamandgetToolCallsStreamiterate the initial stream directly. They used to end silently on a failed or truncated response.getText()throws.cancel()and an earlybreakare not treated as failures.executionPromise. They now reject with the typed error.Structural gate
model-result.tswas already at the gate's fan-out ceiling of 15 edges. The new modules reach it through astream-runtime.tsre-export façade that also carriesReusableReadableStream, the same pattern asasync-tools.ts.max_cc15.Behavior changes (minor)
ErrortoStreamFailedError, which is anErrorsubclass. Message text changed forresponse.failed, for exampleResponse failed (server_error): ….hooks-post-model-call) asserted the silent completion. It now asserts the typed rejection.Tests
stream-watchdog.test.ts(22 tests) uses fake timers throughout. It covers:created+ role prelude, then silence)contentIntervalMson its ownonStallfiring exactly oncestream-failed-error.test.ts(58 tests) runs every getter againstresponse.failed, error-then-close and truncated streams. It covers no-tools runs, the initial turn with tools, and the follow-up turn with tools. It also covers error-then-completion resolving,active-consumersreplay keeping the error event, and cancel/breaknot counting as failures.Port notes (Python / Go bots)
Carry:
kindfield and the transient-code seterrorerrorevents, surface them only at end of stream" rule, on every consume pathThe
ReusableReadableStream/onValueplumbing is TypeScript-specific.Verification
pnpm install --frozen-lockfile,pnpm typecheck,pnpm lint,pnpm test(agent 1316, mcp 12, coverage ratchet holds),pnpm build,pnpm verify:packages, andsentrux gate(no degradation). CI is green.Once this stack lands, OpenRouterTeam/typescript-sdk#770, OpenRouterTeam/typescript-sdk#773, OpenRouterTeam/typescript-sdk#774 and OpenRouterTeam/typescript-sdk#775 can be closed. OpenRouterTeam/typescript-sdk#776 stays with the SDK.
🤖 Generated with Claude Code