feat: add ThinkingChunk to StreamChunk protocol#39
Conversation
There was a problem hiding this comment.
Code Review
This pull request introduces the ThinkingChunk type to support the display of AI reasoning and thinking processes across different chat platforms. The Slack and Teams adapters have been updated to handle these new chunks, with the Teams adapter specifically rendering thinking content as italicized text above the main message. The changes also include comprehensive unit and integration tests. A review comment suggests refactoring the chunk processing logic in the Teams adapter to improve readability and reduce redundant attribute checks.
| elif isinstance(chunk, dict) and chunk.get("type") == "markdown_text": | ||
| text = chunk.get("text", "") | ||
| elif hasattr(chunk, "type") and getattr(chunk, "type", None) == "markdown_text": | ||
| text = getattr(chunk, "text", "") | ||
| elif hasattr(chunk, "type") and getattr(chunk, "type", None) == "thinking": | ||
| thinking = getattr(chunk, "content", "") | ||
| # Render thinking as italic context above the main text | ||
| if accumulated or not message_id: | ||
| continue # Will be included in next text update |
There was a problem hiding this comment.
This series of elif statements can be refactored to reduce redundancy and improve readability. Specifically, the checks for markdown_text are duplicated, and the hasattr checks for different chunk types can be grouped together.
| elif isinstance(chunk, dict) and chunk.get("type") == "markdown_text": | |
| text = chunk.get("text", "") | |
| elif hasattr(chunk, "type") and getattr(chunk, "type", None) == "markdown_text": | |
| text = getattr(chunk, "text", "") | |
| elif hasattr(chunk, "type") and getattr(chunk, "type", None) == "thinking": | |
| thinking = getattr(chunk, "content", "") | |
| # Render thinking as italic context above the main text | |
| if accumulated or not message_id: | |
| continue # Will be included in next text update | |
| elif isinstance(chunk, dict) and chunk.get("type") == "markdown_text": | |
| text = chunk.get("text", "") | |
| elif hasattr(chunk, "type"): | |
| chunk_type = getattr(chunk, "type", None) | |
| if chunk_type == "markdown_text": | |
| text = getattr(chunk, "text", "") | |
| elif chunk_type == "thinking": | |
| thinking = getattr(chunk, "content", "") | |
| # Render thinking as italic context above the main text | |
| if accumulated or not message_id: | |
| continue # Will be included in next text update |
Bridges pydantic-ai thinking events to SDK StreamChunks so agent reasoning chains can be rendered in chat UIs. Additive-only — consumers that don't handle "thinking" chunks silently ignore them. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
e9f7ccf to
3b72aa5
Compare
|
Closing: ThinkingChunk (and related TaskUpdateChunk, PlanUpdateChunk) don't exist in the upstream Vercel chat-sdk. These will be removed from the library and any custom chunk types will live in application code instead. |
…ingChunk, PlanUpdateChunk) These types don't exist in the upstream Vercel TS chat-sdk. This library should remain a mechanical port — custom chunk types belong in application code. Also removes send_structured_chunk from Slack adapter and ThinkingChunk rendering from Teams adapter. Closes Chinchill-AI#39 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…upstream-compatible; supersedes #39) (#169) * feat(core): optional ThinkingChunk StreamChunk variant (default-off, upstream-compatible; supersedes #39) Adds a fourth, opt-in StreamChunk variant for streaming agent reasoning/"thinking" to chat platforms. The whole design is default-off: with no opt-in, the normalized stream and the posted message are byte-for-byte identical to upstream chat@4.31. - Additive type: ThinkingChunk(type="thinking", content=str) added to the StreamChunk union. The existing three variants are unaffected. - Opt-in emit: from_full_stream(stream, emit_thinking=False) and a thread-level emit_thinking config flag (threaded into the internal _from_full_stream) surface AI-SDK reasoning/reasoning-delta (and pydantic-ai part_kind=="thinking") parts as ThinkingChunk only when enabled. Default False drops reasoning exactly as upstream does. - Graceful consume: Thread._handle_stream never accumulates a ThinkingChunk into the posted-message text, and every adapter's stream handler skips it. Slack/Teams expose an optional render_thinking hook (via shared.adapter_utils.maybe_render_thinking); the text-accumulate adapters ignore it structurally — no crash, posted message unchanged. - No state pollution: ThinkingChunk is streaming-only. Message has no thinking field, to_json() is unchanged, and a round-tripped Message is byte-identical, so cross-SDK Redis/Postgres state stays compatible. - Docs: UPSTREAM_SYNC.md Known Non-Parity row added. Rationale: upstream's chat-platform SDK drops reasoning (leaves it to the AI-SDK web UI); chinchill streams thinking to Slack/Teams out-of-band today because there is no path. This gives the SDK a first-class, opt-in one without changing any default behavior. Gauntlet: ruff check + format, audit_test_quality (0 hard failures), verify_test_fidelity --strict (732/732, 0 missing), pyrefly src (0 errors), pytest (5121 passed, 4 pre-existing skips). * refactor(core): keep StreamChunk upstream-exact, ThinkingChunk as opt-in StreamInput Revert the public ``StreamChunk`` union to upstream's three variants (``MarkdownTextChunk | TaskUpdateChunk | PlanUpdateChunk``) so a consumer doing an exhaustive ``match`` over it sees zero change on upgrade. The Python-only ``ThinkingChunk`` is no longer a member of that union. Introduce a public ``StreamInput = str | StreamChunk | ThinkingChunk`` alias for what a stream may yield, and widen only the stream-input/output boundaries to accept it: the ``Adapter.stream()`` protocol signature, ``from_full_stream``/``_from_full_stream`` returns, ``Thread._wrapped_stream``, and each receiving adapter's ``stream()`` signature. A producer can yield ``ThinkingChunk`` (opt-in) and the adapters type-check; code referencing ``StreamChunk`` itself is unaffected. All other thinking behavior is preserved byte-for-byte: ``emit_thinking`` defaults to False (default stream identical to upstream), the ``_handle_stream`` graceful skip, the adapter skip / ``render_thinking`` branches, ``maybe_render_thinking``, and the no-``Message.thinking``-field / round-trip-identical guarantee. The UPSTREAM_SYNC divergence row is updated to state that ``StreamChunk`` is NOT widened and ``ThinkingChunk`` is a separate opt-in input type. Tests now assert ``get_args(StreamChunk)`` has exactly the three upstream variants and that ``ThinkingChunk`` is excluded from ``StreamChunk`` but accepted by ``StreamInput``.
Summary
ThinkingChunkdataclass (type="thinking",content="") toStreamChunkunion for AI reasoning displaycontentfield insend_structured_chunk()so thinking is serialized to Slack's streaming APIContext
Bridges pydantic-ai thinking events (Chinchill-AI/chinchill-api#952) to SDK StreamChunks so agent reasoning chains (e.g. "Analyzing → Checking → Resolving") are displayed rather than just logged.
Test plan
ThinkingChunkdataclass (defaults, content, literal type)🤖 Generated with Claude Code