Repository navigation
fix: raise RuntimeError on second ctx.stream() consumption - #62
shloimy-wiesel with Copilot wants to merge 20 commits into
Conversation
Update issue templates
* "Claude PR Assistant workflow" * "Claude Code Review workflow"
…penAI adapter, and request types
Add the full StreamContext feature surface for AI SDK v6 compatibility:
Stream collection & persistence:
- collect=True accumulates text, reasoning, tool calls, sources, files,
data parts, token counts, and timing into StreamRecord (ctx.record)
- on_finish callback for post-stream DB persistence
- Per-call collect=False to skip recording ephemeral writes
- RuntimeError when collect=True on non-collecting context
StreamContext enhancements:
- Generic[_InfoT] typed custom_information (ctx.info) for request-scoped metadata
- ctx.run() safe task runner: auto-finish, auto-error, GC-safe
- ctx.error() emits ErrorEvent then terminates stream
- ctx.abort() emits AbortEvent per v6 spec (with optional reason)
- ctx.write_file() for FileEvent support
- ctx.write_data() for dynamic data-{name} parts (any JSON-serializable value)
- start_tool_input/stream_tool_input_delta/finish_tool_input for streaming tool input
- start_metadata parameter for start event messageMetadata
- Auto-count streaming tokens with configurable count_func + set_usage() override
- Timing fields (created_at, finished_at, duration_ms) on StreamRecord
New event types:
- ErrorEvent, AbortEvent, FileEvent, DataEvent (dynamic type)
- ToolInputStartEvent, ToolInputDeltaEvent, ToolInputAvailableEvent
OpenAI contrib module (contrib/openai):
- consume_openai_stream() maps OpenAI chunks to ctx.write_* calls
- convert_to_openai_messages() converts UIMessage parts to ChatCompletionMessageParam
- Duck-typed — no hard openai package dependency
Request body types (types.py):
- ChatRequest, UIMessage, and all v6 part models (TextUIPart, ToolUIPart, etc.)
- Discriminated union for MessagePart with dynamic prefix support
- extra="allow" for forward compatibility
Bug fixes:
- Tool name deduplication in consume_openai_stream
- Tool call ID consistency using ToolCallHandle.toolCallId
- write_data() accepts any JSON-serializable value, not just dict
- DataEvent.encode() uses model_dump_json() for non-native types
Also adds chatbot-backend example (FastAPI + Redis + 5 tools),
hierarchical AGENTS.md knowledge base, and 237 passing tests.
#47) feat: add stream_exclude and store_exclude parameters to StreamContext Agent-Logs-Url: https://github.com/shloimy-wiesel/ai-sdk-stream-python/sessions/ed622073-4b0a-4004-a3d8-eb53d3363f9d
feat: add tool_calls_in_reasoning option to StreamContext When enabled, begin_tool_call and start_tool_input skip closing the reasoning block, allowing tool calls to be emitted inline within reasoning for frontends that support interleaved rendering. Agent-Logs-Url: https://github.com/shloimy-wiesel/ai-sdk-stream-python/sessions/5576925b-99a1-4b5e-8f28-bd20be5b267b --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: shloimy-wiesel <144027408+shloimy-wiesel@users.noreply.github.com>
…les (#61) * docs: replace asyncio.create_task pattern with ctx.run() in all examples Update the three locations that still showed the old manual asyncio.create_task + try/finally pattern to use the recommended ctx.run() approach: the __init__ module docstring quickstart, the context.py module docstring FastAPI example, and the live example/backend/routes/chat.py endpoint. The example backend also drops the _background_tasks GC-protection set and the asyncio import, which ctx.run() makes unnecessary. Co-authored-by: shloimy-wiesel <shloimy-wiesel@users.noreply.github.com> * fix: replace lambda with named _work function and update StreamContext docstring - example/backend/routes/chat.py: replace `lambda c: ...` with a named `async def _work(c: StreamContext)` consistent with the docstring examples - context.py: update StreamContext class docstring to mention `ctx.run()` instead of the old "spin up a background task" phrasing Co-authored-by: shloimy-wiesel <shloimy-wiesel@users.noreply.github.com> --------- Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com> Co-authored-by: shloimy-wiesel <shloimy-wiesel@users.noreply.github.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Track a `_streamed` flag on StreamContext. The `stream()` async
generator raises `RuntimeError("ctx.stream() can only be consumed
once")` on its second call, preventing silent deadlocks from retry
middleware or accidental re-iteration.
Add two tests in TestEdgeCases:
- test_stream_raises_on_second_consumption
- test_stream_raises_before_iteration_on_second_call
|
@claude review this pr |
|
Claude finished @shloimy-wiesel's task in 1m 58s —— View job PR Review: fix: raise RuntimeError on second ctx.stream() consumption
SummaryThis PR fixes a real bug: double-consuming What's GoodImplementation ( Tests (
This distinction matters: because Version bump: One thing to flagThe # tests/test_context.py:29–38
async def collect_stream(ctx: StreamContext) -> list[dict]:
async for chunk in ctx.stream():
if chunk.strip() == "data: [DONE]":
break # generator abandoned here, aclose() deferred to GC
...This is pre-existing behavior, not introduced by this PR, and it doesn't cause any correctness issues here — Not a blocker for this PR, but worth a follow-up VerdictApprove. The fix is minimal, correct, and well-tested. No issues blocking merge. |
Calling
ctx.stream()more than once silently deadlocks — the[DONE]sentinel is consumed on the first iteration, leaving the queue permanently empty and the second consumer blocked forever.Changes
StreamContext.__init__— adds_streamed: bool = FalseflagStreamContext.stream()— raisesRuntimeErrorimmediately on re-entry before touching the queue; sets_streamed = Trueon first callTestEdgeCases— two new tests: one assertingRuntimeErroris raised on the second call, one confirming the error surfaces on__anext__()before any queue wait