Skip to content

Commit 230fc37

Browse files
docs(upstream-sync): correct adapters-catalog counts (19 tests, ~25 adapters)
1 parent 5b3520a commit 230fc37

1 file changed

Lines changed: 1 addition & 1 deletion

File tree

docs/UPSTREAM_SYNC.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -687,7 +687,7 @@ stay explicit instead of being rediscovered in code review.
687687
| `ThinkingChunk` opt-in stream-input type (Python-only, default-off; supersedes PR #39) | A **separate, opt-in** dataclass — `ThinkingChunk(type="thinking", content=str)` — surfaces AI-SDK `reasoning`/`reasoning-delta` (and pydantic-ai `part_kind == "thinking"`) parts. **`StreamChunk` is NOT widened**: the canonical union stays `StreamChunk = MarkdownTextChunk \| TaskUpdateChunk \| PlanUpdateChunk` — byte-identical to upstream's three variants, so a consumer doing an exhaustive `match` over `StreamChunk` sees zero change on upgrade. `ThinkingChunk` is accepted only at the **stream-input/output boundaries** via the public alias `StreamInput = str \| StreamChunk \| ThinkingChunk` (the `Adapter.stream()` protocol signature, `from_full_stream`/`_from_full_stream` returns, `Thread._wrapped_stream`, and each receiving adapter's `stream()` signature). A producer can yield `ThinkingChunk` (opt-in) and the adapters that receive the stream type-check; code that only references `StreamChunk` never touches it. **OPT-IN, default-off**: emitted only when a caller passes `from_full_stream(stream, emit_thinking=True)` or sets the thread-level `emit_thinking=True` config; the internal `_from_full_stream` threads the same flag. With the default (`emit_thinking=False`) the normalized stream is **byte-for-byte identical** to upstream — reasoning parts are dropped and **no** `ThinkingChunk` is produced. Consumption is graceful: `Thread._handle_stream` never accumulates a `ThinkingChunk` into the posted-message text, and every adapter's stream handler skips it (Slack/Teams expose an optional `render_thinking` hook via `chat_sdk.shared.adapter_utils.maybe_render_thinking`; the text-accumulate adapters ignore it structurally). **Streaming-only — never persisted**: `Message` has no `thinking` field, `to_json()` is unchanged, and a round-tripped `Message` is byte-identical, so cross-SDK state (Redis/Postgres shared with the TS SDK) stays compatible. | Upstream `from-full-stream.ts` forwards only `text-delta` + `finish-step`; AI-SDK `reasoning`/`reasoning-delta` parts fall through and are discarded. `StreamChunk = MarkdownTextChunk \| TaskUpdateChunk \| PlanUpdateChunk` — no reasoning variant and no stream-input alias. Upstream leaves reasoning display to the AI-SDK web UI. | chinchill actively streams agent thinking to Slack/Teams but has to intercept the model stream out-of-band today because the chat-platform SDK has no path for it. This gives the SDK a first-class, opt-in one without changing any default behavior — and crucially **without widening the public `StreamChunk` union**, so consumers referencing it are unaffected. The whole design constraint is that default-off == upstream and `StreamChunk` == upstream: separate opt-in input type, opt-in emit, graceful/skip consume, zero state pollution. Regression coverage: `tests/test_thinking_chunk.py`, `tests/test_from_full_stream.py::TestThinkingOptIn`, `tests/test_types.py::TestThinkingChunk`, plus per-adapter no-crash tests in `tests/test_slack_api.py`, `tests/test_teams_native_streaming.py`, `tests/test_twilio_adapter.py`, `tests/test_messenger_api.py`. |
688688
| Linear agent-activity emit: raw GraphQL (chat@4.31 / #151 — L4) | The agent-session EMIT path (`post_message` session branch, `start_typing` session branch, `stream` → `_stream_in_agent_session` with its flush/`task_update`/`plan_update` logic, and `_parse_message_from_agent_activity`) is ported as **raw GraphQL mutations** over the existing `_graphql_query` helper, **schema-hardened against Linear's published GraphQL schema**. Mutation names: `agentActivityCreate(input: AgentActivityCreateInput!)` and `agentSessionUpdate(id: String!, input: AgentSessionUpdateInput!)`. `content` is sent inside the `AgentActivityCreateInput.content` **`JSONObject!`** scalar — so `type`/`body`/`action`/`parameter`/`result` are inline JSON fields, with the **lowercase** `AgentActivityType` enum values `"response"` / `"thought"` / `"error"` / `"action"` (confirmed lowercase, NOT PascalCase). `ephemeral: Boolean` is a sibling of `content` and is **included only when set** (absent — not `false` — on response/thought/error). The `agentActivityCreate` return selection requests only schema-valid fields (`success`, `agentActivity { id agentSession { id } sourceComment { id body parentId createdAt updatedAt url user{…} botActor{…} } }`) — `agentSessionId` is **not** a scalar field on Linear's `AgentActivity` type (the schema exposes only the relation `agentSession: AgentSession!`), so the session id is read off the nested `agentSession { id }` relation; requesting the non-existent scalar would server-reject the whole mutation under GraphQL strict selection validation. `plan` items are `{content, status:"completed"}`. `initialize` additionally captures the viewer's `organization.id` into `_default_organization_id` (mirroring upstream's `defaultOrganizationId`) for the emitted raw message's `organizationId`; on the emit path `organizationId` falls back to `""` when `_default_organization_id` is unset (no per-request installation context is plumbed — a pre-existing adapter-wide divergence from upstream, which throws `AuthenticationError` when no organization is resolvable). | Upstream `adapter-linear/src/index.ts` calls `@linear/sdk`'s `createAgentActivity({agentSessionId, content, ephemeral?})` / `updateAgentSession(id, {plan})`; the SDK owns the GraphQL document, the `AgentActivityType` enum, and the `AgentActivityPayload`/`Comment`/`sourceComment` resolution | `@linear/sdk` is TypeScript-only (no official Linear Python SDK — cf. the `linear_client` getter row), so the SDK calls are reproduced as raw GraphQL. Mutation names, the `content: JSONObject!` shape, the lowercase enum casing, and the `plan` item shape are **schema-hardened against the published schema** (`https://linear.app/developers/agent-interaction`, `https://linear.app/developers/graphql`, and the SDK's generated GraphQL documents). **Live-tenant verification pending**: the exact mutation/field names and enum casing are confirmed against the published schema/docs but have **not** been exercised against a live Linear agent-session tenant; if a future live run surfaces a casing/field mismatch (e.g. an enum the schema renders differently at runtime), update the mutation strings here. Faithful-port hazards preserved: `status ?? "Thinking..."` → `is not None` (an empty status stays `""`); `[title, output].filter(Boolean).join("\n")` → `"\n".join(x for x in [title, output] if x)` (drops `None` and `""`); `markdown.slice(...).trim()` uses the JS-`.trim()` whitespace set (`_JS_WHITESPACE`, mirroring `adapters/telegram/rich.py`), not Python's broader `str.strip()`; `if delta or force`; `ephemeral: status != "complete"`; the missing-final-flush bare `throw new Error(...)` → `RuntimeError`. Regression coverage: `tests/test_linear_agent_session_emit.py`. |
689689
| Linear agent-session fetch: raw GraphQL (chat@4.31 / #151 — L5) | The agent-session FETCH/read path (`fetch_messages` session dispatch → `_fetch_agent_session_messages`, plus the append-only guards on `edit_message`/`delete_message` and the `agentSessionId` key in `fetch_thread` metadata) is ported as **raw GraphQL queries** over the existing `_graphql_query` helper, **schema-hardened against Linear's published GraphQL schema** (`linear/packages/sdk/src/schema.graphql` @ master). Two queries: (1) `agentSession(id: String!): AgentSession!` selecting `id`, `issue { id }`, and the nullable `comment { id body parentId createdAt updatedAt url user{…} botActor{…} }` root relation; (2) `comments(filter: CommentFilter, first: Int, last: Int): CommentConnection!` filtered by `{parent: {id: {eq: rootComment.id}}}` for the children, selecting the same `Comment` sub-fields + `pageInfo { hasNextPage endCursor }`. Upstream passes ONLY `first`/`last` here — it never reads `options.cursor` — and the sibling `_fetch_issue_comments`/`_fetch_comment_thread` paths forward no cursor either, so no inbound `after` is plumbed (only `next_cursor` is RETURNED, off `pageInfo.endCursor`). **CRITICAL schema-hardening: `AgentSession` has NO scalar `issueId` field in the published schema** (it exposes only the `issue: Issue` relation alongside `comment`/`sourceComment`/`id`); upstream's `agentSession.issueId` works because `@linear/sdk`'s model derives it from the serialized object, but in raw GraphQL requesting a non-existent `issueId` field would server-reject the whole query (the L4 blocking-bug class). So the issue id is read off the `issue { id }` relation — equivalent to upstream's `agentSession.issueId ?? thread.issueId`, and the same `issueId ?? issue?.id` fallback upstream itself uses at `index.ts:959`. Pagination is direction-driven (`forward` → `first`, otherwise `last`, default limit 50); `next_cursor = endCursor if hasNextPage else None`. Each of `[rootComment, *children.nodes]` is parsed via the upstream `parseMessageFromComment(comment, issueId, agentSession.id)` semantics — reusing L4's `_raw_message_from_source_comment` (user-vs-`botActor` author resolution) + `_parse_agent_session_message` (the `parseMessage` agent-session branch), so **each message's `thread_id` encodes the comment's OWN id** (`linear:{issueId}:c:{comment.id}:s:{agentSessionId}`, NOT a single fixed thread id) and `is_mention=True`. `edit_message`/`delete_message` raise `AdapterError` with the exact upstream strings ("…append-only and cannot be edited" / "…cannot be deleted") for session threads, before any network call. | Upstream `adapter-linear/src/index.ts` calls `@linear/sdk`'s `linear.agentSession(id)` (lazy-resolving `issueId` + the `comment` relation off the SDK model) and `linear.comments({filter, first/last})`; the SDK owns the GraphQL documents and the `Comment`/author resolution. | `@linear/sdk` is TypeScript-only (no official Linear Python SDK — cf. the `linear_client` getter row), so the SDK calls are reproduced as raw GraphQL. Query names, the `agentSession(id)` shape, the root `comments(filter: CommentFilter, first/last)` connection, the `CommentFilter.parent → NullableCommentFilter.id → IDComparator.eq` chain, and every selected `AgentSession`/`Comment`/`User`/`ActorBot`/`PageInfo` field were each **verified field-by-field against the published `schema.graphql`** (this is how the absence of a scalar `AgentSession.issueId` was caught). **Live-tenant verification pending**: the query/field names are confirmed against the published schema but have **not** been exercised against a live Linear agent-session tenant; if a future live run surfaces a field mismatch, update the query strings here. Nullish hazards preserved: `agentSession.issue.id ?? thread.issue_id` and `endCursor ?? undefined` → `is not None` (NOT `or` — an empty issue id still short-circuits per `??`). Regression coverage: `tests/test_linear_agent_session_fetch.py`. |
690-
| `chat/adapters` static adapter catalog (chat@4.31.0, new `./adapters` package subpath) | Not ported | A new SDK-free metadata module (`packages/chat/src/adapters/index.ts`, +24 `test()` cases): types `EnvVar`/`EnvGroup`/`AdapterEnvSpec`/`CatalogAdapter`, an `ADAPTERS` registry of ~22 official + vendor-official adapters, and exports `ADAPTER_NAMES`/`AdapterSlug`/`getAdapter`/`isAdapterSlug`/`listPlatformAdapters`/`listStateAdapters`/`listEnvVars`/`getSecretEnvVars`. Imports no provider SDK, so it's safe for build scripts, onboarding/setup screens, and config-discovery UIs that need package + env-var metadata (incl. secret-masking flags) without loading an adapter. | **Not meaningfully portable verbatim, and not yet needed.** The catalog's spine is TypeScript-ecosystem-specific: every entry is addressed by an **npm `packageName`** (`@chat-adapter/slack`, `@kapso/chat-adapter`, …) and the registry includes ~13 **vendor-official adapters this Python SDK does not ship** (AgentPhone, Kapso, Lark/Feishu, Liveblocks, Beeper Matrix, Resend, Sendblue, ioredis, …). A 1:1 port would ship actively-misleading data to a `pip`-installed consumer (`@chat-adapter/slack` instead of `chat-sdk[slack]`; `get_adapter("kapso")` returning metadata for something uninstallable here). The only Python-applicable slice — per-platform env-var requirements + secret flags — is the same across the 9 platform + 3 state adapters we do ship, but exposing it would be a **new Python-native feature** (a divergent rewrite around `pip` extras, not a port), and no current consumer (chinchill-api configures adapters in code, not via env-var discovery) needs it. Nothing in chat-sdk's core depends on the catalog. Deferred demand-driven: a Python-native `adapters` catalog (our adapters only, `pip`-extra install names, `get_secret_env_vars` masking) can be designed against real requirements if/when a consumer needs config discovery. The unmapped test file is additionally covered by the issue #78 fidelity-scope note. |
690+
| `chat/adapters` static adapter catalog (chat@4.31.0, new `./adapters` package subpath) | Not ported | A new SDK-free metadata module (`packages/chat/src/adapters/index.ts`, 19 `test()` cases): types `EnvVar`/`EnvGroup`/`AdapterEnvSpec`/`CatalogAdapter`, an `ADAPTERS` registry of ~25 official + vendor-official adapters, and exports `ADAPTER_NAMES`/`AdapterSlug`/`getAdapter`/`isAdapterSlug`/`listPlatformAdapters`/`listStateAdapters`/`listEnvVars`/`getSecretEnvVars`. Imports no provider SDK, so it's safe for build scripts, onboarding/setup screens, and config-discovery UIs that need package + env-var metadata (incl. secret-masking flags) without loading an adapter. | **Not meaningfully portable verbatim, and not yet needed.** The catalog's spine is TypeScript-ecosystem-specific: every entry is addressed by an **npm `packageName`** (`@chat-adapter/slack`, `@kapso/chat-adapter`, …) and the registry includes ~13 **vendor-official adapters this Python SDK does not ship** (AgentPhone, Kapso, Lark/Feishu, Liveblocks, Beeper Matrix, Resend, Sendblue, ioredis, …). A 1:1 port would ship actively-misleading data to a `pip`-installed consumer (`@chat-adapter/slack` instead of `chat-sdk[slack]`; `get_adapter("kapso")` returning metadata for something uninstallable here). The only Python-applicable slice — per-platform env-var requirements + secret flags — is the same across the 9 platform + 3 state adapters we do ship, but exposing it would be a **new Python-native feature** (a divergent rewrite around `pip` extras, not a port), and no current consumer (chinchill-api configures adapters in code, not via env-var discovery) needs it. Nothing in chat-sdk's core depends on the catalog. Deferred demand-driven: a Python-native `adapters` catalog (our adapters only, `pip`-extra install names, `get_secret_env_vars` masking) can be designed against real requirements if/when a consumer needs config discovery. The unmapped test file is additionally covered by the issue #78 fidelity-scope note. |
691691
### Platform-specific gaps
692692

693693
| Area | Python | TS | Rationale |

0 commit comments

Comments
 (0)