feat: add @chat-adapter/web — browser chat UI for chat-sdk bots#444
Merged
Conversation
Contributor
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Review the following changes in direct dependencies. Learn more about Socket for GitHub.
|
Return the inner task as Promise<void> instead of void so streaming adapters can await full handler completion and surface user-handler rejections at the wire level. waitUntil semantics for existing webhook adapters are unchanged — the SDK still tracks the work with errors swallowed (and logged) so platforms don't retry on handler bugs. Required by @chat-adapter/web, whose response body is the user handler's stream.
A new platform adapter that lets a chat-sdk bot serve a browser chat
UI alongside Slack/Teams/Discord/etc. without writing any client-side
glue. Speaks the AI SDK UI message stream protocol, so @ai-sdk/react's
useChat and the ai-elements component library work out of the box.
- `@chat-adapter/web` — server: createWebAdapter({ userName, getUser })
- `@chat-adapter/web/react` — client: useChat() preconfigured with
DefaultChatTransport against /api/chat (override via `api`)
Defaults that matter for v1:
- `isDM: true` — every web message routes through onDirectMessage
- `persistMessageHistory: true` — chat-sdk caches each turn in the
configured state adapter so handlers can read prior context via
thread.messages / channel.messages (no platform history API exists)
- channelId === threadId — web has no separate channel concept; this
prevents cross-conversation bleed when a single user has multiple
useChat sessions
- Native `adapter.stream` implementation pumps text-deltas straight
onto the SSE response — no post+edit fallback
Out of scope for v1: cards/JSX rendering, reactions, modals, file
uploads, edit/delete, multi-tab proactive push.
- Register the web adapter in lib/adapters.ts with a demo getUser (single shared identity — replace with NextAuth/Clerk/cookie auth in production) - Expose POST /api/chat backed by bot.webhooks.web (using next/after for waitUntil) - Add a minimal /chat page using @chat-adapter/web/react's useChat — same bot.onDirectMessage handler that powers Slack now powers the browser too Bumps `ai` to ^6.0.174 to align with @ai-sdk/react@^3 (avoids dual provider-utils versions in the workspace).
- Add an entry to adapters.json so the package shows up on /adapters - Add a globe SVG to lib/logos.tsx and wire it into the icon map - Mention the new adapter in docs/adapters.mdx
- Reject user ids containing ':' with HTTP 400 — the character would corrupt the thread-id round-trip through decodeThreadId - Skip emitting text-start/text-end in postMessage when the resolved text is empty so useChat doesn't render blank assistant bubbles - Derive the parseMessage author from raw.role so rehydrated assistant messages report the bot identity instead of "unknown" - Drop the duplicate handler-error log; chat.processMessage already logs at ERROR level - Document the actual persistMessageHistory default (true) and the state-cache rationale; promote the fetchMessages no-op rationale into its JSDoc
- Aborting request.signal mid-stream short-circuits the iterator and still writes text-end via the finally block - Non-text StreamChunks (task_update, plan_update) are dropped without emitting any delta - The SentMessage returned from thread.post matches the id used in text-start / text-end events
The docs site renders each adapter's README, so flesh out @chat-adapter/web to match the depth of @chat-adapter/slack: authentication boundary, threading semantics, streaming, persistence, React hook reference, configuration table, feature matrix, and troubleshooting.
gr2m
approved these changes
May 5, 2026
gr2m
left a comment
There was a problem hiding this comment.
Code changes look good to me! Very cool!
dancer
approved these changes
May 5, 2026
21 tasks
patrick-chinchill
pushed a commit
to Chinchill-AI/chat-sdk-python
that referenced
this pull request
Jun 12, 2026
…alignment Ports the core slice of vercel/chat#444 (processMessage returns the inner task as Promise<void>): process_message now returns the eagerly-started asyncio.Task so streaming callers can await full handler completion and observe handler exceptions; wait_until keeps swallowed-error semantics. Fidelity alignment for the chat@4.29.0 pin: - MAPPING follows upstream's ai.test.ts split into ai/messages.test.ts + ai/index.test.ts (vercel/chat#492) - tests/test_ai_tools.py: 14 tests renamed to converter-exact names so the strict checker exact-matches the upstream it() titles - new faithful ports in tests/test_chat_faithful.py: the #444 awaitable test, plus the #459/#495 queue/burst subject-rehydration tests, skipif- gated on BaseAdapter.fetch_subject so they activate when PR #131 lands https://claude.ai/code/session_013zwTcMek5rNqBTQvs2oF64
patrick-chinchill
added a commit
to Chinchill-AI/chat-sdk-python
that referenced
this pull request
Jun 18, 2026
….29 ports + cut (#134) * feat(messenger): adapter — webhook, Graph API, send/stream (PR 2 of 2) (#124) * feat(messenger): adapter — webhook, Graph API, send/stream (vercel/chat#461) Port of `packages/adapter-messenger/src/index.ts` (PR 2 of 2). Builds on the scaffolding (types/format converter/cards) added in PR #118. Includes: - `MessengerAdapter`: webhook routing (GET verification + POST events), X-Hub-Signature-256 HMAC-SHA256 verification, Graph API client backed by aiohttp, send paths (text / Generic template / Button template / text fallback), buffered streaming, postback / reaction / echo / delivery / read handling, attachment extraction with lazy download, message cache (Messenger has no history API), user-profile cache, thread / channel helpers, typed Graph-API error mapping. - `create_messenger_adapter` factory with FACEBOOK_* env fallbacks. - Exports wired through `chat_sdk.adapters.messenger`. Q1 (init-failure behavior, see #110): match `WhatsAppAdapter` — raise `ValidationError` from both the factory and the constructor when any required credential is missing, so config errors surface loudly at startup. Q3 (signature verification, see #110): pin upstream's X-Hub-Signature-256 + App Secret HMAC contract. A swappable verifier (Slack-style) would diverge from Meta's protocol with no offsetting benefit for a single-secret integration; flagged as a possible future divergence but not introduced here. Tests: `tests/test_messenger_webhook.py` (57) and `tests/test_messenger_api.py` (64) — mirrors the Telegram/WhatsApp file split. Covers signature valid/invalid/missing/wrong-algo/replay, webhook routing for all event types, postback decoding (raw + chat: prefix), card → template paths, stream buffering, truncation, error mapping, and the Q1 constructor failure path. 121 new tests, full suite: 4220 passed, 3 skipped. * fix(messenger): verify HMAC on raw bytes + URL/profile defenses (gemini review) Four follow-ups to PR #124 from Gemini's review of the Messenger adapter: - `_handle_verification` casts `request.url` to `str` before `urlparse`. Starlette/yarl `URL` objects are non-`str` and would raise. - `_get_request_body` now returns `bytes` instead of `str`. Decoding to UTF-8 and re-encoding for the HMAC step risks replacement characters (U+FFFD) for any non-UTF-8 byte sequence — that breaks signature parity with Meta's reference implementation and silently rejects legitimate webhooks. Body sources are also reordered: `body` attribute first (canonical raw bytes on Starlette/Django), `text` attribute fallback (aiohttp's str path). `is not None` everywhere so a legitimately empty `b""` body isn't skipped. - `_verify_signature(body: bytes, ...)` matches the new contract; the `body.encode("utf-8")` step is dropped — HMAC operates on the exact wire bytes Meta signed. JSON parsing at the call site still works (`json.loads` accepts both str and bytes). - `_fetch_user_profile` now `isinstance(profile, dict)` checks the Graph API response before caching. A non-dict response (None, list, unexpected shape) used to poison `_user_profile_cache` and raise `AttributeError` on the next `.get` call in `_profile_display_name`; we now fall back to the minimal `{"id": user_id}` profile and leave the cache untouched. Regression coverage (+7 tests): - `TestVerifySignature::test_verifies_raw_bytes_without_encoding_roundtrip` feeds a body with lone continuation bytes (0x80 0xff) that don't survive UTF-8 round-trip. Fails on the old decode+re-encode path. - `TestGetRequestBody` (new class, 5 cases) pins the bytes return type across all four framework-shaped inputs: bytes body attribute, str body attribute, async-callable body (Starlette/FastAPI), async-callable text (aiohttp), and missing-body → `b""`. - `test_non_dict_profile_response_falls_back` exercises both `None` and list responses, asserting the fallback display name and that the cache stays empty. Existing `TestVerifySignature` cases updated to pass `bytes` bodies, matching the new contract. `_sign` test helper accepts `bytes | str` for convenience. https://claude.ai/code/session_01FyMxQn2BEAzmwKS1GZczKj * fix(messenger): accept uppercase-hex signatures + clarify init-failure docstring (review) https://claude.ai/code/session_01FyMxQn2BEAzmwKS1GZczKj * fix(messenger): implement rehydrate_attachment for queue/debounce safety (codex) Codex P2: Messenger attachments processed under queue/debounce/burst concurrency lose their fetch_data closure during JSON serialization in the state backend. Without a rehydrate_attachment hook, dequeued handlers see attachment.fetch_data is None and cannot download the file. Mirrors WhatsAppAdapter.rehydrate_attachment (same Meta family, same queue-mode failure shape): - _extract_attachments now persists the download URL on attachment.fetch_metadata={"url": url} - New rehydrate_attachment reads that URL and rebuilds the lazy downloader via _make_attachment_downloader, reusing the shared aiohttp session. Returns the attachment unchanged when metadata is missing/incomplete (degraded mode, matches the documented "leave unchanged when no hook" behavior). No auth headers are attached by the rebuilt closure — Messenger payload URLs are signature-gated by Meta and the original _download_attachment already operated without Bearer tokens. Upstream parity: vercel/chat's TS adapter-messenger does not implement this hook because queue mode is Python-only. Tests (4 new, load-bearing): - fetch_metadata carries the URL after extraction - queue/serialize roundtrip + rehydrate restores a working downloader that hits the original URL (would fail without the hook — verified by temporary revert) - two degraded-mode pins (no metadata, metadata without url key) * fix(messenger): use is-not-None for limit/user_name/cache checks + regression tests (audit) Targeted audit follow-up — three real truthiness bugs in src/chat_sdk/adapters/messenger/adapter.py, same Port Rule #1 root pattern: 1. _paginate_messages: ``options.limit or 50`` silently swallowed an explicit ``limit=0`` and substituted 50. Switched to ``is not None``. Regression: tests/test_messenger_api.py::test_explicit_zero_limit_is_not_swallowed_to_default. 2. __init__: ``config.user_name or "bot"`` paired with ``bool(config.user_name)`` silently replaced an explicit ``user_name=""`` with the ``"bot"`` fallback AND left ``_has_explicit_user_name`` False, so ``initialize()`` would then overwrite it from ``chat.get_user_name()`` / ``/me``. Switched both sites to ``is not None`` to match upstream's ``hasExplicitUserName`` semantics. Regression: tests/test_messenger_webhook.py::test_explicit_empty_user_name_is_respected. 3. _fetch_user_profile: ``if cached:`` treated a cached ``{}`` (empty dict, falsy) as a miss and re-fetched every call. Switched to ``if cached is not None``. Theoretical (Graph API rarely returns ``{}``) but cheap to make robust. Regression: tests/test_messenger_api.py::test_empty_dict_cache_entry_is_a_hit_not_a_miss. All three regression tests are load-bearing: each asserts an outcome the old code would have produced differently. Other Meta-family adapters (Teams, Discord, Google Chat) already use the ``is not None`` pattern for the same code site. Validation: ruff check / format clean, audit_test_quality.py 0 hard failures, 4235 passed / 3 skipped (was 4232 + 3 new tests = 4235). * fix(messenger): thread echo events by recipient PSID in parse_message (codex review) For a Messenger echo event (a bot-sent message echoed back), sender.id is the Page ID and recipient.id is the user's PSID — the reverse of a normal inbound message. parse_message previously keyed the thread ID off sender.id unconditionally, so a replayed echo was threaded under messenger:<page id> instead of messenger:<user PSID>, causing fetch_messages('messenger:<PSID>') to miss the bot's own echoed message. Branch on message.is_echo so the thread ID keys off recipient.id for echoes (matching _handle_echo) and sender.id otherwise. Non-echo behavior unchanged. https://claude.ai/code/session_01FyMxQn2BEAzmwKS1GZczKj * build(messenger): add messenger extra declaring aiohttp (codex review) The Messenger adapter lazy-imports aiohttp on its runtime paths (initialize, send, profile lookup, attachment downloads), but the package ships `dependencies = []` and had no `messenger` optional-dependencies extra. A base install enabling only Messenger would raise `ModuleNotFoundError: aiohttp` the moment the adapter talks to Meta. Add `messenger = ["aiohttp>=3.9"]`, matching the sibling aiohttp adapters (telegram/whatsapp/teams/linear). The `all` extra already includes aiohttp>=3.9, so no change there. https://claude.ai/code/session_01FyMxQn2BEAzmwKS1GZczKj --------- Co-authored-by: Claude <noreply@anthropic.com> (cherry picked from commit 3ad373a) https://claude.ai/code/session_013zwTcMek5rNqBTQvs2oF64 * feat(ai): add create_chat_tools tool factory (PR 2 of 3) (#122) * feat(ai): add create_chat_tools tool factory (PR 2 of 3) (vercel/chat#492) Port of `packages/chat/src/ai/tools.ts` (plus the supporting `tools/{channels,messages,reactions,threads,users}.ts` and `types.ts`) introduced by vercel/chat#492. Builds on top of #116, which moved `chat_sdk.ai` from a single module to a package so `tools.py` has a home. `create_chat_tools(chat, preset=, require_approval=, overrides=)` returns a mapping of tool-name -> `ChatTool` dataclass holding a description, JSON-Schema-shaped `input_schema`, an async `execute` callable, and the `needs_approval` flag. Three presets (`reader` / `messenger` / `moderator`) and per-tool overrides match the upstream API surface verbatim. Individual factories (`post_message`, `add_reaction`, ...) are also exported so callers can cherry-pick. PR 3 will wire these tools into the existing handler paths; this PR adds the surface only. Validation: uv run ruff check src/ tests/ -> clean uv run ruff format --check src/ tests/ -> clean uv run python scripts/audit_test_quality.py -> 0 hard failures uv run pytest tests/ -q -> 4081 passed, 3 skipped Refs #98, #109. * fix(ai/tools): wrap ChatNotImplementedError + tighten preset check (gemini review) Four small follow-ups to PR 2 of the chat-ai port from Gemini's review: - Import `ChatNotImplementedError` alongside `ChatError`. - Wrap `fetch_channel_messages` invocation in `try/except ChatNotImplementedError` and re-raise as `ChatError` (preserves cause). The early `getattr(adapter, "fetch_channel_messages", None) is None` branch only catches missing attributes; `BaseAdapter` exposes the attribute as a stub that itself raises `ChatNotImplementedError`, so without this wrap the tool surfaces a different exception type than callers expect. Matches the `Chat.get_user` pattern. - Same wrap for `list_threads`. - `_resolve_preset_tools` now distinguishes a single `str` preset from any iterable of presets via `isinstance(preset, str)` rather than `isinstance(preset, list)`, so tuples/sets/Sequences work correctly. Two new regression tests (`test_fetch_channel_messages_wraps_not_implemented`, `test_list_threads_wraps_not_implemented`) drive an `AsyncMock` whose `side_effect` is `ChatNotImplementedError`, assert the surfaced exception is `ChatError` with the expected message, and pin the cause chain (`__cause__ is ChatNotImplementedError`). https://claude.ai/code/session_01FyMxQn2BEAzmwKS1GZczKj * fix(ai/tools): isolate per-tool input schemas to prevent mutation bleed (review) * fix(ai/tools): pass list_threads options as keyword (codex P2) `MockAdapter.list_threads` is declared as `list_threads(self, channel_id, **kwargs)`, so the tool's positional `list_method(channel_id, ListThreadsOptions(...))` raised `TypeError` for any consumer wiring `create_chat_tools` into MockAdapter (the SDK's own mock). Production adapters accept the kwarg form just as readily as the positional form, so the change is universally safe. The existing `test_list_threads_projects_summaries` used `AsyncMock(return_value=...)` which masked the TypeError by replacing the real `MockAdapter.list_threads` entirely. New `test_list_threads_uses_keyword_options` exercises the real MockAdapter and asserts the default empty result — fails on the old positional call with `TypeError`, passes after. https://claude.ai/code/session_01FyMxQn2BEAzmwKS1GZczKj * style(ai/tools): apply is-not-None idiom + hoist core import (audit nits) --------- Co-authored-by: Claude <noreply@anthropic.com> (cherry picked from commit bf66edb) https://claude.ai/code/session_013zwTcMek5rNqBTQvs2oF64 * feat(chat): process_message returns the handler task + 4.29 fidelity alignment Ports the core slice of vercel/chat#444 (processMessage returns the inner task as Promise<void>): process_message now returns the eagerly-started asyncio.Task so streaming callers can await full handler completion and observe handler exceptions; wait_until keeps swallowed-error semantics. Fidelity alignment for the chat@4.29.0 pin: - MAPPING follows upstream's ai.test.ts split into ai/messages.test.ts + ai/index.test.ts (vercel/chat#492) - tests/test_ai_tools.py: 14 tests renamed to converter-exact names so the strict checker exact-matches the upstream it() titles - new faithful ports in tests/test_chat_faithful.py: the #444 awaitable test, plus the #459/#495 queue/burst subject-rehydration tests, skipif- gated on BaseAdapter.fetch_subject so they activate when PR #131 lands https://claude.ai/code/session_013zwTcMek5rNqBTQvs2oF64 * release: 0.4.29 — version cut, CHANGELOG consolidation, fidelity re-pin, ai example - pyproject 0.4.29a2 -> 0.4.29; README status + 9-platform claims - CHANGELOG: consolidated 0.4.29 release entry (0.4.29a3 notes folded in) + backfilled 0.4.27.1 section - lint.yml upstream clone re-pinned chat@4.26.0 -> chat@4.29.0 - docs/UPSTREAM_SYNC.md: version-table rows for 0.4.27.1/0.4.29; Known Non-Parity rows for GitHub octokit / Linear linear_client getters (no Python SDK object to expose), @chat-adapter/tests kit, and the Teams microsoft-teams-apps migration deferral to 0.4.30 (issue #93) - CLAUDE.md: pin references moved to chat@4.29.0 - examples/ai_tools_example.py + README AI section (chat/ai PR 3 of 3, design #109) - 0.4.30 wave tracking issue: #135 https://claude.ai/code/session_013zwTcMek5rNqBTQvs2oF64 * feat(slack): native markdown_text for outgoing messages (vercel/chat#440) Port of upstream 3546b3f. Slack natively renders markdown via the markdown_text parameter on chat.postMessage / postEphemeral / update / scheduleMessage, so the adapter passes markdown through directly instead of converting to mrkdwn. - str / PostableRaw messages still go to text (preserves literal *). - PostableMarkdown / PostableAst go to markdown_text (12k char limit). - to_blocks_with_table, _mdast_table_to_slack_block, and the _render_with_table_blocks call sites are removed (tables now ride along in markdown_text). - SlackMarkdownConverter alias removed; use SlackFormatConverter. - render_formatted(ast) / from_ast now return standard markdown (was mrkdwn) via stringify_markdown. - response_url payloads reject markdown_text (no_text), so those render through the retained mrkdwn node renderer (to_response_url_text). - Incoming message events still arrive as mrkdwn and parse unchanged. https://claude.ai/code/session_013zwTcMek5rNqBTQvs2oF64 (cherry picked from commit f07815e327ef1f2ba8ff688b2eb593af818c4ab6) * feat(slack): external installation provider for bot token management (vercel/chat#467) Port of upstream c46fdb6. Adds SlackAdapterConfig.installation_provider (SlackInstallationProvider protocol) for multi-workspace apps using external token management (e.g. Vercel Connect). When set, the adapter bypasses internal StateAdapter storage for token lookups on incoming webhooks — the provider is authoritative (no state fallback) and read-only (set_installation / delete_installation / OAuth callback still write to internal state). Enterprise Grid support rides along: org-wide installs (is_enterprise_install) resolve by enterprise_id instead of team_id across event_callback, slash command, and interactive payload entry paths; RequestContext carries enterprise_id/is_enterprise_install; attachment fetch_metadata captures enterpriseId/isEnterpriseInstall (omitted when absent) and rehydrate_attachment routes through _resolve_token_for_team so the provider is honored after a JSON roundtrip. Composes with the existing bot_token resolver design (see docs/UPSTREAM_SYNC.md non-parity rows): a configured default bot_token (static or resolver) still selects single-workspace mode and bypasses per-installation resolution entirely; _get_token / client caches are untouched. https://claude.ai/code/session_013zwTcMek5rNqBTQvs2oF64 (cherry picked from commit ee1e30703514d8a871d7f64c0ccc17c0774a7d6d) * fix(discord): handle interactions in gateway-only mode (vercel/chat#490) Port of upstream b9b17cd. Discord sends interactions through either the Gateway or an Interactions Endpoint URL, not both — deployments without an endpoint URL receive interactions over the Gateway, and the adapter previously dropped them ("Forwarded Gateway event (no handler)"). The Python adapter is HTTP-interactions-only with a gateway-forwarder receiver, so the port lands on that surface: a forwarded GATEWAY_INTERACTION_CREATE event (raw wire-format INTERACTION_CREATE dispatch payload) is now acknowledged via the interaction callback REST endpoint — POST /interactions/{id}/{token}/callback with type 5 for slash commands / type 6 for components, the same wire calls upstream's resident discord.js handler makes via deferReply() / deferUpdate() — then routed through the existing slash-command and action handler paths. Deferred slash responses resolve exactly like HTTP ones (post_message PATCHes the @original webhook message). Callback path segments are URL-quoted (hazard #12) and defer failures are logged without dispatching the handler, matching upstream's listener-level catch. Also aligns slash-command boolean option flattening with TS String(true) — "status true", not Python str(True) "status True" — pinned by the ported upstream test. https://claude.ai/code/session_013zwTcMek5rNqBTQvs2oF64 (cherry picked from commit 458428afc383b57704e6fe7c6d8abde23235afda) * feat(chat): Transcripts API + rename message history cache to thread_history (vercel/chat#448) Port of upstream 46d183b (chat@4.29.0). Rename (with back-compat, mirroring upstream): - message_history.py -> thread_history.py; MessageHistoryCache -> ThreadHistoryCache. Old module path kept as a deprecated re-export shim. - ChatConfig.thread_history added; deprecated ChatConfig.message_history still read, thread_history wins when both are set. - Adapter.persist_thread_history added; deprecated persist_message_history still honored (either flag enables persistence). Telegram and WhatsApp adapters switch to the new flag, matching upstream. - State storage key prefix "msg-history:" is deliberately unchanged so existing persisted data is not orphaned. New Transcripts API: - transcripts.py: TranscriptsApiImpl (append/list/count/delete) keyed by a cross-platform user key, backed by StateAdapter.append_to_list. delete() writes a tombstone via append_to_list(max_length=1) because state.delete only addresses the k/v namespace on non-memory adapters. - ChatConfig.transcripts + ChatConfig.identity (IdentityResolver); the constructor raises when transcripts is set without identity. Inbound dispatch resolves message.user_key once per message via the resolver. - chat.transcripts accessor raises when not configured (fail loudly). - New types: TranscriptEntry, TranscriptsConfig, TranscriptRole, AppendInput, AppendOptions, ListQuery, CountQuery, DeleteTarget, DeleteResult, IdentityContext, IdentityResolver, DurationString. Tests: test_message_history.py renamed to test_thread_history.py (names aligned to thread-history.test.ts titles, deprecated-alias test added); chat.test.ts persistThreadHistory block ported into test_chat_faithful.py; new test_transcripts.py and test_transcripts_wiring.py port transcripts.test.ts and transcripts-wiring.test.ts 1:1. https://claude.ai/code/session_013zwTcMek5rNqBTQvs2oF64 (cherry picked from commit 765420dc4d21804b19f1d8eccddebc3a392b0e61) * feat(chat): add callback_url to buttons and modals (vercel/chat#454) https://claude.ai/code/session_013zwTcMek5rNqBTQvs2oF64 (cherry picked from commit 20a5fd10a2dd46094fdf812ff47628a12bb9d362) * sync(4.29): fidelity scope extension, divergence rows, final test ports - MAPPING: four new chat@4.29.0 core test files mapped (callback-url, thread-history, transcripts, transcripts-wiring) — 12 of 19 files now in scope; stale 4.26-era references in the script/template updated - fidelity_baseline.json regenerated at chat@4.29.0 (731 TS tests in scope, 0 missing; strict mode green) - docs/UPSTREAM_SYNC.md: Known Non-Parity rows from the port wave — GitHub octokit / Linear linear_client getters, @chat-adapter/tests kit, Teams modal-submit webhook options slice, jsx-runtime callbackUrl props, Transcripts API Python adaptations, Slack legacy mrkdwn renderer scope note, Discord gateway-only interactions surface, and the Teams microsoft-teams-apps deferral to 0.4.30 - tests/test_chat_faithful.py: the [Slash Commands] duplicate-title openModal-unsupported port (matcher counts names as a multiset) and the #459 subject-wiring port (skipif-gated on BaseAdapter.fetch_subject, activates when PR #131 lands) https://claude.ai/code/session_013zwTcMek5rNqBTQvs2oF64 * fix(types): restore pyrefly conformance — history flags off the Adapter Protocol CI's Lint & Type Check job failed on pyrefly (43 errors) while every other step was green: - persist_thread_history / persist_message_history were declared on the structural Adapter Protocol, making them REQUIRED members and breaking conformance for every adapter that satisfies the Protocol without extending BaseAdapter (the exact trap documented in PR #131). Moved to BaseAdapter only, with a NOTE on the Protocol; the SDK reads both flags via getattr. - MessengerAdapter (recovered #124 — the original cause of the failure on the recovery push) now defines get_user, mirroring BaseAdapter's ChatNotImplementedError contract; the real Graph-backed lookup remains issue #132. Also: _map_attachment_type returns the Attachment.type Literal, and the user-profile cache write is typed. - transcripts: retention_ms coerced to int (append_to_list ttl_ms contract; fractional milliseconds are meaningless). uv run pyrefly check: 0 errors (matches the origin/main baseline). https://claude.ai/code/session_013zwTcMek5rNqBTQvs2oF64 * fix(chat): await modal-context state write before opening modal `_store_modal_context` was synchronous and scheduled the state write as a fire-and-forget task before `open_modal` was awaited. With a remote (Redis/Postgres) state backend a fast modal submit could race ahead of the write and miss the stored callbackUrl/channel, silently breaking modal callback POSTs and context restoration. Make `_store_modal_context` `async` and `await` it at both modal-opening call sites (slash + action paths) before `adapter.open_modal`, matching upstream where `openModal` awaits `storeModalContext` (chat.ts :1280/:1342/:1554). Remove the now-unnecessary `asyncio.sleep(0.02)` workaround in the modal-callback test; the context is persisted deterministically by the time `open_modal` returns. https://claude.ai/code/session_013zwTcMek5rNqBTQvs2oF64 * fix(messenger): use `is not None` for config credential fallbacks Convert the `config.X or <default/env>` truthiness patterns to `config.X if config.X is not None else <default/env>` for api_version (adapter) and the app_secret / page_access_token / verify_token env fallbacks (config helpers), per the TS->Python truthiness port rule. Mirrors upstream's `??` null-coalescing (index.ts:92/931). Behaviour is unchanged for real configs; an explicit value now wins over the fallback even if it is falsy. https://claude.ai/code/session_013zwTcMek5rNqBTQvs2oF64 * fix(types): widen ChatInstance.process_message return annotation The `ChatInstance` Protocol annotated `process_message` as `-> None`, but the concrete `Chat.process_message` returns `asyncio.Task[None] | None` (it hands back the handler task so streaming callers can await it). Widen the Protocol annotation to match the implementation and upstream's interface-equals-impl typing. No runtime impact. https://claude.ai/code/session_013zwTcMek5rNqBTQvs2oF64 --------- Co-authored-by: Claude <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
add
@chat-adapter/webA new platform adapter that lets a chat-sdk bot serve a browser chat UI alongside Slack, Teams, Discord, etc. The same
bot.onDirectMessage(...)handler fires for every platform — including streamed replies viathread.post(stream).The adapter speaks the AI SDK UI message stream protocol, so
@ai-sdk/react'suseChatand theai-elementscomponent library work out of the box. No client-side glue.What's in the package
@chat-adapter/webships two subpath exports:@chat-adapter/web— server-sidecreateWebAdapter({ userName, getUser })that produces anAdapterfor theChatconstructor. Handles webhook parsing, user resolution, and streaming the response.@chat-adapter/web/react— thin client wrapper exposinguseChat()preconfigured withDefaultChatTransport. Re-exportsUIMessageandUseChatHelperstypes.How the pieces fit
isDM: trueroutes every web message throughonDirectMessage.channelIdFromThreadId === threadIdkeepschannel.messagesscoped per useChat conversation.persistMessageHistory: true(default) backfillsthread.messagesfrom state since web has no platform history API.Notable design choices
getUseris the security boundaryWeb requests come straight from a browser, so unlike the platform adapters there's no signature to verify.
getUseris the auth boundary: returningnull→ HTTP 401 and no handler runs. The adapter's job is to plug into whatever the host app already uses (NextAuth, Clerk, custom session cookies). User ids that contain the reserved:delimiter are rejected with HTTP 400 to keep the thread-id round-trip clean.Native streaming end-to-end
thread.postacceptsAsyncIterable<string | StreamChunk>and pumps deltas straight onto the SSE response body — no edit loop, no rate-limit concerns. Plays nicely with the AI SDK'sstreamText.request.signalis forwarded into the stream loop, souseChat'sstop()short-circuits the iterator on the server side.task_update/plan_updatechunks have no native v1 representation in the UI message stream and are dropped silently.chat.processMessagereturns aPromiseThe Web adapter response body is the user handler's output stream, so we need to surface handler errors to the client.
Chat.processMessagepreviously returnedvoid; it now returnsPromise<void>that rejects on handler failure. Existing webhook adapters usingoptions.waitUntilare unchanged: the SDK still tracks the work with errors swallowed (logged) so platforms don't retry on handler bugs.Message persistence defaults to
trueWeb has no platform-side history API, so
thread.messages/channel.messagesare only populated through the configured state adapter's message history cache. Defaulting totruematches the typical use case; opt out only if your handler re-derives history from the request body'smessages[]itself.v1 scope
In: text + markdown, native streaming, DM-style routing (
isDM: true), persisted message history, abort propagation viarequest.signal, useChat-compatibleuseChathook.Out (deferred to v2): cards/JSX rendering, reactions, modals, file uploads, edit/delete, multi-tab proactive push.
Example app
examples/nextjs-chatgains:/chatpage usinguseChatwithai-elementscomponents/api/chatroute delegating tobot.webhooks.webwith Next.jsafter()for trackingbotinstance — same handlers fire from Slack and from the browserTests
packages/adapter-web/src/index.test.ts— 18 tests covering construction, thread-id encoding, input validation (400/401 paths), end-to-end handler dispatch (onDirectMessagerouting, async-iterable streaming, error propagation), and directstream()coverage (abort short-circuit, dropping non-texttask_update/plan_updatechunks,SentMessage.idmatches the streamedtext-*event id).packages/chat/src/chat.test.ts— new tests for the awaitableprocessMessagecontract (resolves on success, rejects with the original error on handler failure,waitUntiltracks both).