Skip to content

Streaming: chat_stream + SSE endpoint - #7

Merged
sagi5060 merged 4 commits into
devfrom
feat/2-streaming
Jul 26, 2026
Merged

Streaming: chat_stream + SSE endpoint#7
sagi5060 merged 4 commits into
devfrom
feat/2-streaming

Conversation

@sagi5060

Copy link
Copy Markdown
Collaborator

What

  • App.chat_stream(name, session_id, message, **runner_options) — async iterator of text deltas, wrapping the Agents SDK Runner.run_streamed. Same session semantics as chat().
  • HeadlessRunner.run_streamed — streaming counterpart to HeadlessRunner.run, mirroring its trace_run span, sandbox attach/detach, and run_config/max_turns handling; the trace's output is set from the streamed result's final_output once the generator is fully drained.
  • POST /agents/{name}/chat?stream=truetext/event-stream response: incremental data: {"delta": ...} events, then one event: done carrying the reassembled full output.
  • Non-streaming chat() / POST /agents/{name}/chat path is unchanged.

Why

Middle's UX targets (first acknowledgement p95 < 5s) need incremental output; today chat() blocks until the whole turn completes.

Decisions / deviations

  • The SSE done event's output is the deltas rejoined, not a separate RunResult field — that's exactly the final output for a plain-text agent (the streaming use case), and keeps chat_stream's contract to "async iterator of text deltas" per the issue, without holding the SDK's RunResultStreaming open past that.
  • Tests fake the SDK boundary (Runner.run_streamed) and stub HeadlessRunner.from_agent rather than mocking a live model — no live key needed. They assert: deltas arrive incrementally (not batched), non-text-delta events are filtered, run_config/max_turns/session are threaded through identically to run, and chat()/chat_stream() resolve the same session object for a given session_id (session history stays identical whether a turn is streamed or not).

Test plan

  • .venv/bin/ruff check agentdeck/ tests/
  • .venv/bin/ty check agentdeck
  • .venv/bin/pytest tests/ -q (7 passed)

Closes #2

🤖 Generated with Claude Code

sagi5060 and others added 4 commits July 26, 2026 22:36
…e}/chat

Runner.run_streamed's async iterator of raw response events replaces the
blocking Runner.run for UX targets that need first output before the whole
turn completes. HeadlessRunner.run_streamed mirrors run's trace/sandbox/
run_config lifecycle, App.chat_stream threads the same session as chat(),
and the SSE endpoint (?stream=true) reassembles deltas into the done event's
full output.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Review fixes for the streaming PR:

- run_streamed wraps consumption in contextlib.aclosing and cancels the SDK
  RunResultStreaming in a finally, so an abandoned generator (client
  disconnect, which Starlette never aclose()s) stops the detached run loop
  before the sandbox unwinds instead of deferring to GC-time finalizers.
- run_streamed now ends with a StreamDone(final_output, usage) sentinel; the
  done SSE event carries the SDK's final_output instead of the re-joined
  deltas, which disagreed with chat() for tool-using agents and returned raw
  JSON for output_type agents. usage rides along for token accounting.
- serve.py reads session_id/message before the response starts (missing field
  -> 422, not a 200 that streams nothing) and emits an `event: error` frame,
  with no internal detail, when the turn fails mid-stream.
- Failed or abandoned streamed turns record tr.set_output(error=...) so they
  show up as ERROR in Langfuse.
- SSE responses send Cache-Control: no-cache and X-Accel-Buffering: no.
- BaseRunner declares run_streamed (opt-in, not abstract); stale headless
  module docstring fixed; redundant asyncio markers dropped.
- New endpoint tests over FastAPI's TestClient: frame sequence, 4xx on missing
  session_id, mid-stream error frame; plus a cancel-on-abandonment test.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Resolve app.py (chat_stream + open/aclose lifecycle), serve.py (streaming
chat endpoint over dev's deck() 503 helper and error handlers), CHANGELOG.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… 503s pre-startup)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@sagi5060
sagi5060 merged commit fceb48c into dev Jul 26, 2026
@sagi5060
sagi5060 deleted the feat/2-streaming branch July 27, 2026 17:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Streaming: chat_stream + SSE endpoint

1 participant