From e7fafc60135e99d81fb37c5cd5d682d62e002718 Mon Sep 17 00:00:00 2001 From: patrick-chinchill Date: Thu, 18 Jun 2026 15:39:27 -0700 Subject: [PATCH 1/5] =?UTF-8?q?chore(release):=20cut=200.4.30=20=E2=80=94?= =?UTF-8?q?=20Teams=20SDK=20migration=20+=204.30.0=20parity=20(#93=20PR=20?= =?UTF-8?q?4/4)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Final PR of the Teams SDK migration (issue #93). PRs 1-3 (inbound+auth, outbound, native streaming) are merged; this cuts the 0.4.30 release. - Version bump: pyproject 0.4.29 -> 0.4.30; UPSTREAM_PARITY "4.29.0" -> "4.30.0". - Fidelity re-pin chat@4.29.0 -> chat@4.30.0 in lint.yml + verify_test_fidelity.py (docstring, default parity fallback, clone hint). packages/chat/src is byte-for-byte identical between the two tags, so zero new test ports: strict fidelity stays 100% (732/732, 0 missing) against chat@4.30.0. Baseline regenerated (ts_parity -> chat@4.30.0; the recorded total_ts_tests literal 731 -> 732 corrects a stale count from the merged adapter waves, not the re-pin — the count is identical against both tags). - Docs: project-instructions version map + fidelity pin; README status line; CHANGELOG 0.4.30 entry (Twilio adapter, Telegram streaming, Slack subpaths, WhatsApp/Slack/gchat fixes, Teams #93 PRs 1-4); UPSTREAM_SYNC.md parity header + the Teams deferral row flipped to delivered. - Version-label normalization: malformed `adapter-teams@chat@4.30.0` and loose `adapter-teams@4.30.0` -> `@chat-adapter/teams@4.30.0` in adapter.py (5), bridge.py (1), UPSTREAM_SYNC.md (4). Comment/doc-only. Does NOT tag/publish — the release is a separate maintainer-gated step (live Teams 429 streaming check + PyPI authorization). --- .github/workflows/lint.yml | 2 +- CHANGELOG.md | 28 ++++++++++++++++++++++---- CLAUDE.md | 7 ++++--- README.md | 2 +- docs/UPSTREAM_SYNC.md | 15 +++++++------- pyproject.toml | 2 +- scripts/fidelity_baseline.json | 6 +++--- scripts/verify_test_fidelity.py | 6 +++--- src/chat_sdk/__init__.py | 2 +- src/chat_sdk/adapters/teams/adapter.py | 10 ++++----- src/chat_sdk/adapters/teams/bridge.py | 2 +- 11 files changed, 52 insertions(+), 30 deletions(-) diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 06fef1c..b4868eb 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -60,7 +60,7 @@ jobs: - name: Clone upstream vercel/chat at pinned parity tag id: clone_upstream run: | - git clone --depth 1 --branch chat@4.29.0 \ + git clone --depth 1 --branch chat@4.30.0 \ https://github.com/vercel/chat.git /tmp/vercel-chat - name: Test fidelity check (strict — zero missing in mapped core files) diff --git a/CHANGELOG.md b/CHANGELOG.md index 098b929..cced30e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,13 +1,33 @@ # Changelog -## Unreleased +## 0.4.30 -In flight toward `vercel/chat@4.30.0` (landing incrementally; `UPSTREAM_PARITY` -stays `4.29.0` until the wave completes). +Synced to upstream `vercel/chat@4.30.0`. The mapped core (`packages/chat/src`) is byte-for-byte unchanged from `4.29.0`, so this wave is all adapter work: a **new Twilio adapter**, a **Telegram native-streaming** port, a **Slack primitives-subpath** wave, a batch of **WhatsApp / Slack / Google Chat** fixes, and the headline — the **Teams adapter migration to the official `microsoft-teams-apps` SDK** (issue #93, delivered across four PRs). Sets `UPSTREAM_PARITY = "4.30.0"`; CI fidelity re-pinned to `chat@4.30.0` (732/732 mapped-core tests still pass, 0 missing). ### New adapter: Twilio (SMS / MMS / Voice) -- **`chat_sdk.adapters.twilio`** (vercel/chat#558). Twilio Programmable Messaging adapter (10th platform): inbound message webhooks with `X-Twilio-Signature` HMAC-SHA1 verification (`hmac.compare_digest`), outbound SMS/MMS through the Messages REST API (hand-rolled over an injectable transport — no official `twilio` SDK, mirroring upstream), 1:1 DM threads keyed `twilio:{sender}:{recipient}`, plus standalone `api` / `webhook` / `voice` helpers (TwiML builders, call + transcription parsing). New extra: `chat-sdk-python[twilio]`. Imports stay lazy so the package loads without `aiohttp` installed. +- **`chat_sdk.adapters.twilio`** (vercel/chat#558; PRs #142 + the scaffolding PR). Twilio Programmable Messaging adapter (10th platform): inbound message webhooks with `X-Twilio-Signature` HMAC-SHA1 verification (`hmac.compare_digest`), outbound SMS/MMS through the Messages REST API (hand-rolled over an injectable transport — no official `twilio` SDK, mirroring upstream), 1:1 DM threads keyed `twilio:{sender}:{recipient}`, plus standalone `api` / `webhook` / `voice` helpers (TwiML builders, call + transcription parsing). New extra: `chat-sdk-python[twilio]`. Imports stay lazy so the package loads without `aiohttp` installed. + +### Teams adapter: migration to the official `microsoft-teams-apps` SDK (issue #93) + +The hand-rolled Bot Framework REST + JWT stack is replaced by the official Microsoft Teams Python SDK (`microsoft-teams-apps` ≥ 2.0.13, added to the `[teams]` extra), mirroring upstream `@chat-adapter/teams@4.30.0`. Landed as four PRs: + +- **PR 1 — inbound + auth** (#143). New `adapters/teams/bridge.py`: a `BridgeHttpAdapter` implementing the SDK `HttpServerAdapter` protocol routes already-authenticated webhooks through the SDK `App`. JWT validation now runs through the SDK's `TokenValidator` (RS256 + audience + Bot Framework issuer via the live JWKS) in place of the hand-rolled `_verify_bot_framework_token` block. Graph reads stay hand-rolled (no `msgraph-sdk` / `[graph]` extra). +- **PR 2 — outbound** (#144). `post_message` / `start_typing` route through `App.send`; `edit_message` / `delete_message` route through `App.api.conversations.activities(...).update` / `.delete`. Per-thread service-URL routing retargets the SDK `ApiClient`'s service-url chain (validated against the SSRF allow-list). The camelCase wire dict is still returned as `RawMessage.raw`, preserving the public contract (attachment shape, file delivery, returned id). +- **PR 3 — native streaming** (#145). DM streaming uses the SDK's native `IStreamer` (`microsoft-teams-apps` `HttpStream`) via `app.activity_sender.create_stream(...)` and `stream.emit(...)` per chunk, replacing the hand-rolled Bot Framework streaming wire format. The SDK owns the streamType/streamSequence framing, the inter-flush throttle (~500ms, 429-safe), and 429 retry. Atomically unwinds the two transitional public-type divergences (`RawMessage.text`, `update_interval_ms`) that PR 3 made unnecessary. +- **PR 4 — release cut** (this entry). Version bump to `0.4.30`, fidelity re-pin to `chat@4.30.0`, docs, and the `@chat-adapter/teams@4.30.0` version-label normalization. + +The residual adapter-level divergences (we keep the SDK as the auth + transport layer but route the authenticated activity ourselves through a lenient `CoreActivity`; the streamer is closed in our own `_handle_message_activity` `finally` because our bridge owns dispatch) are documented in `docs/UPSTREAM_SYNC.md`. + +### Adapter ports — Telegram, Slack + +- **Telegram: native DM draft streaming** (vercel/chat#340; PR #140). DMs stream via the `sendMessageDraft` Bot API method (the draft bubble updates in place, throttled to `update_interval_ms`, default 250ms), then a regular `sendMessage` persists the final text; non-DM threads return `None` before consuming any chunks so the SDK's post+edit fallback handles groups/channels. Adds a shared `with_telegram_markdown_fallback()` retry-without-`parse_mode` path wrapping `post_message` / `edit_message` / `send_document` / `send_attachment`. +- **Slack: webhook + primitives subpaths** (vercel/chat#538, #547, #548, #555, #559; PR #139). New runtime-free `chat_sdk.adapters.slack.webhook` (and `slack.api`) subpaths for lower-level Slack request verification, signed-body reading, and Events/slash/interactive payload parsing into typed dataclasses. The adapter now verifies through the shared `verify_slack_request` / `verify_slack_signature` primitives (the inline `_verify_signature` method is removed, matching upstream); the slack package `__init__` is now lazy (PEP 562) so importing a subpath does not pull in the full adapter runtime. The new `slack/api` primitives carry SSRF/token-leak guards (`send_slack_response_url` + `fetch_slack_file` host allowlists). + +### Adapter fixes — WhatsApp, Slack, Google Chat + +- **WhatsApp: typing-indicator support** (vercel/chat#320; PR #141). `start_typing` resolves the latest inbound message id from the `ThreadHistoryCache` and posts a `typing_indicator` payload (also marking the message read); Graph API default bumped v21.0 → v25.0; `_graph_api_request` and the typing-indicator failure path raise `AdapterError` instead of `RuntimeError`. +- **Slack / Google Chat: 4.30 rendering fixes** (vercel/chat#523, #553, #573; PR #141). Includes collapsing redundant autolink formatting for Google Chat email/`mailto:` links (port of upstream `177735a`). #### Python-specific (divergence from upstream) diff --git a/CLAUDE.md b/CLAUDE.md index dbf0e1d..6ac74e4 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,7 +1,7 @@ # Claude Code Quick Reference -- chat-sdk-python ## What is this? -Python port of [Vercel Chat SDK](https://github.com/vercel/chat) (synced to upstream v4.29.0; next wave 4.30.0 is tracked separately). Multi-platform async chat framework. +Python port of [Vercel Chat SDK](https://github.com/vercel/chat) (synced to upstream v4.30.0). Multi-platform async chat framework. ## Key Commands ```bash @@ -27,6 +27,7 @@ Our version embeds the upstream Vercel Chat version: `0.{upstream_major}.{upstre - `0.4.27` = synced to upstream `4.27.0` - `0.4.27.1` = Python-only fix on top of `4.27.0` - `0.4.29` = synced to upstream `4.29.0` (upstream never tagged 4.27/4.28 as `chat@*`) +- `0.4.30` = synced to upstream `4.30.0` - `UPSTREAM_PARITY` constant in `__init__.py` = programmatic access ## Architecture @@ -109,7 +110,7 @@ will not pass CI. **Fidelity check** (`scripts/verify_test_fidelity.py`) verifies every TS `it("...")` in the mapped core files has a matching Python `def test_*()`, -pinned to `chat@4.29.0` (matches `UPSTREAM_PARITY`; upstream never tagged +pinned to `chat@4.30.0` (matches `UPSTREAM_PARITY`; upstream never tagged `chat@4.27.0`/`chat@4.28.0`). The `MAPPING` dict in that script is the authoritative scope list — extending it to the remaining unmapped `packages/chat/src/*.test.ts` files is tracked as issue #78. @@ -124,7 +125,7 @@ divergence in `docs/UPSTREAM_SYNC.md`. Before the fidelity check can run locally, clone the pinned upstream checkout (same command CI uses in `lint.yml`): ```bash -git clone --depth 1 --branch chat@4.29.0 \ +git clone --depth 1 --branch chat@4.30.0 \ https://github.com/vercel/chat.git /tmp/vercel-chat ``` Then `TS_ROOT=/tmp/vercel-chat uv run python scripts/verify_test_fidelity.py --strict`. diff --git a/README.md b/README.md index 487ea71..4f8ff40 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ Multi-platform async chat SDK for Python. Port of [Vercel Chat](https://github.com/vercel/chat). -> **Status: 0.4.29 — synced to [Vercel Chat 4.29.0](https://github.com/vercel/chat)** (`UPSTREAM_PARITY = "4.29.0"`). The 4.30 sync wave is tracked in [#135](https://github.com/Chinchill-AI/chat-sdk-python/issues/135). See [CHANGELOG.md](CHANGELOG.md). +> **Status: 0.4.30 — synced to [Vercel Chat 4.30.0](https://github.com/vercel/chat)** (`UPSTREAM_PARITY = "4.30.0"`). See [CHANGELOG.md](CHANGELOG.md). ## Why chat-sdk? diff --git a/docs/UPSTREAM_SYNC.md b/docs/UPSTREAM_SYNC.md index 95b2059..dab168f 100644 --- a/docs/UPSTREAM_SYNC.md +++ b/docs/UPSTREAM_SYNC.md @@ -16,6 +16,7 @@ Our version embeds the upstream Vercel Chat version: `0.{upstream_major}.{upstre | `0.4.27` | `4.27.0` | Synced to upstream 4.27.0 | | `0.4.27.1` | `4.27.0` | Python-only fix on top of 4.27.0 (Slack upload confirmation backport) | | `0.4.29` | `4.29.0` | Synced to upstream 4.29.0 (upstream never tagged `chat@4.27.0`/`chat@4.28.0`) | +| `0.4.30` | `4.30.0` | Synced to upstream 4.30.0 (Teams adapter migrated to the `microsoft-teams-apps` SDK, issue #93) | The `UPSTREAM_PARITY` constant in `chat_sdk/__init__.py` provides programmatic access to the upstream version this release is synced to. @@ -79,9 +80,9 @@ tests. If upstream tests lock in inconsistent behavior, choose one of: ### Test fidelity (strict mode) `scripts/verify_test_fidelity.py` runs in CI (`.github/workflows/lint.yml`) pinned -to `vercel/chat@4.29.0` (matches the `UPSTREAM_PARITY` constant in +to `vercel/chat@4.30.0` (matches the `UPSTREAM_PARITY` constant in `src/chat_sdk/__init__.py`). **CI runs `--strict`** — the repo ships at 0 -missing *for mapped core files* as of `0.4.29`. Scope is defined by the +missing *for mapped core files* as of `0.4.30`. Scope is defined by the `MAPPING` dict in the script (extending to the remaining unmapped `packages/chat/src/*.test.ts` files is tracked as issue #78). Unmapped files are not checked — tightening scope requires editing `MAPPING` and @@ -610,8 +611,8 @@ stay explicit instead of being rediscovered in code review. | Chat resolver | 3-level: explicit → ContextVar → global | Process-global singleton | See [DECISIONS.md](DECISIONS.md#why-3-level-chat-resolver) | | PostableObject history | Cached in message history with real message ID | Not cached (skips history) | Upstream gap — posted messages should appear in thread/channel history | | Teams `msteams` transport key | Stripped from action values | Not stripped | Upstream gap — SDK-injected metadata should not leak to handlers | -| Teams inbound activity routing (issue #93 PR 1) | `BridgeHttpAdapter.dispatch` feeds webhooks through the SDK `HttpServer` (JWT validation), but the adapter **overrides `app.server.on_request`** with `_dispatch_activity` instead of letting the SDK's default router run. Our callback dumps the lenient `CoreActivity` to a camelCase dict and routes by `type` to the existing handler logic. | Upstream `adapter-teams@4.30.0` registers `app.on("message" / "card.action" / …)` and lets `@microsoft/teams.apps` route via its typed dispatcher; handlers receive `ctx.activity` as a strongly-typed `IMessageActivity` etc. | The Python SDK's default `on_request` (`App._process_activity_event` → `ActivityProcessor.process_activity`) runs `ActivityTypeAdapter.validate_python` (strict per-activity validation — `recipient`/`id` required) **and** a live `api.users.token.get` network call inside `_build_context` before any handler fires. Minimal serverless webhook payloads (and the adapter's dict-based handler logic) can't survive strict validation, and the token fetch would make an unwanted outbound call per inbound activity. We keep the SDK as the **auth + transport** layer (JWT genuinely validated by its `TokenValidator`) but route the already-authenticated activity ourselves through the lenient `CoreActivity`, preserving the exact pre-migration handler behavior. The SDK `on_message`/`on_card_action`/… decorators are still registered for parity/forward-compat. Regression coverage: `tests/test_teams_extended.py::TestActivityTypes`, `tests/test_teams_coverage.py::TestSdkInboundAuth`, `tests/test_teams_bridge.py`. | -| Teams dialog/modal inbound (issue #93 PR 1) | `on_dialog_open` / `on_dialog_submit` are registered on the SDK App but only cache user context (no `process_modal_submit`, no task-module response) | Upstream `adapter-teams@4.30.0` `handleDialogOpen`/`handleDialogSubmit` drive `chat.processModalSubmit` + `modalToAdaptiveCard` | The pre-migration Python Teams adapter never implemented modal/dialog inbound processing, so PR 1 (inbound + auth plumbing) preserves that behavior rather than introducing new modal handling. Wiring dialogs to `chat.process_modal_submit` is tracked as a later wave of the #93 migration. | +| Teams inbound activity routing (issue #93 PR 1) | `BridgeHttpAdapter.dispatch` feeds webhooks through the SDK `HttpServer` (JWT validation), but the adapter **overrides `app.server.on_request`** with `_dispatch_activity` instead of letting the SDK's default router run. Our callback dumps the lenient `CoreActivity` to a camelCase dict and routes by `type` to the existing handler logic. | Upstream `@chat-adapter/teams@4.30.0` registers `app.on("message" / "card.action" / …)` and lets `@microsoft/teams.apps` route via its typed dispatcher; handlers receive `ctx.activity` as a strongly-typed `IMessageActivity` etc. | The Python SDK's default `on_request` (`App._process_activity_event` → `ActivityProcessor.process_activity`) runs `ActivityTypeAdapter.validate_python` (strict per-activity validation — `recipient`/`id` required) **and** a live `api.users.token.get` network call inside `_build_context` before any handler fires. Minimal serverless webhook payloads (and the adapter's dict-based handler logic) can't survive strict validation, and the token fetch would make an unwanted outbound call per inbound activity. We keep the SDK as the **auth + transport** layer (JWT genuinely validated by its `TokenValidator`) but route the already-authenticated activity ourselves through the lenient `CoreActivity`, preserving the exact pre-migration handler behavior. The SDK `on_message`/`on_card_action`/… decorators are still registered for parity/forward-compat. Regression coverage: `tests/test_teams_extended.py::TestActivityTypes`, `tests/test_teams_coverage.py::TestSdkInboundAuth`, `tests/test_teams_bridge.py`. | +| Teams dialog/modal inbound (issue #93 PR 1) | `on_dialog_open` / `on_dialog_submit` are registered on the SDK App but only cache user context (no `process_modal_submit`, no task-module response) | Upstream `@chat-adapter/teams@4.30.0` `handleDialogOpen`/`handleDialogSubmit` drive `chat.processModalSubmit` + `modalToAdaptiveCard` | The pre-migration Python Teams adapter never implemented modal/dialog inbound processing, so PR 1 (inbound + auth plumbing) preserves that behavior rather than introducing new modal handling. Wiring dialogs to `chat.process_modal_submit` is tracked as a later wave of the #93 migration. | | Fallback streaming with whitespace-only streams (non-Teams adapters) | Placeholder cleared to `" "` on final edit | Placeholder left visible (`"..."` stuck) | Upstream 4.26 guards against empty edits but leaves the placeholder stranded on the message. We issue one final `edit_message(" ")` so the placeholder disappears when no real content was produced. Teams does not route through `_fallback_stream` (DMs stream natively through the SDK `IStreamer`; group chats accumulate-and-post), so this divergence applies only to Slack / Discord / GitHub / Telegram / Google Chat / Linear / WhatsApp. | | Google Chat `` round-trip | `to_ast()` / `extract_plain_text()` parse the custom-label syntax back to a link node / bare label | `toAst()` / `extractPlainText()` leave `` as raw text (or parse the whole string as an autolink with a malformed URL) | Upstream 4.26 emits `` in `from_ast` but never taught the reverse direction to parse it. A message posted with `[label](url)` then read back through `fetch_messages` comes back as unstructured text (or worse, a link node with the full `url\|text` as its URL) in upstream. We close the round-trip via an AST placeholder substitution: each `` is extracted to a private-use sentinel, Markdown is parsed on the rest, and link nodes are injected where the sentinels landed. This avoids the Markdown parser's incomplete handling of balanced-parens link destinations, so URLs like `https://en.wikipedia.org/wiki/Foo_(bar)` round-trip intact. | | `from_json(data, adapter=X)` → `_adapter_name` | Updated to `X.name` so `to_json()` reflects the bound adapter | Kept at `json.adapterName`, so re-serialization can emit a name that no longer matches the actual adapter | Upstream TS has the same gap but only exposes it via the `fromJSON(json, adapter?)` overload. In Python we lean on this API more (explicit `chat=` / explicit `adapter=` is preferred over the singleton). We sync the name on rebind so runtime and serialize agree. | @@ -621,8 +622,8 @@ stay explicit instead of being rediscovered in code review. | Fallback streaming stream-exception capture (non-Teams adapters) | `_fallback_stream` captures exceptions from the stream iterator, flushes whatever content was already rendered, awaits `pending_edit`, and re-raises after cleanup | `try/finally` only — exception propagates immediately, `pendingEdit` is un-awaited, and the placeholder is stranded as `"..."` | Upstream leaves a hard UX failure when streams crash mid-flight (common: LLM connection drops): placeholder visible forever, orphan background task. We flush + clean up before re-raising so the caller still sees the original error and users see the partial content instead of a spinner. This divergence does not apply to Teams: Teams DMs stream natively through the SDK `IStreamer` (`_stream_via_emit`), and a non-cancel iterator exception propagates straight to the caller while the SDK closes the streamer after the handler returns. | | Slack `stream()` to a top-level DM (empty `thread_ts`) | Normalizes the empty `thread_ts` to `None` and degrades to a single accumulated `post_message` call so the streamed reply still lands (chat-sdk-python#94) | Passes the empty `thread_ts` straight to `chat.startStream` (`adapter-slack/src/index.ts` `stream()`), which Slack rejects (`invalid_thread_ts`) — the streamed DM reply is silently dropped | Top-level DM messages intentionally encode `threadTs=""` on both sides (`_handle_message_event` / `handleMessageEvent`, "matches openDM subscriptions") — that part is faithful to upstream and **not** a bug. The bug is that upstream's `stream()` never reconciled that legitimate value with `startStream`'s requirement for a non-empty `thread_ts`; `postMessage` accepts no `thread_ts` for DMs, so we degrade instead of erroring. Tracked for contribution upstream — remove this divergence once vercel/chat fixes `stream()` to handle empty-`thread_ts` DM thread ids. | | Fallback streaming final SentMessage content (non-Teams adapters) | SentMessage + final edit carry `final_content` (remend'd — inline markers auto-closed) | SentMessage + final edit carry raw `accumulated` | Narrow UX refinement. If a stream ends with an unclosed `*`/`~~`/etc., upstream ships the unclosed marker; we run `_remend` so the user sees a clean final message. Not observable in the common case where streams close their own markers. Teams DMs stream through the SDK `IStreamer` and the Teams accumulate-and-post path ships raw `accumulated` via `post_message`, matching upstream; this divergence applies only to the remaining adapters that still route through `_fallback_stream`. | -| Teams group-chat / channel streaming via accumulate-and-post | `TeamsAdapter.stream` accumulates the full text and issues a single `post_message` (SDK-backed) instead of post+edit, even for group chats and channel threads | Same (`adapter-teams@chat@4.30.0`: `if (activeStream && !activeStream.canceled) … else { accumulate; postMessage }`) — no divergence at the adapter level | Documented for clarity: the Python port matches upstream's behavior of avoiding the post+edit flicker where Teams doesn't support native streaming. The buffered fallback routes through the same SDK `App.send` path as a normal `post_message`. | -| Teams native streaming via the SDK `IStreamer` (DMs) | `TeamsAdapter._handle_message_activity` captures a Teams SDK `IStreamer` (`microsoft_teams.apps.StreamerProtocol` / `HttpStream`) for DMs via `app.activity_sender.create_stream(ref)`, registers it in `_active_streams`, and `await`s a `processing_done` gate (a wrapped `wait_until` shim) so the streamer stays alive while the handler streams. `stream()` → `_stream_via_emit` calls `stream.emit(text)` per chunk and NEVER calls `close()`; the adapter's `_handle_message_activity` `finally` calls `stream.close()` once (the lifecycle-owner role the SDK App's `process_activity` plays upstream). | `adapter-teams@chat@4.30.0` `index.ts` does exactly this: `this.activeStreams.set(threadId, ctx.stream)`, build `processingDone` + wrapped `waitUntil`, `await processingDone`, `streamViaEmit` calls `stream.emit(text)` and never `close()` (the SDK App auto-closes after the handler returns). | **No adapter-level divergence.** The only mechanical difference is the close call site: upstream lets the SDK `App` auto-close `ctx.stream` because the SDK owns dispatch; our bridge overrides `server.on_request`, so we own dispatch and reproduce the close in `_handle_message_activity`'s `finally`. The SDK `HttpStream.close` no-ops when the stream was canceled or had no content, so closing in both success and cancel paths is safe (matching the SDK App, which closes in both its success and `StreamCancelledError` branches). Cancellation is detected via `stream.canceled` (checked before each emit) and by catching `StreamCancelledError` (other exceptions re-raise). The first chunk id is captured via `on_chunk` and awaited only when text was emitted and the stream was not canceled. Replaces the prior hand-rolled wire format, the 1500ms emit throttle, and the `RawMessage.text` / `update_interval_ms` divergences (all unwound in #93 PR 3). | +| Teams group-chat / channel streaming via accumulate-and-post | `TeamsAdapter.stream` accumulates the full text and issues a single `post_message` (SDK-backed) instead of post+edit, even for group chats and channel threads | Same (`@chat-adapter/teams@4.30.0`: `if (activeStream && !activeStream.canceled) … else { accumulate; postMessage }`) — no divergence at the adapter level | Documented for clarity: the Python port matches upstream's behavior of avoiding the post+edit flicker where Teams doesn't support native streaming. The buffered fallback routes through the same SDK `App.send` path as a normal `post_message`. | +| Teams native streaming via the SDK `IStreamer` (DMs) | `TeamsAdapter._handle_message_activity` captures a Teams SDK `IStreamer` (`microsoft_teams.apps.StreamerProtocol` / `HttpStream`) for DMs via `app.activity_sender.create_stream(ref)`, registers it in `_active_streams`, and `await`s a `processing_done` gate (a wrapped `wait_until` shim) so the streamer stays alive while the handler streams. `stream()` → `_stream_via_emit` calls `stream.emit(text)` per chunk and NEVER calls `close()`; the adapter's `_handle_message_activity` `finally` calls `stream.close()` once (the lifecycle-owner role the SDK App's `process_activity` plays upstream). | `@chat-adapter/teams@4.30.0` `index.ts` does exactly this: `this.activeStreams.set(threadId, ctx.stream)`, build `processingDone` + wrapped `waitUntil`, `await processingDone`, `streamViaEmit` calls `stream.emit(text)` and never `close()` (the SDK App auto-closes after the handler returns). | **No adapter-level divergence.** The only mechanical difference is the close call site: upstream lets the SDK `App` auto-close `ctx.stream` because the SDK owns dispatch; our bridge overrides `server.on_request`, so we own dispatch and reproduce the close in `_handle_message_activity`'s `finally`. The SDK `HttpStream.close` no-ops when the stream was canceled or had no content, so closing in both success and cancel paths is safe (matching the SDK App, which closes in both its success and `StreamCancelledError` branches). Cancellation is detected via `stream.canceled` (checked before each emit) and by catching `StreamCancelledError` (other exceptions re-raise). The first chunk id is captured via `on_chunk` and awaited only when text was emitted and the stream was not canceled. Replaces the prior hand-rolled wire format, the 1500ms emit throttle, and the `RawMessage.text` / `update_interval_ms` divergences (all unwound in #93 PR 3). | | Teams streaming throttle / Bot Framework wire format ownership | The SDK `HttpStream` owns the entire Bot Framework streaming wire format (`streamType`/`streamSequence`/`streamId`), the inter-flush throttle, and 429 retry. We hand it text via `emit()` and read back the assigned id via `on_chunk`. | Same — `@microsoft/teams.apps`'s `IStreamer` owns all of this in the JS SDK. | **THROTTLE PARITY (verified against the installed SDK source — `microsoft-teams-apps==2.0.13.4`):** the SDK throttles and is 429-safe, so we don't regress to rate-limit errors: (1) `http_stream.py:266` — after a flush, if more is queued, the next flush is scheduled via `call_later(0.5, …)`, i.e. a 500ms inter-flush delay (the module docstring at `http_stream.py:39-41` states this is "to ensure we dont hit API rate limits with Microsoft Teams"); (2) `http_stream.py:283,290` — `add_stream_update(self._index)` stamps the Bot Framework `streamSequence` and `self._index` increments per stream activity; (3) `http_stream.py:285-288` — each chunk send goes through `retry(..., RetryOptions(max_delay=4.0, max_attempts=8))`, so transient 429s are retried with backoff; (4) `http_stream.py:180-201` — `close()` waits for the queue to drain (`_wait_for_id_and_queue`) and the final `add_stream_final()` send also goes through `retry()`. **A LIVE Teams check (streaming a real long response without a 429) is out of scope for this build and is flagged for the reviewers/maintainer.** | | Teams divider rendering | `card_to_adaptive_card` hoists `separator: True` onto the next sibling (or emits a non-empty Container for a trailing divider) | `convertDividerToElement` emits an empty `Container` with `separator: True` | Upstream shares the same bug: Microsoft Teams renders an empty Container at zero height, so the separator line is effectively invisible. Python port fixes locally (issue #45) rather than blocking on upstream. | | `SlackAdapter.current_token` / `current_token_async` / `current_client` | Public accessors that return the request-context-bound token and a preconfigured `AsyncWebClient`. `current_token` (sync `@property`) reads the cache; `current_token_async` (async method) invokes the resolver on demand for callable `bot_token` configs used outside `handle_webhook`. | Not exposed (`getToken()` is private on the TS `SlackAdapter`) | Python-only addition (issue #47). Downstream code that calls Slack Web APIs from inside a handler — email resolution, user profile fetches, reaction bookkeeping — otherwise depends on underscore-prefixed helpers. The async variant is required because the sync `current_token` cannot drive an async resolver (see `bot_token` resolver invocation site row). | @@ -658,7 +659,7 @@ stay explicit instead of being rediscovered in code review. | Discord Gateway WebSocket | HTTP interactions only | Both HTTP and Gateway | Gateway requires persistent connection | | Discord gateway-only interactions (vercel/chat#490) | Handled on the forwarded-event surface: a `GATEWAY_INTERACTION_CREATE` envelope (raw INTERACTION_CREATE dispatch payload in `data`) is deferred via `POST /interactions/{id}/{token}/callback` (type 5 slash / type 6 component) and routed through the existing HTTP interaction handlers; a malformed forward missing `id`/`token` is logged and skipped | Upstream handles `Events.InteractionCreate` directly on the resident discord.js client via `deferReply()`/`deferUpdate()`; the upstream forwarder never forwards interactions | Python has no resident Gateway client (row above), so gateway-only deployments run an external listener shim that forwards raw dispatch payloads (`x-discord-gateway-token`). Observable wire behavior is identical — same callback REST calls, same handler routing, same `@original` deferred-response resolution. | | Teams `User-Agent: Vercel.ChatSDK` outbound header | Not set on `aiohttp` calls | Propagated by `botbuilder` 2.0.8 | Python Teams adapter doesn't use `botbuilder` (raw `aiohttp`). Upstream's vercel/chat#415 was a JS-only `botbuilder` SDK bump that flipped `X-User-Agent` → `User-Agent`. No equivalent dependency to bump on the Python side. Setting a `User-Agent` on the ~9 outbound `aiohttp` call sites would be a defense-in-depth nice-to-have; deferred to a follow-up. | -| Teams adapter on `microsoft-teams-apps` (official MS Python SDK) | Hand-rolled Bot Framework REST + JWT verification (see the transitional native-streaming rows above) | `@microsoft/teams.apps` owns the wire format, throttling, and activity routing | **Explicitly deferred to the 0.4.30 cycle** (issue #93). The Python SDK only went GA 2026-05-01; the migration is a 4–6 day restructuring of `adapter.py` (~2,300 LOC) + 6 test files and was too large to land inside the 4.29 wave's tail. The 3.12 floor bump (#111) — the migration's prerequisite — already landed in 0.4.29. | +| Teams adapter on `microsoft-teams-apps` (official MS Python SDK) | Inbound webhook + JWT auth, outbound send/edit/delete/typing, and native DM streaming all delegate to the official `microsoft-teams-apps` SDK `App`; Graph reads stay hand-rolled over `aiohttp` | `@microsoft/teams.apps` owns the wire format, throttling, and activity routing | **Delivered in 0.4.30** (issue #93, PRs 1–4). The migration shipped as four PRs: inbound + auth (#143), outbound (#144), native streaming via the SDK `IStreamer` (#145), and this release cut. The 3.12 floor bump (#111) — the migration's prerequisite — landed in 0.4.29. The residual adapter-level divergences (we keep the SDK as auth + transport but route the authenticated activity ourselves; close the streamer in our own `finally` because our bridge owns dispatch) are documented in the Teams divergence rows above. Graph stays hand-rolled (no `msgraph-sdk` / `[graph]` extra). | | Telegram `get_user().is_bot` | Always `False` (matches upstream — `getChat` does not expose `is_bot`) | Always `false` (same caveat documented in upstream code comment) | The Telegram Bot API's `getChat` endpoint does not surface the `is_bot` field that's available on the `User` object inside incoming `Message` updates. Callers needing bot detection must use `message.author.is_bot` from webhooks instead of `chat.get_user(...).is_bot`. | | WhatsApp `get_user` | Raises `ChatNotImplementedError` (`Chat.get_user` translates to "does not support get_user") | Not implemented upstream either (no `getUser` on the WhatsApp adapter) | WhatsApp Cloud API has no user lookup endpoint — phone numbers are the only stable identifier and there's no equivalent of `users.info` exposed to business apps. Documented explicitly so callers don't expect parity with Slack/Teams/Discord. | diff --git a/pyproject.toml b/pyproject.toml index 5a17630..9c57f98 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "chat-sdk" -version = "0.4.29" +version = "0.4.30" description = "Multi-platform async chat SDK for Python — port of Vercel Chat" keywords = [ "chat", diff --git a/scripts/fidelity_baseline.json b/scripts/fidelity_baseline.json index a4e46fe..10fc99d 100644 --- a/scripts/fidelity_baseline.json +++ b/scripts/fidelity_baseline.json @@ -1,7 +1,7 @@ { - "_comment": "Ratchet-down baseline for scripts/verify_test_fidelity.py. This repo ships at strict fidelity for mapped core files (0 missing) against chat@4.26.0, so the baseline is empty. Scope: the MAPPING dict in scripts/verify_test_fidelity.py is the authoritative list of TS files checked; it currently covers 8 of the 17 packages/chat/src/*.test.ts files. Default CI mode runs --strict via .github/workflows/lint.yml; this file is retained for local workflows that want to opt back into baseline mode (e.g. during an upstream sync where several ports land in flight). To baseline genuinely-divergent tests, run scripts/verify_test_fidelity.py --update-baseline after documenting the divergence in docs/UPSTREAM_SYNC.md.", - "ts_parity": "chat@4.29.0", - "total_ts_tests": 731, + "_comment": "Ratchet-down baseline for scripts/verify_test_fidelity.py. This repo ships at strict fidelity for mapped core files (0 missing) against chat@4.30.0, so the baseline is empty. Scope: the MAPPING dict in scripts/verify_test_fidelity.py is the authoritative list of TS files checked; it currently covers 13 of the packages/chat/src/*.test.ts files. Default CI mode runs --strict via .github/workflows/lint.yml; this file is retained for local workflows that want to opt back into baseline mode (e.g. during an upstream sync where several ports land in flight). To baseline genuinely-divergent tests, run scripts/verify_test_fidelity.py --update-baseline after documenting the divergence in docs/UPSTREAM_SYNC.md.", + "ts_parity": "chat@4.30.0", + "total_ts_tests": 732, "total_missing": 0, "missing": {} } diff --git a/scripts/verify_test_fidelity.py b/scripts/verify_test_fidelity.py index e36a133..a18bdf6 100644 --- a/scripts/verify_test_fidelity.py +++ b/scripts/verify_test_fidelity.py @@ -14,7 +14,7 @@ ``--strict`` is the current CI contract (see ``.github/workflows/lint.yml``): the baseline is ignored and any missing translation — or a missing upstream checkout — fails the build. This repo ships at strict fidelity for mapped -core files (0 missing) against ``chat@4.29.0``. The ``MAPPING`` dict below +core files (0 missing) against ``chat@4.30.0``. The ``MAPPING`` dict below is the authoritative scope list (extending it to the remaining unmapped ``packages/chat/src/*.test.ts`` files is tracked as issue #78). @@ -317,7 +317,7 @@ def write_baseline(path: Path, all_missing: dict[str, list], total_ts: int) -> N current_parity = _current_parity_tag() payload = { "_comment": existing_comment if existing_comment is not None else _DEFAULT_BASELINE_COMMENT, - "ts_parity": current_parity if current_parity is not None else "chat@4.29.0", + "ts_parity": current_parity if current_parity is not None else "chat@4.30.0", "total_ts_tests": total_ts, "total_missing": sum(len(v) for v in all_missing.values()), "missing": { @@ -446,7 +446,7 @@ def main() -> int: print(f" - {path}") print( "\nClone the upstream repo at the pinned parity tag, e.g.:\n" - " git clone --depth 1 --branch chat@4.29.0 " + " git clone --depth 1 --branch chat@4.30.0 " "https://github.com/vercel/chat.git /tmp/vercel-chat\n" "then re-run with TS_ROOT=/tmp/vercel-chat." ) diff --git a/src/chat_sdk/__init__.py b/src/chat_sdk/__init__.py index 8a7f2aa..d630a4f 100644 --- a/src/chat_sdk/__init__.py +++ b/src/chat_sdk/__init__.py @@ -225,7 +225,7 @@ ) # The upstream Vercel Chat version this release is synced to. -UPSTREAM_PARITY = "4.29.0" +UPSTREAM_PARITY = "4.30.0" __all__ = [ "UPSTREAM_PARITY", diff --git a/src/chat_sdk/adapters/teams/adapter.py b/src/chat_sdk/adapters/teams/adapter.py index 0486641..29b4fe7 100644 --- a/src/chat_sdk/adapters/teams/adapter.py +++ b/src/chat_sdk/adapters/teams/adapter.py @@ -96,7 +96,7 @@ def _to_app_options(config: TeamsAdapterConfig) -> dict[str, Any]: """Convert :class:`TeamsAdapterConfig` (public API) to Teams SDK ``AppOptions``. Python port of ``packages/adapter-teams/src/config.ts`` ``toAppOptions`` - (synced to ``adapter-teams@chat@4.30.0``). Maps our public + (synced to ``@chat-adapter/teams@4.30.0``). Maps our public ``app_id``/``app_password``/``app_tenant_id``/``federated``/``app_type``/ ``api_url`` fields onto the SDK's ``client_id``/``client_secret``/ ``tenant_id``/``managed_identity_client_id``/``service_url`` keys, reading @@ -194,7 +194,7 @@ def _handle_teams_error(error: Any, operation: str) -> NoReturn: """Convert Teams SDK / Bot Framework errors to adapter errors and raise. Python port of ``packages/adapter-teams/src/errors.ts`` ``handleTeamsError`` - (synced to ``adapter-teams@chat@4.30.0``). Maps the error onto our taxonomy: + (synced to ``@chat-adapter/teams@4.30.0``). Maps the error onto our taxonomy: - ``401`` → :class:`AuthenticationError` - ``403`` (or a "permission" message) → :class:`AdapterPermissionError` @@ -789,7 +789,7 @@ async def _handle_message_activity( # handler is done and the streamer can be closed. # # Mirrors upstream ``handleMessageActivity`` in - # ``packages/adapter-teams/src/index.ts`` (``adapter-teams@chat@4.30.0``): + # ``packages/adapter-teams/src/index.ts`` (``@chat-adapter/teams@4.30.0``): # ``this.activeStreams.set(threadId, ctx.stream)`` → build # ``processingDone`` + wrapped ``waitUntil`` → ``await processingDone``. # Upstream lets the Teams SDK ``App`` auto-close ``ctx.stream`` after the @@ -1690,7 +1690,7 @@ async def stream( streaming only in 1:1 chats. Mirrors upstream ``stream`` in - ``packages/adapter-teams/src/index.ts`` (``adapter-teams@chat@4.30.0``): + ``packages/adapter-teams/src/index.ts`` (``@chat-adapter/teams@4.30.0``): delegate to ``streamViaEmit`` when ``activeStream && !activeStream.canceled``, else accumulate and ``postMessage``. """ @@ -1749,7 +1749,7 @@ async def _stream_via_emit( delivered (empty stream, or canceled before the first flush). Mirrors upstream ``streamViaEmit`` in - ``packages/adapter-teams/src/index.ts`` (``adapter-teams@chat@4.30.0``). + ``packages/adapter-teams/src/index.ts`` (``@chat-adapter/teams@4.30.0``). """ from microsoft_teams.apps import StreamCancelledError diff --git a/src/chat_sdk/adapters/teams/bridge.py b/src/chat_sdk/adapters/teams/bridge.py index bfa51ac..7a7445d 100644 --- a/src/chat_sdk/adapters/teams/bridge.py +++ b/src/chat_sdk/adapters/teams/bridge.py @@ -1,7 +1,7 @@ """Bridge between framework-agnostic webhooks and the Microsoft Teams SDK. Python port of ``packages/adapter-teams/src/bridge-adapter.ts`` (synced to -upstream ``adapter-teams@chat@4.30.0``). +upstream ``@chat-adapter/teams@4.30.0``). The :class:`BridgeHttpAdapter` implements the Teams SDK's :class:`~microsoft_teams.apps.http.adapter.HttpServerAdapter` protocol. Rather From 0008f42f48303d5df31f881733678e7101255252 Mon Sep 17 00:00:00 2001 From: patrick-chinchill Date: Thu, 18 Jun 2026 15:57:25 -0700 Subject: [PATCH 2/5] docs(release): clarify 0.4.30 core-parity wording + finish label normalization MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Review fast-follow for PR 4. The CHANGELOG now states the mapped core is content-identical *between the chat@4.29.0 and chat@4.30.0 upstream tags* (verified: thread.ts/types.ts/thread.test.ts and the full packages/chat/src tree are byte-identical) — the prior 'unchanged from 4.29.0' phrasing was accurate but misread as a claim about our code. Also sweeps the two remaining old-style `adapter-teams@chat@4.30.0` labels in the Teams test docstrings to the canonical `@chat-adapter/teams@4.30.0` npm tag, completing the normalization the PR's scope called for. No logic change. --- CHANGELOG.md | 2 +- tests/test_teams_extended.py | 2 +- tests/test_teams_native_streaming.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cced30e..81381e7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,7 @@ ## 0.4.30 -Synced to upstream `vercel/chat@4.30.0`. The mapped core (`packages/chat/src`) is byte-for-byte unchanged from `4.29.0`, so this wave is all adapter work: a **new Twilio adapter**, a **Telegram native-streaming** port, a **Slack primitives-subpath** wave, a batch of **WhatsApp / Slack / Google Chat** fixes, and the headline — the **Teams adapter migration to the official `microsoft-teams-apps` SDK** (issue #93, delivered across four PRs). Sets `UPSTREAM_PARITY = "4.30.0"`; CI fidelity re-pinned to `chat@4.30.0` (732/732 mapped-core tests still pass, 0 missing). +Synced to upstream `vercel/chat@4.30.0`. The mapped core (`packages/chat/src`) is content-identical between the `chat@4.29.0` and `chat@4.30.0` upstream tags, so this wave is all adapter work: a **new Twilio adapter**, a **Telegram native-streaming** port, a **Slack primitives-subpath** wave, a batch of **WhatsApp / Slack / Google Chat** fixes, and the headline — the **Teams adapter migration to the official `microsoft-teams-apps` SDK** (issue #93, delivered across four PRs). Sets `UPSTREAM_PARITY = "4.30.0"`; CI fidelity re-pinned to `chat@4.30.0` (732/732 mapped-core tests still pass, 0 missing). ### New adapter: Twilio (SMS / MMS / Voice) diff --git a/tests/test_teams_extended.py b/tests/test_teams_extended.py index 4b92bdc..171d919 100644 --- a/tests/test_teams_extended.py +++ b/tests/test_teams_extended.py @@ -908,7 +908,7 @@ class TestStream: async def test_group_chat_stream_accumulates_and_posts_single_message(self): """Group chats / channels accumulate the stream and post one message. - Mirrors upstream ``stream`` (adapter-teams@chat@4.30.0): native + Mirrors upstream ``stream`` (@chat-adapter/teams@4.30.0): native ``streamViaEmit`` is reserved for DMs (where an ``IStreamer`` exists); non-DM threads accumulate and ``postMessage`` a single message via the SDK ``App.send`` (PR 2-backed). diff --git a/tests/test_teams_native_streaming.py b/tests/test_teams_native_streaming.py index 6df3a27..a5790ba 100644 --- a/tests/test_teams_native_streaming.py +++ b/tests/test_teams_native_streaming.py @@ -1,6 +1,6 @@ """Behavioral tests for Teams native streaming via the SDK ``IStreamer``. -Port of upstream ``adapter-teams@chat@4.30.0`` ``index.ts`` +Port of upstream ``@chat-adapter/teams@4.30.0`` ``index.ts`` (``streamViaEmit`` / ``streamWithEmitter``): for DMs, the Teams adapter dispatches stream chunks through the Teams SDK ``IStreamer.emit()`` and lets the SDK ship the Bot Framework streaming wire format From 23575f07aff90615ad590660f7fa2cadac467c13 Mon Sep 17 00:00:00 2001 From: patrick-chinchill Date: Thu, 18 Jun 2026 17:10:29 -0700 Subject: [PATCH 3/5] docs(release): document 0.4.30 parity-audit wave + remaining exceptions CHANGELOG: add 'Pre-existing parity gaps closed (4.30 audit)' subsection covering the 7 ported fixes (PRs #147-#150) and a documented-exceptions note (Linear agent-sessions #151 deferred to 4.31 / #152; adapter-web and the GitHub/Linear native-client + message.subject halves stay Known Non-Parity). UPSTREAM_SYNC: - file-mapping table: add state-ioredis -> redis.py (IoRedisStateAdapter) and state-pg -> postgres.py rows (both ported + tested, were missing). - Known Non-Parity (platform gaps): add Linear agent-sessions row (#151) and an adapter-web row that splits the portable server-side WebAdapter (deferred) from the genuinely browser-only client subpaths, correcting the earlier over-broad 'browser-only; no Python runtime' note. - Correct the Google Chat file-uploads row: inbound attachment parsing is fully implemented (_create_attachment), so the gap is outbound file delivery only (post_message still logs 'not yet supported'). --- CHANGELOG.md | 11 +++++++++++ docs/UPSTREAM_SYNC.md | 6 +++++- 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 81381e7..7a8c64d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -33,6 +33,17 @@ The residual adapter-level divergences (we keep the SDK as the auth + transport - **Twilio media-download SSRF guard.** `rehydrate_attachment` validates the rehydrated media URL (https + Twilio-owned host) inside the download closure before forwarding the account SID / auth token as HTTP Basic, where upstream `fetchTwilioMedia` GETs the URL blindly. Folded into the existing `rehydrate_attachment` URL allowlist non-parity row (Slack / Teams / Google Chat / **Twilio**); enforces `CLAUDE.md`'s SSRF rule. Regression: `tests/test_twilio_adapter.py::TestRehydrateAttachment::test_media_downloader_refuses_untrusted_hosts`. +### Pre-existing parity gaps closed (4.30 audit) + +A pre-ship parity audit (the CI fidelity check covers only the mapped CORE `packages/chat` tests, never the adapters) surfaced 9 gaps present since 0.4.29 — the `chat@4.29.0` and `chat@4.30.0` tags are content-identical for the mapped core — and the 7 portable ones were closed in this wave: + +- **Google Chat: clear `cardsV2` on edit-to-plain-text** (PR #148). BUG — editing a card message down to plain text left the old card stranded on the message; `edit_message` now sends an explicit empty `cardsV2` so the card is dropped (streaming finalization is the common trigger). Plus **`Select` / `RadioSelect` → `selectionInput` widgets**: these card elements now render as Google Chat `selectionInput` widgets and the selected option is read back from `formInputs`. +- **Teams: ChoiceSet auto-submit fan-out** (PR #149). BUG / contract break — an Adaptive Card `Action.Submit` carrying multiple input keys now fires one `process_action` per input key, so each `on_action(input_key)` handler runs, instead of a single `__auto_submit` dispatch that no handler matched. Plus **`list_threads` / `post_channel_message`** (were raising `ChatNotImplementedError`, now implemented) and **`api_url` / `TEAMS_API_URL`** for a custom Bot Framework endpoint (GCC-High / sovereign cloud). +- **`api_url` custom-endpoint config across Slack / Discord / GitHub / Linear** (PR #150). Custom base URLs for GovSlack / Enterprise Grid / GitHub Enterprise / self-hosted Linear; an empty string falls back to the default, matching upstream's truthy-spread default. Plus **GitHub public `get_installation_id()`**. +- **Core: root `chat_sdk` re-exports the deprecated `chat/ai` type aliases** (PR #147). `AiMessage`, `AiMessagePart`, `ToAiMessagesOptions`, … resolve from the package root again (they had moved to `chat_sdk.ai`); `chat_sdk.ai` stays the canonical home. + +**Documented exceptions.** 0.4.30 matches `chat@4.30.0` *with documented exceptions*. The **Linear agent-sessions** surface (#151) is the largest single gap from the audit and is deferred to the 4.31 wave. `adapter-web`, plus the GitHub / Linear native-client (`octokit` / `linearClient`) and `message.subject` halves, remain documented Known Non-Parity in `docs/UPSTREAM_SYNC.md`. The 4.31 wave is tracked in #152. + ## 0.4.29 (2026-06-12) Synced to upstream `vercel/chat@4.29.0` (release commit `6581d31`, May 18 2026; upstream never tagged `chat@4.27.0`/`chat@4.28.0`). Headlines: **Meta Messenger adapter** (9th platform), **`chat/ai` tool factories** (`create_chat_tools`), **`callback_url` on buttons and modals**, **Transcripts API + `thread_history` rename**, **`burst` concurrency strategy**, a Slack feature wave (verifier precedence flip, external installation providers, native `markdown_text`, `web_client`), the upstream adapter-hardening security pass, and a Python floor bump to 3.12. Sets `UPSTREAM_PARITY = "4.29.0"`; CI fidelity re-pinned to `chat@4.29.0`. diff --git a/docs/UPSTREAM_SYNC.md b/docs/UPSTREAM_SYNC.md index dab168f..cf7a2bc 100644 --- a/docs/UPSTREAM_SYNC.md +++ b/docs/UPSTREAM_SYNC.md @@ -194,6 +194,8 @@ The Python module layout mirrors the TS package layout: | `packages/adapter-slack/src/index.ts` | `src/chat_sdk/adapters/slack/adapter.py` | | `packages/state-memory/src/index.ts` | `src/chat_sdk/state/memory.py` | | `packages/state-redis/src/index.ts` | `src/chat_sdk/state/redis.py` | +| `packages/state-ioredis/src/index.ts` | `src/chat_sdk/state/redis.py` (`IoRedisStateAdapter`) | +| `packages/state-pg/src/index.ts` | `src/chat_sdk/state/postgres.py` | ## What to Port vs What to Adapt @@ -655,13 +657,15 @@ stay explicit instead of being rediscovered in code review. |------|--------|-----|-----------| | Teams certificate auth | Rejected (app password only) | Supported | Low demand; can add later | | Teams `dialog_open_timeout_ms` config | Not implemented | Configurable | Low demand | -| Google Chat file uploads | Ignored in message parse | Supported | API complexity; can add later | +| Google Chat outbound file delivery | Files attached to an outbound message are dropped — `post_message` logs "File uploads are not yet supported for Google Chat" and posts text/cards only (no media upload). Inbound attachments **are** parsed: `_create_attachment` builds a full `Attachment` (type detection, `fetch_data` download closure, `fetch_metadata`) for every `message.attachment` entry. | Both directions supported (multipart media upload on send) | Outbound upload (multipart to the Media API) not yet ported; API complexity, can add later. Inbound parity is complete, so this row is outbound-only. | | Discord Gateway WebSocket | HTTP interactions only | Both HTTP and Gateway | Gateway requires persistent connection | | Discord gateway-only interactions (vercel/chat#490) | Handled on the forwarded-event surface: a `GATEWAY_INTERACTION_CREATE` envelope (raw INTERACTION_CREATE dispatch payload in `data`) is deferred via `POST /interactions/{id}/{token}/callback` (type 5 slash / type 6 component) and routed through the existing HTTP interaction handlers; a malformed forward missing `id`/`token` is logged and skipped | Upstream handles `Events.InteractionCreate` directly on the resident discord.js client via `deferReply()`/`deferUpdate()`; the upstream forwarder never forwards interactions | Python has no resident Gateway client (row above), so gateway-only deployments run an external listener shim that forwards raw dispatch payloads (`x-discord-gateway-token`). Observable wire behavior is identical — same callback REST calls, same handler routing, same `@original` deferred-response resolution. | | Teams `User-Agent: Vercel.ChatSDK` outbound header | Not set on `aiohttp` calls | Propagated by `botbuilder` 2.0.8 | Python Teams adapter doesn't use `botbuilder` (raw `aiohttp`). Upstream's vercel/chat#415 was a JS-only `botbuilder` SDK bump that flipped `X-User-Agent` → `User-Agent`. No equivalent dependency to bump on the Python side. Setting a `User-Agent` on the ~9 outbound `aiohttp` call sites would be a defense-in-depth nice-to-have; deferred to a follow-up. | | Teams adapter on `microsoft-teams-apps` (official MS Python SDK) | Inbound webhook + JWT auth, outbound send/edit/delete/typing, and native DM streaming all delegate to the official `microsoft-teams-apps` SDK `App`; Graph reads stay hand-rolled over `aiohttp` | `@microsoft/teams.apps` owns the wire format, throttling, and activity routing | **Delivered in 0.4.30** (issue #93, PRs 1–4). The migration shipped as four PRs: inbound + auth (#143), outbound (#144), native streaming via the SDK `IStreamer` (#145), and this release cut. The 3.12 floor bump (#111) — the migration's prerequisite — landed in 0.4.29. The residual adapter-level divergences (we keep the SDK as auth + transport but route the authenticated activity ourselves; close the streamer in our own `finally` because our bridge owns dispatch) are documented in the Teams divergence rows above. Graph stays hand-rolled (no `msgraph-sdk` / `[graph]` extra). | | Telegram `get_user().is_bot` | Always `False` (matches upstream — `getChat` does not expose `is_bot`) | Always `false` (same caveat documented in upstream code comment) | The Telegram Bot API's `getChat` endpoint does not surface the `is_bot` field that's available on the `User` object inside incoming `Message` updates. Callers needing bot detection must use `message.author.is_bot` from webhooks instead of `chat.get_user(...).is_bot`. | | WhatsApp `get_user` | Raises `ChatNotImplementedError` (`Chat.get_user` translates to "does not support get_user") | Not implemented upstream either (no `getUser` on the WhatsApp adapter) | WhatsApp Cloud API has no user lookup endpoint — phone numbers are the only stable identifier and there's no equivalent of `users.info` exposed to business apps. Documented explicitly so callers don't expect parity with Slack/Teams/Discord. | +| Linear agent sessions | Not ported — the ~120-reference agent-sessions surface (`parseMessageFromAgentSessionEvent`, `agentActivity`, `AgentSessionEventWebhookPayload`, `LinearAgentSessionThreadId`, …) is absent from the Python `LinearAdapter` | Full agent-sessions support (`adapter-linear` 4.27.0, `bc94f0a`): parses agent-session webhook events into messages, emits agent activity, and routes the agent-session thread id | Largest single gap from the 0.4.30 audit; pre-existing (present since 0.4.29). Deferred to the 4.31 wave — tracked in **#151**. | +| `adapter-web` (`@chat-adapter/web`) | Neither half ported. (a) The server-side `WebAdapter` is **deferred** — not yet ported; (b) the client subpaths are out of scope (see Rationale). | Two distinct things: (a) a server-side `WebAdapter` — an `Adapter` implementation serving a browser chat UI over the AI SDK UI stream protocol (`3490a8c`, vercel/chat#444); (b) React/Vue/Svelte client subpaths (`716e934`) | The `WebAdapter` (a) **is portable** to a Python server SDK (it's a standard `Adapter`) and is deferred, not excluded — a future wave can port it. The client subpaths (b) are genuinely browser-only (front-end framework bindings) and are out of scope for a Python server SDK. (Corrects the earlier over-broad "browser-only; no Python runtime" note in CHANGELOG.) | ### Serialization differences From f77fc787640ee28f2d27fbccc2ce11a0b25dc68d Mon Sep 17 00:00:00 2001 From: patrick-chinchill Date: Thu, 18 Jun 2026 22:01:42 -0700 Subject: [PATCH 4/5] docs(release): correct phantom-feature framing before PyPI cut MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CHANGELOG: - Drop false gchat file-delivery bullet (PR #112 never merged; upstream itself does not implement gchat outbound file delivery) - Fix Slack DM block-action citation to merged PR #137 (supersedes #133) - Re-point 0.4.27.1 vehicle from unmerged #120 to tag v0.4.27.1 + #117 - Drop numberless Twilio 'scaffolding PR' (only #142 exists) UPSTREAM_SYNC: - Reframe Google Chat outbound file delivery as parity (upstream also logs 'not yet supported' + media.upload TODO, index.ts:1282-1289) - Correct Teams cert-auth row: TS is not 'Supported' — config throws at startup in both SDKs (types.ts:31, config.ts:13); parity, issue #58 - Add Messenger get_user row: upstream has no getUser; raising stub is parity, Graph-API impl tracked as #132 --- CHANGELOG.md | 7 +++---- docs/UPSTREAM_SYNC.md | 5 +++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7a8c64d..336e779 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,7 @@ Synced to upstream `vercel/chat@4.30.0`. The mapped core (`packages/chat/src`) i ### New adapter: Twilio (SMS / MMS / Voice) -- **`chat_sdk.adapters.twilio`** (vercel/chat#558; PRs #142 + the scaffolding PR). Twilio Programmable Messaging adapter (10th platform): inbound message webhooks with `X-Twilio-Signature` HMAC-SHA1 verification (`hmac.compare_digest`), outbound SMS/MMS through the Messages REST API (hand-rolled over an injectable transport — no official `twilio` SDK, mirroring upstream), 1:1 DM threads keyed `twilio:{sender}:{recipient}`, plus standalone `api` / `webhook` / `voice` helpers (TwiML builders, call + transcription parsing). New extra: `chat-sdk-python[twilio]`. Imports stay lazy so the package loads without `aiohttp` installed. +- **`chat_sdk.adapters.twilio`** (vercel/chat#558; PR #142). Twilio Programmable Messaging adapter (10th platform): inbound message webhooks with `X-Twilio-Signature` HMAC-SHA1 verification (`hmac.compare_digest`), outbound SMS/MMS through the Messages REST API (hand-rolled over an injectable transport — no official `twilio` SDK, mirroring upstream), 1:1 DM threads keyed `twilio:{sender}:{recipient}`, plus standalone `api` / `webhook` / `voice` helpers (TwiML builders, call + transcription parsing). New extra: `chat-sdk-python[twilio]`. Imports stay lazy so the package loads without `aiohttp` installed. ### Teams adapter: migration to the official `microsoft-teams-apps` SDK (issue #93) @@ -80,8 +80,7 @@ Synced to upstream `vercel/chat@4.29.0` (release commit `6581d31`, May 18 2026; ### Python-only improvements - **Slack `files_upload_v2` confirmation surfaced through `post()`** (PR #117; also shipped early as 0.4.27.1). `SentMessage.raw` carries `uploaded_file_ids`; history persistence nulls `raw` to avoid storage bloat/PII. -- **Slack: DM block-action responses no longer thread** (PR #133). `_handle_block_actions` mirrors `_handle_message_event`'s DM handling so HITL button replies don't create phantom "1 reply" threads. -- **Google Chat: file delivery via multipart media upload** (PR #112). +- **Slack: DM block-action responses no longer thread** (PR #137, supersedes #133). `_handle_block_actions` mirrors `_handle_message_event`'s DM handling so HITL button replies don't create phantom "1 reply" threads. ### Not ported / deferred (documented in docs/UPSTREAM_SYNC.md) @@ -98,7 +97,7 @@ Synced to upstream `vercel/chat@4.29.0` (release commit `6581d31`, May 18 2026; ## 0.4.27.1 (2026-05-29) -Python-only point release on the 0.4.27 line (branched from `v0.4.27`; PR #120). Backports the Slack `files_upload_v2` confirmation fix (PR #117) so `SentMessage.raw` carries `uploaded_file_ids`, plus the history-persistence `raw` null-out. Shipped ahead of 0.4.29 to unblock chinchill-api's delivery-confirmation gating. +Python-only point release on the 0.4.27 line (branched from `v0.4.27`; shipped via tag `v0.4.27.1` + merged PR #117). Backports the Slack `files_upload_v2` confirmation fix (PR #117) so `SentMessage.raw` carries `uploaded_file_ids`, plus the history-persistence `raw` null-out. Shipped ahead of 0.4.29 to unblock chinchill-api's delivery-confirmation gating. ## 0.4.29a2 (2026-05-28) diff --git a/docs/UPSTREAM_SYNC.md b/docs/UPSTREAM_SYNC.md index cf7a2bc..ce35299 100644 --- a/docs/UPSTREAM_SYNC.md +++ b/docs/UPSTREAM_SYNC.md @@ -655,15 +655,16 @@ stay explicit instead of being rediscovered in code review. | Area | Python | TS | Rationale | |------|--------|-----|-----------| -| Teams certificate auth | Rejected (app password only) | Supported | Low demand; can add later | +| Teams certificate auth | Config accepted, **throws at startup** (not yet supported) | **Same** — config shape present but throws at startup, not yet supported (`adapter-teams/src/types.ts:31`, `config.ts:13`) | **Parity — neither SDK implements Teams certificate auth yet**; both carry the deprecated `certificate` config that throws at startup. Tracked as issue #58. | | Teams `dialog_open_timeout_ms` config | Not implemented | Configurable | Low demand | -| Google Chat outbound file delivery | Files attached to an outbound message are dropped — `post_message` logs "File uploads are not yet supported for Google Chat" and posts text/cards only (no media upload). Inbound attachments **are** parsed: `_create_attachment` builds a full `Attachment` (type detection, `fetch_data` download closure, `fetch_metadata`) for every `message.attachment` entry. | Both directions supported (multipart media upload on send) | Outbound upload (multipart to the Media API) not yet ported; API complexity, can add later. Inbound parity is complete, so this row is outbound-only. | +| Google Chat outbound file delivery | Inbound attachment parsing implemented — `_create_attachment` builds a full `Attachment` (type detection, `fetch_data` download closure, `fetch_metadata`) for every `message.attachment` entry. Outbound file delivery logs "File uploads are not yet supported for Google Chat" and ignores files (posts text/cards only, no media upload). | **Same** — logs "File uploads are not yet supported for Google Chat" and leaves a `media.upload` TODO (`adapter-gchat/src/index.ts:1282-1289`). | **Parity — neither SDK implements Google Chat outbound file delivery.** A Python-only enhancement attempt (PR #112) exists but is unmerged; landing it would be a divergence (upstream lacks it), gated on real need. | | Discord Gateway WebSocket | HTTP interactions only | Both HTTP and Gateway | Gateway requires persistent connection | | Discord gateway-only interactions (vercel/chat#490) | Handled on the forwarded-event surface: a `GATEWAY_INTERACTION_CREATE` envelope (raw INTERACTION_CREATE dispatch payload in `data`) is deferred via `POST /interactions/{id}/{token}/callback` (type 5 slash / type 6 component) and routed through the existing HTTP interaction handlers; a malformed forward missing `id`/`token` is logged and skipped | Upstream handles `Events.InteractionCreate` directly on the resident discord.js client via `deferReply()`/`deferUpdate()`; the upstream forwarder never forwards interactions | Python has no resident Gateway client (row above), so gateway-only deployments run an external listener shim that forwards raw dispatch payloads (`x-discord-gateway-token`). Observable wire behavior is identical — same callback REST calls, same handler routing, same `@original` deferred-response resolution. | | Teams `User-Agent: Vercel.ChatSDK` outbound header | Not set on `aiohttp` calls | Propagated by `botbuilder` 2.0.8 | Python Teams adapter doesn't use `botbuilder` (raw `aiohttp`). Upstream's vercel/chat#415 was a JS-only `botbuilder` SDK bump that flipped `X-User-Agent` → `User-Agent`. No equivalent dependency to bump on the Python side. Setting a `User-Agent` on the ~9 outbound `aiohttp` call sites would be a defense-in-depth nice-to-have; deferred to a follow-up. | | Teams adapter on `microsoft-teams-apps` (official MS Python SDK) | Inbound webhook + JWT auth, outbound send/edit/delete/typing, and native DM streaming all delegate to the official `microsoft-teams-apps` SDK `App`; Graph reads stay hand-rolled over `aiohttp` | `@microsoft/teams.apps` owns the wire format, throttling, and activity routing | **Delivered in 0.4.30** (issue #93, PRs 1–4). The migration shipped as four PRs: inbound + auth (#143), outbound (#144), native streaming via the SDK `IStreamer` (#145), and this release cut. The 3.12 floor bump (#111) — the migration's prerequisite — landed in 0.4.29. The residual adapter-level divergences (we keep the SDK as auth + transport but route the authenticated activity ourselves; close the streamer in our own `finally` because our bridge owns dispatch) are documented in the Teams divergence rows above. Graph stays hand-rolled (no `msgraph-sdk` / `[graph]` extra). | | Telegram `get_user().is_bot` | Always `False` (matches upstream — `getChat` does not expose `is_bot`) | Always `false` (same caveat documented in upstream code comment) | The Telegram Bot API's `getChat` endpoint does not surface the `is_bot` field that's available on the `User` object inside incoming `Message` updates. Callers needing bot detection must use `message.author.is_bot` from webhooks instead of `chat.get_user(...).is_bot`. | | WhatsApp `get_user` | Raises `ChatNotImplementedError` (`Chat.get_user` translates to "does not support get_user") | Not implemented upstream either (no `getUser` on the WhatsApp adapter) | WhatsApp Cloud API has no user lookup endpoint — phone numbers are the only stable identifier and there's no equivalent of `users.info` exposed to business apps. Documented explicitly so callers don't expect parity with Slack/Teams/Discord. | +| Messenger `get_user` | Raising stub (`ChatNotImplementedError`); a Graph-API-backed impl is tracked as issue #132 | No `getUser` method on the Messenger adapter | **Parity — upstream Messenger has no user-lookup method**; the Python raising stub matches. (Meta's Graph API *could* back a real implementation, unlike WhatsApp — hence #132 stays open as an enhancement.) | | Linear agent sessions | Not ported — the ~120-reference agent-sessions surface (`parseMessageFromAgentSessionEvent`, `agentActivity`, `AgentSessionEventWebhookPayload`, `LinearAgentSessionThreadId`, …) is absent from the Python `LinearAdapter` | Full agent-sessions support (`adapter-linear` 4.27.0, `bc94f0a`): parses agent-session webhook events into messages, emits agent activity, and routes the agent-session thread id | Largest single gap from the 0.4.30 audit; pre-existing (present since 0.4.29). Deferred to the 4.31 wave — tracked in **#151**. | | `adapter-web` (`@chat-adapter/web`) | Neither half ported. (a) The server-side `WebAdapter` is **deferred** — not yet ported; (b) the client subpaths are out of scope (see Rationale). | Two distinct things: (a) a server-side `WebAdapter` — an `Adapter` implementation serving a browser chat UI over the AI SDK UI stream protocol (`3490a8c`, vercel/chat#444); (b) React/Vue/Svelte client subpaths (`716e934`) | The `WebAdapter` (a) **is portable** to a Python server SDK (it's a standard `Adapter`) and is deferred, not excluded — a future wave can port it. The client subpaths (b) are genuinely browser-only (front-end framework bindings) and are out of scope for a Python server SDK. (Corrects the earlier over-broad "browser-only; no Python runtime" note in CHANGELOG.) | From 1b442bc1a244ffe9381833b90fa469e195d0ff28 Mon Sep 17 00:00:00 2001 From: patrick-chinchill Date: Thu, 18 Jun 2026 22:05:16 -0700 Subject: [PATCH 5/5] docs(changelog): clarify 0.4.30 audit-gap count (8 closed + 1 deferred = 9) --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 336e779..affda3d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -35,7 +35,7 @@ The residual adapter-level divergences (we keep the SDK as the auth + transport ### Pre-existing parity gaps closed (4.30 audit) -A pre-ship parity audit (the CI fidelity check covers only the mapped CORE `packages/chat` tests, never the adapters) surfaced 9 gaps present since 0.4.29 — the `chat@4.29.0` and `chat@4.30.0` tags are content-identical for the mapped core — and the 7 portable ones were closed in this wave: +A pre-ship parity audit (the CI fidelity check covers only the mapped CORE `packages/chat` tests, never the adapters) surfaced 9 gaps present since 0.4.29 — the `chat@4.29.0` and `chat@4.30.0` tags are content-identical for the mapped core — and all but one (Linear agent sessions, deferred to 4.31) were closed in this wave, across four PRs: - **Google Chat: clear `cardsV2` on edit-to-plain-text** (PR #148). BUG — editing a card message down to plain text left the old card stranded on the message; `edit_message` now sends an explicit empty `cardsV2` so the card is dropped (streaming finalization is the common trigger). Plus **`Select` / `RadioSelect` → `selectionInput` widgets**: these card elements now render as Google Chat `selectionInput` widgets and the selected option is read back from `formInputs`. - **Teams: ChoiceSet auto-submit fan-out** (PR #149). BUG / contract break — an Adaptive Card `Action.Submit` carrying multiple input keys now fires one `process_action` per input key, so each `on_action(input_key)` handler runs, instead of a single `__auto_submit` dispatch that no handler matched. Plus **`list_threads` / `post_channel_message`** (were raising `ChatNotImplementedError`, now implemented) and **`api_url` / `TEAMS_API_URL`** for a custom Bot Framework endpoint (GCC-High / sovereign cloud).