Skip to content
24 changes: 24 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,30 @@ Narrower tasks help while you iterate:
When you bump a state module's dependencies, also run `go mod tidy` in the
matching example module; otherwise its `go.sum` goes stale.

### What Tests Must Cover

Tests check external behavior and public contracts, not private
implementation details. Keep these families covered:

- runtime construction and shutdown
- handler registration and replacement
- routing order and no-op missing handlers
- explicit subscription and unsubscribe
- direct-message implicit mention routing
- self-message filtering
- accepted, ignored, rejected, duplicate, and lock-conflict events
- state conformance across memory, Redis, Postgres, and NATS
- token-owned lock lease acquire, release, extend, expiry, and stale release
- Slack signature verification and URL verification
- Slack golden payload normalization
- thread ID construction and validation
- thread handle reconstruction
- text, Markdown, sent message, ephemeral, and ephemeral fallback posting
- typed adapter access
- documentation coverage of intentional Vercel Chat SDK differences in the
README, reference, explanation, and GoDoc (see
[Documentation](#documentation))

## Documentation

Docs follow [Diátaxis](https://diataxis.fr/); the
Expand Down
31 changes: 15 additions & 16 deletions docs/README.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
# Chat SDK Go Documentation

User-facing documentation is organized along [Diátaxis](https://diataxis.fr/):
learning-oriented tutorials, task-oriented how-to guides, information-oriented
reference, and understanding-oriented explanation.
The docs follow [Diátaxis](https://diataxis.fr/): a tutorial to learn,
how-to guides to get a task done, reference to look things up, and
explanation to understand the design.

## Tutorials

Expand All @@ -13,7 +13,7 @@ Start here if you are new to the SDK.

## How-To Guides

Task-oriented guides for people already running a bot.
For people who already have a bot running.

- [Choose a state backend](how-to/choose-a-state-backend.md) — memory, Redis,
Postgres, or NATS JetStream.
Expand All @@ -30,22 +30,21 @@ Task-oriented guides for people already running a bot.

## Reference

- [Reference](reference.md) — module layout and pkg.go.dev pointers, the
runtime's semantics by concept (construction, webhooks, routing, dispatch,
state, concurrency, messages, history, actors, adapter access),
per-adapter capability status, the examples, and the testing contract.
- [Linear agent capability gaps](linear-agent-capabilities.md) — tracked list
of Linear agent APIs the adapter does not yet wrap.
- [Reference](reference.md) — modules and packages, how the runtime behaves
(routing, dispatch, state, concurrency, messages, history), what each
adapter supports, and the runnable examples. The API itself is in the
[GoDoc](https://pkg.go.dev/github.com/coder/chat).
- [Linear agent capabilities](linear-agent-capabilities.md) — what the Linear
adapter supports today and what it does not wrap yet.

## Explanation

- [Architecture and design decisions](explanation.md) — an index over
[`CONTEXT.md`](../CONTEXT.md) (the ubiquitous language and architecture
document) and the [ADRs](adr/) that record every significant decision,
plus the design goals, the Vercel Chat SDK concept map, the non-goals, and
the intentional gaps.
- [Architecture and design decisions](explanation.md) — the model in brief,
design goals, the Vercel Chat SDK concept map, non-goals, and intentional
gaps, with an index of the [ADRs](adr/) and
[`CONTEXT.md`](../CONTEXT.md), the project's vocabulary.

## Non-User Documentation
## For Contributors

- [`CONTRIBUTING.md`](../CONTRIBUTING.md) — how to build, test, and propose
changes to this repository.
Expand Down
137 changes: 71 additions & 66 deletions docs/explanation.md
Original file line number Diff line number Diff line change
@@ -1,53 +1,14 @@
# Architecture And Design Decisions

Chat SDK Go's design is documented in two places, and this page is the index
over both. It also states the [design goals](#design-goals), maps the
project against [Vercel Chat SDK](#vercel-chat-sdk-alignment), and lists the
[non-goals](#non-goals) and [intentional gaps](#intentional-gaps).

- [`CONTEXT.md`](../CONTEXT.md) — the ubiquitous language and architecture
document. It defines every domain term precisely (with the synonyms to
avoid), states the architectural invariants as explicit relationships, and
records resolved ambiguities against upstream Vercel Chat SDK behavior.
- [`docs/adr/`](adr/) — Architecture Decision Records. Every significant
decision has one, including the decisions *not* to build something.

## Reading CONTEXT.md

If you want to understand the system, read `CONTEXT.md` top to bottom; it is
the single most information-dense document in the repository. Its `Language`
section groups the vocabulary by area — runtime lifecycle, the platform
adapter boundary, Linear session lifecycle, threads and routing, the
event/message model, dispatch and concurrency, observability, state and
history, content and formatting, and tenancy and identity. The
`Relationships` section is the closest thing to a formal specification of the
runtime's invariants, and `Flagged ambiguities` explains where and why the
design deliberately diverges from Vercel Chat SDK.

## Decision Records

| ADR | Decision | Status |
| --- | --- | --- |
| [0001](adr/0001-linear-app-actor-slice.md) | Linear app-actor slice before a full Linear adapter | Accepted |
| [0002](adr/0002-async-dispatch.md) | Deferred runtime dispatch (ack-then-work) | Accepted |
| [0003](adr/0003-slash-commands.md) | Command Events and slash command routing | Accepted |
| [0004](adr/0004-interactive-components.md) | Interaction Events and native content instead of a card DSL | Accepted |
| [0005](adr/0005-rate-limit-handling.md) | Rate-limit retry lives in adapters, with typed `RateLimited` errors | Accepted |
| [0006](adr/0006-multi-tenant-install.md) | Multi-tenant installs via app-implemented `InstallStore`; OAuth flows stay app-owned | Accepted |
| [0007](adr/0007-teams-adapter.md) | Microsoft Teams adapter approach (Bot Framework, direct HTTP) | Proposed — gated on a spike |
| [0008](adr/0008-linear-full-adapter.md) | Full Linear agent activity surface (thought/response/action/elicitation/error) plus session updates (plans, external URLs) | Accepted |
| [0009](adr/0009-message-history.md) | Message history stays application-owned; optional storage-free `HistoryReader` | Accepted |
| [0010](adr/0010-observability.md) | Optional `Observer` seam; no OpenTelemetry in core | Accepted |
| [0011](adr/0011-resumable-streaming.md) | Resumable streaming deferred from core, not foreclosed | Proposed |
| [0012](adr/0012-concurrency-strategy.md) | Concurrency strategy expansion (`drop`/`queue`/`debounce`/`concurrent`/`burst` + lock scope implemented; force/steerability names reserved) | Accepted |
| [0013](adr/0013-linear-generic-comments.md) | Linear generic issue/comment participation | Accepted |
| [0014](adr/0014-nats-state-adapter.md) | NATS JetStream state adapter | Accepted |
| [0015](adr/0015-runtime-coordination.md) | Deferred-dispatch admission bound; cross-instance coalescing rejected for now | Accepted |
This page explains why Chat SDK Go is shaped the way it is: the model in
brief, the design goals, how it maps to
[Vercel Chat SDK](#vercel-chat-sdk-alignment), and what it leaves out on
purpose ([non-goals](#non-goals) and [intentional gaps](#intentional-gaps)).
The full record is in [`CONTEXT.md`](../CONTEXT.md) and the
[ADRs](#where-the-design-is-recorded).

## The Short Version

For readers who want the model in five paragraphs:

**The runtime coordinates conversations; it does not own your product.**
`chat.Chat` verifies webhooks through adapters, normalizes platform payloads
into events, dedupes them, serializes work per thread with token-owned lock
Expand Down Expand Up @@ -92,8 +53,8 @@ diverges and documents the divergence.

Chat SDK Go follows Vercel Chat SDK's conversation semantics where they fit
Go, built outward from a production-shaped Slack slice. It is
not a TypeScript API port and does not promise full feature parity. For
readers who know Vercel Chat SDK, this is the concept-by-concept status map:
not a TypeScript API port and does not promise full feature parity. If you
know Vercel Chat SDK, this table maps each concept to its status here:

| Vercel Chat SDK concept | Chat SDK Go status |
| --- | --- |
Expand Down Expand Up @@ -127,9 +88,9 @@ history — are documented on the affected symbols' GoDoc and in the

## Non-Goals

These are deliberate design boundaries, each recorded in an ADR. Most are
permanent ownership boundaries; streaming is the one explicitly *deferred*
boundary — out of the core runtime today, not foreclosed forever.
These are deliberate boundaries, each recorded in an ADR. Most are
permanent: they mark what belongs to your application. Streaming is the
exception; it is *deferred*, out of the core today but not ruled out.

- **Streaming token transport in the core runtime** —
[ADR 0011](adr/0011-resumable-streaming.md) defers token streaming and
Expand Down Expand Up @@ -159,29 +120,25 @@ boundary — out of the core runtime today, not foreclosed forever.

## Intentional Gaps

These are not bugs; they are things the current scope deliberately does not
include:
These are smaller things the current scope leaves out on purpose. They are
not bugs.

API shape:

- no TypeScript API compatibility
- no full Vercel Chat SDK feature parity
- no multiple handlers per routing hook
- no lazy runtime initialization
- no Linear personal API key mode, and no single-install static access token
(pre-exchanged access tokens are supported through the multi-tenant
`InstallStore`)
- no Linear streaming, reactions, or Markdown conversion
- no built-in OAuth web flow: authorize/callback/token-exchange routes and
install storage are application-owned
([ADR 0006](adr/0006-multi-tenant-install.md))
- no live Slack end-to-end test in CI
- no dedicated `OnDirectMessage` hook
- no public proactive `OpenDM`, except adapter behavior needed for explicit
ephemeral fallback
- no pattern handlers
- no middleware
- no history persistence APIs: `HistoryReader` is a storage-free live
read-through, implemented by the Slack and Linear adapters
- no thread application state APIs

Messages and content:

- no edit, delete, reaction, or other outbound mutation APIs beyond what a
native interaction response needs
- no JSX cards, files, or typed Block Kit / Adaptive Card payload builders
(native Block Kit content ships as an opaque payload via
`NativeContentPoster`)
Expand All @@ -190,10 +147,58 @@ include:
- no synchronous modal `view_submission` response (modal open via
`views.open` ships; the synchronous `response_action` is incompatible with
ack-then-work and is deferred)
- no edit, delete, reaction, or other outbound mutation APIs beyond what a
native interaction response needs

State and history:

- no history persistence APIs: `HistoryReader` is a storage-free live
read-through, implemented by the Slack and Linear adapters
- no thread application state APIs

Linear:

- no Linear personal API key mode, and no single-install static access token
(pre-exchanged access tokens are supported through the multi-tenant
`InstallStore`)
- no Linear streaming, reactions, or Markdown conversion

Operations:

- no built-in OAuth web flow: authorize/callback/token-exchange routes and
install storage are application-owned
([ADR 0006](adr/0006-multi-tenant-install.md))
- no built-in HTTP server or router integrations
- no bundled metrics framework, exporters, or scrape endpoint (an optional
no-op `Observer` seam is provided; OpenTelemetry stays out of the core
import graph)
- no built-in HTTP server or router integrations
- no live Slack end-to-end test in CI
- no adapter marketplace/package conventions

## Where The Design Is Recorded

- [`CONTEXT.md`](../CONTEXT.md) is the project's vocabulary and architecture
document. Read it top to bottom to understand the system. Its `Language`
section defines every domain term (with the synonyms to avoid), grouped by
area. Its `Relationships` section is the closest thing to a formal
specification of the runtime's invariants. Its `Flagged ambiguities`
section explains where and why the design diverges from Vercel Chat SDK.
- [`docs/adr/`](adr/) holds the Architecture Decision Records. Every
significant decision has one, including decisions *not* to build
something:

| ADR | Decision | Status |
| --- | --- | --- |
| [0001](adr/0001-linear-app-actor-slice.md) | Linear app-actor slice before a full Linear adapter | Accepted |
| [0002](adr/0002-async-dispatch.md) | Deferred runtime dispatch (ack-then-work) | Accepted |
| [0003](adr/0003-slash-commands.md) | Command Events and slash command routing | Accepted |
| [0004](adr/0004-interactive-components.md) | Interaction Events and native content instead of a card DSL | Accepted |
| [0005](adr/0005-rate-limit-handling.md) | Rate-limit retry lives in adapters, with typed `RateLimited` errors | Accepted |
| [0006](adr/0006-multi-tenant-install.md) | Multi-tenant installs via app-implemented `InstallStore`; OAuth flows stay app-owned | Accepted |
| [0007](adr/0007-teams-adapter.md) | Microsoft Teams adapter approach (Bot Framework, direct HTTP) | Proposed — gated on a spike |
| [0008](adr/0008-linear-full-adapter.md) | Full Linear agent activity surface (thought/response/action/elicitation/error) plus session updates (plans, external URLs) | Accepted |
| [0009](adr/0009-message-history.md) | Message history stays application-owned; optional storage-free `HistoryReader` | Accepted |
| [0010](adr/0010-observability.md) | Optional `Observer` seam; no OpenTelemetry in core | Accepted |
| [0011](adr/0011-resumable-streaming.md) | Resumable streaming deferred from core, not foreclosed | Proposed |
| [0012](adr/0012-concurrency-strategy.md) | Concurrency strategy expansion (`drop`/`queue`/`debounce`/`concurrent`/`burst` + lock scope implemented; force/steerability names reserved) | Accepted |
| [0013](adr/0013-linear-generic-comments.md) | Linear generic issue/comment participation | Accepted |
| [0014](adr/0014-nats-state-adapter.md) | NATS JetStream state adapter | Accepted |
| [0015](adr/0015-runtime-coordination.md) | Deferred-dispatch admission bound; cross-instance coalescing rejected for now | Accepted |
67 changes: 33 additions & 34 deletions docs/how-to/choose-a-state-backend.md
Original file line number Diff line number Diff line change
@@ -1,21 +1,32 @@
# How To Choose A State Backend

Runtime state is required: the runtime stores subscribed-thread membership,
event dedupe marks, and thread lock leases in a `chat.State`. It is
coordination state, not product state — keep your application's own data in
your own database keyed by `ThreadID`.
Every bot needs a `chat.State`. The runtime keeps three things there:
which threads are subscribed, which events it has already accepted (dedupe
marks, written before any handler runs), and who holds each thread's lock. That is coordination state, not
product state: keep your application's data in your own database, keyed by
`ThreadID`.

Four implementations ship today:
## Pick One

| Backend | Module | Use for |
| Backend | Module | Use it when |
| --- | --- | --- |
| Memory | `github.com/coder/chat/state/memory` (in the core module) | Tests and local demos. Lost on restart. |
| Redis | `github.com/coder/chat/state/redis` | Production, horizontally scaled deployments. |
| Postgres | `github.com/coder/chat/state/postgres` | Production, when Postgres is already your coordination store. |
| NATS JetStream | `github.com/coder/chat/state/nats` | Production, when you already run NATS with JetStream. |
| Memory | `github.com/coder/chat/state/memory` (core module) | You are writing tests or following the tutorial. State is lost on restart. |
| Redis | `github.com/coder/chat/state/redis` | You already run Redis. |
| Postgres | `github.com/coder/chat/state/postgres` | You already run Postgres. |
| NATS JetStream | `github.com/coder/chat/state/nats` | You already run NATS with JetStream. |

Redis, Postgres, and NATS live in separate Go modules so applications that only
need core, Slack, or memory state do not pull their dependencies.
The three durable backends are equivalent for the runtime: they implement
the same token-owned lock lease and dedupe contract and pass the same
conformance suite, so any of them lets you run several bot replicas safely.
Pick the one you already operate. Redis and Postgres are tested against
real servers via Testcontainers; NATS is tested against an embedded
JetStream server.

Redis, Postgres, and NATS are separate Go modules, so an application that
uses only the core module does not pull their dependencies.

Whichever you pick, give each bot application its own namespace; see
[One namespace per bot application](#one-namespace-per-bot-application).

## Memory

Expand Down Expand Up @@ -121,25 +132,13 @@ runnable example is

## One Namespace Per Bot Application

The `Prefix`/`Namespace` options default to `chat`. If two *independent* bot
applications share one Redis, Postgres, or NATS service with the default,
their subscription, dedupe, and lock records collide — thread IDs carry
platform tenant/channel identity but no application identity, so app A
subscribing a thread can route that thread's follow-ups into app B's
`OnSubscribedMessage`, and one app's locks can suppress the other's events.
Give every bot application its own stable namespace, shared only by that
app's replicas (replicas must share the namespace — that is what makes
dedupe and locking work across them).

## How To Decide

- Writing tests or following the tutorial: use memory.
- Already running Redis: use Redis. Same for Postgres and NATS — the backends
are contract-equivalent, so pick the one you already operate.
- Running more than one bot replica: any of the durable backends works; all
three implement the same token-owned lock lease and dedupe contract, which is
what makes horizontal scaling safe.

All backends are exercised by the same conformance suite; Redis and Postgres
integration tests run against real backends via Testcontainers, and NATS tests
run against an embedded JetStream server.
The Redis `Prefix`, Postgres `Namespace`, and NATS `Prefix` options default to
`chat`. Set them.

- **Replicas of one bot share a namespace.** That is what lets them dedupe
and lock across each other.
- **Independent bots need different namespaces.** Thread IDs identify the
platform tenant and channel but not your application. If two bots share a
backend and a namespace, bot A subscribing a thread can route that
thread's follow-ups into bot B's `OnSubscribedMessage`, and one bot's locks
can suppress the other's events.
Loading
Loading