You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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).
Copy file name to clipboardExpand all lines: docs/UPSTREAM_SYNC.md
+1Lines changed: 1 addition & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -684,6 +684,7 @@ stay explicit instead of being rediscovered in code review.
684
684
| Teams `cards_input` empty-options default (vercel/chat#8c71411, chat@4.31) |`input_request_to_teams_adaptive_card` reads `options = request.get("options") or []`| Upstream `cards-primitives/input.ts` reads `const options = request.options ?? []`| Benign truthiness divergence. The only values that differ between `or []` and `?? []` are the falsy-but-present ones (`[]`, `None`); for an options list both produce the same empty list, so the rendered card is byte-identical. Documented (not "fixed" to `is not None`) because there is no observable behavior difference — a present empty `options` and an absent `options` both yield "no choices". |
685
685
| Teams `graph` path-segment encoding (vercel/chat#8c71411, chat@4.31) | Path segments (`team_id` / `channel_id` / `chat_id` / `message_id`) are interpolated through `quote(segment, safe='')`| Upstream `graph/{channels,messages}.ts` use `encodeURIComponent(...)`| Benign over-encoding divergence. `quote(safe='')` percent-encodes a strictly larger set than `encodeURIComponent` (notably `!`, `'`, `(`, `)`, `*` — which `encodeURIComponent` leaves literal). Graph IDs (team/channel/chat/message GUIDs and thread tokens) never contain those characters, so the encoded path is identical in practice; where they did differ, the stricter `quote` is the safer choice (no URL injection via an unescaped sub-delimiter). Documented rather than narrowed to match `encodeURIComponent` exactly. |
686
686
| Teams `graph` defensive shape coercion (vercel/chat#8c71411, chat@4.31) | `to_graph_message` / channel + message readers coerce unexpected Graph payload shapes with `isinstance` guards (`x if isinstance(x, Mapping) else {}`, `value if isinstance(value, list) else []`) before reading fields | Upstream `graph/messages.ts` reads `message.from?.user` / `message.body?.content ?? ""` etc. with optional chaining — a non-object where an object is expected throws at the property access | Benign defensive divergence. Upstream's optional chaining tolerates `null`/`undefined` but throws on a wrong-typed non-null (e.g. a string where an object is expected); our `isinstance` coercion fails closed to an empty mapping/list instead of raising. For well-formed Graph responses the behavior is identical; the divergence only manifests on malformed payloads, where returning an empty-shape result is more resilient than throwing. Mirrors the repo's general "more resilient than throw" stance (cf. the `renderPostable on unknown input` row). |
687
+
| `ThinkingChunk` StreamChunk variant (Python-only, default-off; supersedes PR #39) | A fourth `StreamChunk` variant — `ThinkingChunk(type="thinking", content=str)` — surfaces AI-SDK `reasoning`/`reasoning-delta` (and pydantic-ai `part_kind == "thinking"`) parts. **OPT-IN, default-off**: emitted only when a caller passes `from_full_stream(stream, emit_thinking=True)` or sets the thread-level `emit_thinking=True` config; the internal `_from_full_stream` threads the same flag. With the default (`emit_thinking=False`) the normalized stream is **byte-for-byte identical** to upstream — reasoning parts are dropped and **no** `ThinkingChunk` is produced. Consumption is graceful: `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 `chat_sdk.shared.adapter_utils.maybe_render_thinking`; the text-accumulate adapters ignore it structurally). **Streaming-only — never persisted**: `Message` has no `thinking` field, `to_json()` is unchanged, and a round-tripped `Message` is byte-identical, so cross-SDK state (Redis/Postgres shared with the TS SDK) stays compatible. | Upstream `from-full-stream.ts` forwards only `text-delta` + `finish-step`; AI-SDK `reasoning`/`reasoning-delta` parts fall through and are discarded. `StreamChunk = MarkdownTextChunk | TaskUpdateChunk | PlanUpdateChunk` — no reasoning variant. Upstream leaves reasoning display to the AI-SDK web UI. | chinchill actively streams agent thinking to Slack/Teams but has to intercept the model stream out-of-band today because the chat-platform SDK has no path for it. This gives the SDK a first-class, opt-in one without changing any default behavior. The whole design constraint is that default-off == upstream: additive type, opt-in emit, graceful/skip consume, zero state pollution. Regression coverage: `tests/test_thinking_chunk.py`, `tests/test_from_full_stream.py::TestThinkingOptIn`, `tests/test_types.py::TestThinkingChunk`, plus per-adapter no-crash tests in `tests/test_slack_api.py`, `tests/test_teams_native_streaming.py`, `tests/test_twilio_adapter.py`, `tests/test_messenger_api.py`. |
0 commit comments