feat(typescript): expose SSE event id and retry metadata#16677
feat(typescript): expose SSE event id and retry metadata#16677dvdaruri-art wants to merge 3 commits into
Conversation
Add ServerSentEvent<T> interface and events() method to Stream class. Parse id: and retry: SSE fields in both discriminated and non-discriminated paths. Expose lastEventId property for resumable streaming support. Achieves parity with Python SDK's ServerSentEvent dataclass. Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
🤖 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:
|
| if (this.prefix != null && line.startsWith(RETRY_PREFIX)) { | ||
| continue; | ||
| } |
There was a problem hiding this comment.
🟡 iterDataMessages recognizes retry: SSE fields but silently discards their values
In the non-discriminated SSE path (iterDataMessages), the retry: field is recognized and skipped at lines 147-149 to prevent it from being misinterpreted as data, but unlike id: (which is parsed and stored at lines 139-145), the retry value is never parsed or included in the yielded ServerSentEvent<T>. When users call events() on a non-discriminated SSE stream (i.e., no eventDiscriminator set), the yielded objects will never contain a retry field — even when retry: fields are present in the stream. This is inconsistent with the discriminated path (iterSseEvents at Stream.template.ts:217-221), which properly parses and yields retry, and contradicts the events() JSDoc ("Iterates over full SSE events including metadata (event, id, retry)").
Prompt for agents
In iterDataMessages (Stream.template.ts around line 147), the retry: field is recognized and skipped but the value is discarded. It should be parsed and tracked (similar to how id: is tracked in _lastEventId), then included in the yielded ServerSentEvent object at lines 164-167. Since iterDataMessages processes lines independently without event-block boundaries, the retry value could be tracked in a local variable and reset after each data yield, similar to how prefixSeen is reset. The yielded object should include the retry field: yield { data: message, eventId: this._lastEventId, retry: lastRetryValue }. Note that the same inconsistency exists for the event: field — event: lines are not recognized or tracked in iterDataMessages at all, despite events() promising event metadata.
Was this helpful? React with 👍 or 👎 to provide feedback.
There was a problem hiding this comment.
Good catch — fixed in b4c3f50. iterDataMessages now parses retry: values and includes them in the yielded ServerSentEvent<T>, matching the discriminated path. The value resets after each data yield.
| return; | ||
| } | ||
| const message = await this.parse(fromJson(line)); | ||
| yield message; | ||
| yield { | ||
| data: message, | ||
| eventId: this._lastEventId, | ||
| }; | ||
| prefixSeen = false; |
There was a problem hiding this comment.
🚩 event: field is not tracked in the non-discriminated SSE path
In iterDataMessages, event: lines are not explicitly recognized — they fall through to the data-prefix check at line 151-158 and get silently skipped (since event: ... doesn't contain data:). The yielded ServerSentEvent<T> from this path never includes the event field. This is architecturally consistent with how iterDataMessages works (line-by-line without event-block semantics), but it means events() on a non-discriminated SSE stream provides less metadata than the discriminated path. The event: line association requires event-block boundaries (blank-line separation) to work correctly, so fixing this would require refactoring iterDataMessages to understand event blocks, effectively merging it with iterSseEvents. This is related to but distinct from the reported retry bug.
(Refers to lines 123-168)
Was this helpful? React with 👍 or 👎 to provide feedback.
There was a problem hiding this comment.
Acknowledged — this is an inherent architectural limitation of iterDataMessages. This path processes data: lines independently (no blank-line event block accumulation), so there's no way to associate an event: line with a specific data: line without refactoring to full event-block parsing. Since non-discriminated SSE doesn't use the event: field for dispatch (that's what iterSseEvents with eventDiscriminator is for), this is by design. Users who need event: metadata should configure eventDiscriminator in their API definition, which routes to the full-featured iterSseEvents path.
SDK Generation Benchmark ResultsComparing PR branch against median of 5 nightly run(s) on Full benchmark table (click to expand)
main (generator): generator-only time via --skip-scripts (includes Docker image build, container startup, IR parsing, and code generation — this is the same Docker-based flow customers use via |
Address Devin Review feedback: iterDataMessages now parses and yields retry values instead of silently discarding them. Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Description
Adds
lastEventIdgetter andevents()async generator to the TypeScript SDK'sStreamclass, building on theServerSentEvent<T>/withMetadata()support already on main. This gives users SSE spec-compliant access to the last event ID as an instance-level property (persists across events per the SSE spec).Changes Made
_lastEventIdinstance variable toStreamclass — tracks the last event ID at the instance level (persists across events per SSE spec), accessible viastream.lastEventIdgetterevents()async generator method — convenience alias for iteratingServerSentEvent<T>objects (equivalent towithMetadata())iterDataMessages()anditerSseEvents()to usethis._lastEventIdinstead of local variables, so the ID is accessible externally during/after iterationlastEventIdtracking, null character rejection, andevents()metadata iterationTesting
lastEventId, null char rejection,events()method in both discriminated and non-discriminated pathsLink to Devin session: https://app.devin.ai/sessions/3fcaaa36e6f446809c50a8fe676b0b9e
Requested by: @dvdaruri-art