Show the chat run: show_process weaving and the ChatStream events view - #454
Merged
Conversation
…nts view
chat(stream=True) now returns a ChatStream: iterating it yields the
answer text with the run woven in by default — "[thinking] " sections,
one "[tool] name arguments" line per call with its clipped result —
and .events yields the run as typed dicts (thinking/answer deltas,
tool_call with parsed arguments, tool_result with the full output).
One run serves one view; close() kills it like a closed generator.
- show_process: on by default ("on where available"); False for the
bare answer stream; a dict (ChatProcessOptions: thinking, tool_calls,
tool_results, max_chars) selects the parts. Explicit True without
stream=True raises.
- Managed clients weave what the endpoint serves: tool-call lines
parsed from its block_metadata chunk tags (that wire carries no
thinking and no tool results); old-wire chunks stay plain answer
text, and the bare answer view no longer leaks tool-argument JSON.
- Engine: one typed-event primitive (_chat_events_agen /
_cloud_chunk_events) with _weave as a pure renderer over it; the
chat lane's prologue is shared via _chat_agent, behavior unchanged
on chat_completions/responses/messages.
453 tests green (16 new, red-verified), no-openai-agents leg simulated,
pyright flat vs main.
Claude-Session: https://claude.ai/code/session_014GN8u3zdH3RpeftHZChavP
| arguments = getattr(raw, "arguments", "") or "" | ||
| try: | ||
| arguments = json.loads(arguments) | ||
| except (ValueError, TypeError): |
| arguments = "".join(pieces) | ||
| try: | ||
| arguments = json.loads(arguments) | ||
| except ValueError: |
Member
Author
Code reviewNo issues found. Checked for bugs and CLAUDE.md compliance. 🤖 Generated with Claude Code |
…idate show_process before the managed request Three review findings on the show_process weave, all red-verified: - _weave nested every tool result under the most recent [tool] line, which misattributes results when a turn makes parallel calls (the SDK streams all calls, then all results). A result now nests only when the line above is its own call (by call_id); otherwise it stands alone with its call's clipped arguments echoed, so same-name parallel calls stay tellable apart. Hidden-call mode falls out unchanged (no stored arguments, no echo). - Empty deltas now stop at the event source. chat_completions and the managed chunk lane both filter them; the new local lane did not, so a mid-stream "" (litellm forwards annotated/provider-field empties) leaked into the bare view and flipped _weave sections, splitting one thinking burst into repeated labels. - The managed streaming lane sent the billed request before _process_options ran, so a config typo cost a real chat call. chat() now chokes on bad show_process before dispatching, matching the local lane's validate-first order. Two docstring truths: close()'s "a run never consumed never starts" holds only for own-model chat (the managed request is already on the wire), and the module docstring now covers the managed chunk weave. 456 tests green; the three new ones red-verified; managed-lane paths re-run with the agents package blocked; pyright adds nothing on touched lines. Claude-Session: https://claude.ai/code/session_01XeeD2214z6Vd6qKcAi9ZvJ
…am overloads
Four review fixes plus two coverage gaps, each red- or
mutation-verified:
- _cloud_chunk_events treated any non-tool_use tag inside an open tool
block as answer text, so argument JSON leaked into the
show_process=False answer — the one meant to be appended back as
conversation history. Inside an open block nothing is answer:
argument chunks now accumulate under any tag. And non-string
argument pieces stringify at the join instead of killing the whole
stream with a raw TypeError.
- _process_options sorted unknown keys before repr-ing them, so
mixed-type keys ({1: True, "foo": 1}) raised a bare TypeError past
the caller's `except PageIndexAPIError`; sorting the reprs keeps the
single error type.
- chat() gains @overload on stream, so the docstring's own `.events`
usage type-checks for py.typed consumers (previously pyright ruled
`Cannot access attribute "events" for class "str"` on the exact
documented snippet). pageindex/ error count unchanged (234).
- Coverage: the streaming lane's whole `finally` could be deleted with
the suite still green — the new abandonment test pins the teardown
(pump exits, turn 2 emits nothing, _aclose_backend closes the
per-call client). And FakeModel emitted only the reasoning event
production never sends (litellm folds reasoning into
reasoning_content, which arrives as summary deltas); it now
alternates variants, so dropping either from the isinstance tuple
goes red.
459 tests green; managed-path tests re-run with the agents package
blocked; flake8 parity on every touched file.
Claude-Session: https://claude.ai/code/session_01DWBCCTDzwuVamBf5MvQ4eP
| model: Optional[str] = None, | ||
| reasoning_effort: Optional[str] = None, | ||
| show_process: Union[bool, Mapping[str, Any], None] = None, | ||
| ) -> str: ... |
| model: Optional[str] = None, | ||
| reasoning_effort: Optional[str] = None, | ||
| show_process: Union[bool, Mapping[str, Any], None] = None, | ||
| ) -> "ChatStream": ... |
| reasoning_effort: Optional[str] = None, | ||
| ) -> Union[str, Iterator[str]]: | ||
| show_process: Union[bool, Mapping[str, Any], None] = None, | ||
| ) -> Union[str, "ChatStream"]: ... |
…sy show_process message
ChatStream.events was a property whose getter latched the stream's one
view on mere attribute access: a debugger variable pane, hasattr, or
getattr(stream, "events", None) — which PageIndexAPIError escapes, as
getattr only swallows AttributeError — was enough to make a later
`for chunk in stream:` refuse, with nothing consumed. The getter now
returns a lazy generator: the managed refusal, the view claim and the
run start all happen on first consumption, so introspection is
side-effect free and the text view stays usable after a probe.
And the stream=False guard's message told falsy-but-not-False values
("show_process=0", "") that they passed show_process=True; the check
itself is the ruled falsy-{} trap and stands, but the message now
names the off values and echoes what was got.
461 tests green (2 red-verified new: inert read on both lanes, plus
the falsy-message case); changed managed-path tests re-run with the
agents package blocked; pyright pageindex/ 234 -> 234.
Claude-Session: https://claude.ai/code/session_01DWBCCTDzwuVamBf5MvQ4eP
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
chat(stream=True)now shows the run. It returns aChatStreamwith two views over one run:API
show_processonchat()— on by default ("on where available").False= bare answer stream; a dict (pageindex.ChatProcessOptions) selects parts:thinking/tool_calls/tool_results(bools, default on) andmax_chars(line cap, default 200). ExplicitTruewithoutstream=Trueraises.ChatStream: iterating yields text (next()works; existingfor piece in client.chat(..., stream=True)code is unchanged in shape),.eventsyields typed dicts. One run serves one view;close()kills the stream like a closed generator.chat()and the protocol surfaces (chat_completions/responses/messages) are untouched.Behavior changes (release-notes material)
chat()output now includes the process by default — passshow_process=Falsefor the old bare answer stream (and when joining a stream back into conversation history).block_metadatachunk tags (that wire carries no thinking and no tool results). Old-wire chunks (noblock_metadata) remain plain answer text. As a consequence, the bare answer view no longer leaks the tool-argument JSON the endpoint interleaves intodelta.content—chat_completions()'s raw text mode is deliberately left as-is.Engine
One typed-event primitive per lane (
_chat_events_agenfor the in-process agent,_cloud_chunk_eventsfor managed chunks) with_weaveas a pure renderer over it — both views, both lanes, one code path. The chat lane's prologue is shared via_chat_agent(pure refactor;chat_completionsbehavior unchanged).Verification
{}trap, events sequence with parsed arguments and unclipped output, one-view claim,next()compat, closed-stream deadness, managed weave / clean-off / old-wire compat).agentsblocked): managed weaving and all validation paths run without openai-agents.https://claude.ai/code/session_014GN8u3zdH3RpeftHZChavP