perf: amortize replay buffer compaction in ToolEventBroadcaster and ReusableReadableStream - #109
Conversation
Co-Authored-By: matt.apperson <me@mattapperson.com>
Co-Authored-By: matt.apperson <me@mattapperson.com>
Co-Authored-By: matt.apperson <me@mattapperson.com>
Original prompt from matt.apperson
|
🤖 Devin AI EngineerI'll be helping with this pull request! Here's what you should know: ✅ I will automatically:
Note: I can only respond to comments from users who have write access to this repository. ⚙️ Control Options:
|
Summary
Port of typescript-sdk#798 onto the repo where this code actually ships. That PR was written against
@openrouter/sdk's hand-writtensrc/lib/{reusable-stream,tool-event-broadcaster,model-result}.ts, but openrouter-web's inference path reaches these classes through@openrouter/agent(packages/router/plugins/server-toolslazily imports@openrouter/agent/call-model), so the SDK copies are dead relative to production traffic and the fix has no effect there.Two changes, both opt-in-safe.
1. Replay compaction behind a new
streamReplayoption. Both replay classes retained every event for the lifetime of acallModel()run, which is expensive for long generator-tool streams in a constrained runtime. Replay is also a compatibility contract — delayed and sequential stream getters must still start from event zero — so the tradeoff is now explicit at the call site.'full'is the default and preserves current behavior exactly;'active-consumers'releases entries once every attached consumer has advanced past them.Consumer positions become absolute against a logical buffer head, so the common case is O(1) and the physical copy is amortized rather than quadratic:
Clearing slots is what frees payload memory; the
slice()only bounds backing-array length. Reading a cleared slot is unreachable under the position invariant, so it throws rather than ending the stream — a silently truncated response is the worst available failure mode here.2. Terminal response events end the stream. A provider can send an authoritative
response.completed/response.failed/response.incompletewithout closing the HTTP body. The pump now buffers and broadcasts that event, marks itself complete, cancels the source reader at most once, and releases the lock exactly once; cancellation failure is cleanup-only and never overrides the provider's terminal result. Tool execution can therefore continue instead of waiting for an EOF that never arrives.Divergence from the SDK PR
model-result.tshas diverged heavily between the two repos (~7.2k lines here), and this copy already captured terminal events in its initial pipe. Rather than paste the SDK's version over it, only the missing pieces were added, expressed against agent-only machinery that the upstream PR never had to consider:initialResponse/initialResponseErrorcache the terminal result so aggregate getters stay correct once history is trimmed, andgetInitialResponse()emits the pending model call before returning a cached response — otherwise thePostModelCallhook would silently stop firing on that path.extractCompletionFromBuffer/tryExtractCompletionFromBufferteardown paths (includinggetUsage()) now consult that cache first, because in'active-consumers'mode a backward buffer scan can no longer find a terminal event that has already been released.streamReplayis client-only, so it is stripped in both places:clientOnlyFieldsinasync-params.tsfor the async path, and the already-resolved destructure inmodel-result.tsfor reconstructed follow-up request bodies.API example
Landing this is also a prerequisite for deleting the SDK-side copies, which are being removed separately.
How I verified
pnpm lint,pnpm typecheck, andpnpm testinpackages/agent: 98 files, 1168 tests passing, no type errors. New coverage includes default full replay for sequential and post-completion consumers in both classes, active-consumer watermark behavior and repeated amortized compaction, active-mode aggregate response/tool/error caching,PostModelCallfiring exactly once on the cached-terminal path, usage still resolving after an active-mode consumer has drained the terminal event, absence ofstreamReplayfrom captured initial and follow-up request JSON, open bodies ending at completed/failed/incomplete events, and exactly-once cancellation plus reader-lock release.The 50k-event drain measurements in the SDK PR were taken against the SDK files and have not been re-run here, so treat them as indicative rather than as evidence for this diff.
Link to Devin session: https://openrouter.devinenterprise.com/sessions/140e41de7e104f599a273d35fadbdfe4
Requested by: @mattapperson