Add explicit SSE event names for local v3 streaming#1858
Add explicit SSE event names for local v3 streaming#1858
Conversation
🦋 Changeset detectedLatest commit: 96cd037 The changes in this PR will be included in the next version bump. This PR includes changesets to release 5 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
✱ Stainless preview buildsThis PR will update the Edit this comment to update it. It will appear in the SDK's changelogs. 🚧 stagehand-java studio
✅ stagehand-typescript studio · code · diff
✅ stagehand-ruby studio · code · diff
✅ stagehand-csharp studio · code · diff
✅ stagehand-kotlin studio · code · diff
✅ stagehand-python studio · code · diff
✅ stagehand-go studio · code · diff
✅ stagehand-openapi studio · code · diff
✅ stagehand-php studio · code · diff
This comment is auto-generated by GitHub Actions and is automatically kept up to date as you push. |
Greptile SummaryThis PR upgrades the local v3 SSE streaming protocol by emitting an explicit Key changes:
Confidence Score: 3/5
Important Files Changed
Sequence DiagramsequenceDiagram
participant Client
participant Server as stream.ts
participant SDK as Stainless SDK
Client->>Server: POST /v1/sessions/:id/act (streamResponse: true)
Server-->>Client: HTTP 200 text/event-stream
Server-->>Client: event: starting\ndata: {"data":{"status":"starting"},"type":"system","id":"..."}
SDK-->>SDK: event_type=starting → handle: yield (emit to consumer)
Server-->>Client: event: connected\ndata: {"data":{"status":"connected"},"type":"system","id":"..."}
SDK-->>SDK: event_type=connected → handle: yield (emit to consumer)
loop log messages
Server-->>Client: event: running\ndata: {"data":{"status":"running","message":"..."},"type":"log","id":"..."}
SDK-->>SDK: event_type=running → handle: yield (emit to consumer)
end
alt Success
Server-->>Client: event: finished\ndata: {"data":{"status":"finished","result":...},"type":"system","id":"..."}
Note over SDK: Previously handle: done (not yielded)<br/>Now handle: yield (surfaced to consumer)
SDK-->>SDK: event_type=finished → handle: yield (emit to consumer)
else Error
Server-->>Client: event: error\ndata: {"data":{"status":"error","error":"..."},"type":"system","id":"..."}
SDK-->>SDK: event_type=error → handle: error (throw)
end
Server-->>Client: [connection close — reply.raw.end()]
Last reviewed commit: "Add typed SSE event ..." |
| - event_type: error | ||
| handle: error | ||
| - event_type: null | ||
| - event_type: [starting, connected, running, finished] | ||
| handle: yield |
There was a problem hiding this comment.
finished changed from handle: done to handle: yield — potential breaking change for generated SDK consumers
In the old config, the finished event had handle: done, which in Stainless streaming means the SDK treats that event as a stream-termination signal and does not yield it to consumer code. With handle: yield, the finished event is now emitted to SDK consumers like any other event.
This is a semantic change in the contract of the generated SDK: any caller iterating over the stream would now receive a finished event in their loop, whereas before the stream simply ended. Unless generated SDK consumers already expect and handle a finished event in their event handler, this is a breaking change.
Since the server still calls reply.raw.end() immediately after sending finished, the stream termination itself is unaffected — but whether the finished payload is surfaced to SDK consumers changes. This should be explicitly tested/verified against the generated SDK client, or a comment added to the config explaining the deliberate shift.
There was a problem hiding this comment.
yeah dont we still need handle: done somewhere? will stainless properly know when the stream is complete without some branch reporting done?
There was a problem hiding this comment.
No, you don't need handle: done. The stream completes when the HTTP connection closes, which happens right after the finished event is sent.
Looking at packages/server-v3/src/lib/stream.ts:208-214:
sendData("finished", "system", {
status: "finished",
result: result?.result,
actionId,
});
if (shouldStreamResponse) {
reply.raw.end(); // ← HTTP stream closes here
return reply;
}The old handle: done was preventing the finished event from being yielded to SDK consumers — which was exactly the bug this PR fixes ("The stainless sdks are dropping the final finished SSE event instead of yielding it").
With the new config:
finishedevent is sent withevent: finished\ndata: {...}- Event is yielded to SDK consumers (they can now see it)
- HTTP stream immediately closes via
reply.raw.end() - Stainless detects stream completion from the closed connection
The handle: done was just an optimization to say "don't yield this event" — but Stainless doesn't need it to know the stream is complete. The closed HTTP connection is the real termination signal.
There was a problem hiding this comment.
No issues found across 6 files
Confidence score: 5/5
- Automated review surfaced no issues in the provided summaries.
- No files require special attention.
Architecture diagram
sequenceDiagram
participant Client as Client (SDK / Stainless)
participant Server as Server-v3 (stream.ts)
participant Runner as Action Runner (Core)
Note over Client,Server: Request with streamResponse: true
Client->>Server: POST /v1/sessions/:id/act
Server->>Server: Initialize createStreamingResponse()
rect rgb(240, 240, 240)
Note right of Server: SSE Stream Start
Server->>Client: NEW: event: starting \n data: { "status": "starting", ... }
end
Server->>Runner: execute(action)
loop Action Execution
Runner-->>Server: Log update
Server->>Client: NEW: event: running \n data: { "type": "log", "data": { "message": "..." } }
end
Server->>Client: NEW: event: connected \n data: { "status": "connected" }
alt Success Path
Runner-->>Server: Return Result
Server->>Client: NEW: event: finished \n data: { "status": "finished", "result": "..." }
else Error Path
Runner-->>Server: Throw Exception
Server->>Client: NEW: event: error \n data: { "status": "error", "error": "message" }
end
Note over Client,Server: CHANGED: Client SDK now routes by "event:" name <br/> instead of parsing JSON prefix in "data:"
Server->>Client: reply.raw.end() (Close SSE)
Why
The stainless sdks are dropping the final finished SSE event instead of yielding it. This is due to the fact that we were not using
eventfields (basically setting event types) as per the SSE spec.The fix is to emit explicit SSE
event:names and match them in Stainless. But on the hosted API we cannot switch that on for everyone at once, because older clients still expect the olddata:-only SSE framing. Thus, we will need to have branching logic in our hosted server:eventfield.>= 3.13.0: use typed SSE framing withevent:+data:.Once this and it's core counterpart PR are merged, then we will release another version of all stainless sdks -
3.13, which will be the first typed-SSE release.What Changed
event:names from the local v3 streaming helper while keeping the JSONdata:payload unchangedevent_typehandlersTesting
pnpm --dir /tmp/stagehand-local-sse.lSk5Av --filter @browserbasehq/stagehand-server-v3 run gen:openapipnpm --dir /tmp/stagehand-local-sse.lSk5Av exec prettier --check stainless.ymlpnpm --dir /tmp/stagehand-local-sse.lSk5Av --filter @browserbasehq/stagehand lintpnpm --dir /tmp/stagehand-local-sse.lSk5Av --filter @browserbasehq/stagehand-server-v3 lintA test run with the three different client types against the updated server:

Summary by cubic
Emit explicit SSE event names for v3 streaming (
starting,connected,running,finished,error) while keeping the JSONdata:payload unchanged. Updates the streaming helper, Stainless config, OpenAPI docs, and tests to use and verify typed events.New Features
event: <status>withdata: { data, type, id }.stainless.ymlstreaming matching toevent_type(yields on starting/connected/running/finished; handleserror).Dependencies
@browserbasehq/stagehandand@browserbasehq/stagehand-server-v3.Written for commit 96cd037. Summary will update on new commits. Review in cubic