Skip to content

feat: add ThinkingChunk to StreamChunk protocol#39

Closed
tony-chinchill-ai wants to merge 2 commits into
Chinchill-AI:mainfrom
tony-chinchill-ai:feat/thinking-chunk
Closed

feat: add ThinkingChunk to StreamChunk protocol#39
tony-chinchill-ai wants to merge 2 commits into
Chinchill-AI:mainfrom
tony-chinchill-ai:feat/thinking-chunk

Conversation

@tony-chinchill-ai

Copy link
Copy Markdown
Contributor

Summary

  • Add ThinkingChunk dataclass (type="thinking", content="") to StreamChunk union for AI reasoning display
  • Slack adapter: extract content field in send_structured_chunk() so thinking is serialized to Slack's streaming API
  • Teams adapter: render thinking chunks as italic text above accumulated response
  • Additive-only — consumers that don't handle "thinking" chunks silently ignore them

Context

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

  • 3 new tests for ThinkingChunk dataclass (defaults, content, literal type)
  • 2 new tests for Teams stream thinking chunk handling (italic rendering, skip-when-no-text)
  • Full suite passes (3307 passed, 0 failures)
  • Lint clean on changed files

🤖 Generated with Claude Code

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread src/chat_sdk/adapters/teams/adapter.py Outdated
Comment on lines +773 to +781
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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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.

Suggested change
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

tony-chinchill-ai and others added 2 commits April 10, 2026 14:45
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>
@patrick-chinchill patrick-chinchill marked this pull request as draft April 10, 2026 22:46
@tony-chinchill-ai

Copy link
Copy Markdown
Contributor Author

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.

tony-chinchill-ai added a commit to tony-chinchill-ai/chat-sdk-python that referenced this pull request Apr 10, 2026
…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>
patrick-chinchill added a commit that referenced this pull request Jun 20, 2026
…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``.
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.

1 participant