Skip to content

Commit d460fd3

Browse files
committed
docs: restructure along Diátaxis — maturity tiers, non-goals, tutorial, how-tos
Closes #9 - README: honest adapter maturity tiers (Slack supported, Linear experimental, Teams spike), Non-Goals section with ADR rationales, Diátaxis docs index, and staleness fixes (rate-limit retry, multi-tenant installs, HistoryReader, DispatchDeferred, ConcurrencyQueue, NATS state, Linear full activity surface are all implemented and now documented as such). - docs/: Diátaxis skeleton — verified Slack bot tutorial, six how-to guides, reference (pkg.go.dev pointers + capability matrix), explanation index over CONTEXT.md and the ADRs. - docs/agents/issue-tracker.md: GitHub issues are the public source of truth; .scratch/ demoted to internal working notes.
1 parent 5c7494c commit d460fd3

12 files changed

Lines changed: 1193 additions & 64 deletions

‎README.md‎

Lines changed: 160 additions & 57 deletions
Large diffs are not rendered by default.

‎docs/README.md‎

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
# Chat SDK Go Documentation
2+
3+
User-facing documentation is organized along [Diátaxis](https://diataxis.fr/):
4+
learning-oriented tutorials, task-oriented how-to guides, information-oriented
5+
reference, and understanding-oriented explanation.
6+
7+
## Tutorials
8+
9+
Start here if you are new to the SDK.
10+
11+
- [Your first Slack bot](tutorials/slack-bot.md) — zero to a running Slack bot
12+
in under 30 minutes.
13+
14+
## How-To Guides
15+
16+
Task-oriented guides for people already running a bot.
17+
18+
- [Choose a state backend](how-to/choose-a-state-backend.md) — memory, Redis,
19+
Postgres, or NATS JetStream.
20+
- [Defer long-running work (ack-then-work)](how-to/deferred-dispatch.md) —
21+
acknowledge webhooks fast and run handlers on a detached context.
22+
- [Handle slash commands](how-to/slash-commands.md) — route Slack slash
23+
commands through `OnCommand`.
24+
- [Handle interactive components](how-to/interactive-components.md) — buttons,
25+
menus, Block Kit content, and modals.
26+
- [Install into multiple workspaces (multi-tenant)](how-to/multi-tenant-install.md) —
27+
resolve per-tenant credentials with an `InstallStore`.
28+
- [Run Linear agent sessions](how-to/linear-agent-sessions.md) — build a Linear
29+
agent with thoughts, responses, actions, elicitations, and plans.
30+
31+
## Reference
32+
33+
- [API and package reference](reference.md) — pkg.go.dev pointers, module
34+
layout, and per-adapter capability status.
35+
- [Linear agent capability gaps](linear-agent-capabilities.md) — tracked list
36+
of Linear agent APIs the adapter does not yet wrap.
37+
38+
## Explanation
39+
40+
- [Architecture and design decisions](explanation.md) — an index over
41+
[`CONTEXT.md`](../CONTEXT.md) (the ubiquitous language and architecture
42+
document) and the [ADRs](adr/) that record every significant decision.
43+
44+
## Non-User Documentation
45+
46+
- [`docs/agents/`](agents/) — instructions for coding agents working on this
47+
repository, not for SDK users.

‎docs/agents/issue-tracker.md‎

Lines changed: 23 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,35 @@
1-
# Issue tracker: Local Markdown
1+
# Issue tracker: GitHub Issues
22

3-
Issues and PRDs for this repo live as markdown files in `.scratch/`.
3+
The public source of truth for this repo's roadmap, feature requests, and bug
4+
reports is GitHub issues:
45

5-
## Conventions
6+
https://github.com/coder/chat/issues
7+
8+
Anything user-facing — planned work, accepted/rejected proposals, bug state —
9+
belongs there. Use `gh issue` to read and update it.
10+
11+
## `.scratch/` is internal working notes only
12+
13+
The `.scratch/` directory holds internal working notes: PRDs, implementation
14+
breakdowns, and drafts that agents produce while working. It is not a public
15+
roadmap, it makes no promises, and nothing in it should be treated as
16+
authoritative over GitHub issues. When a scratch note graduates into real
17+
planned work, file a GitHub issue for it.
18+
19+
### Conventions for `.scratch/`
620

721
- One feature per directory: `.scratch/<feature-slug>/`
822
- The PRD is `.scratch/<feature-slug>/PRD.md`
9-
- Implementation issues are `.scratch/<feature-slug>/issues/<NN>-<slug>.md`, numbered from `01`
10-
- Triage state is recorded as a `Status:` line near the top of each issue file (see `triage-labels.md` for the role strings)
23+
- Implementation notes are `.scratch/<feature-slug>/issues/<NN>-<slug>.md`, numbered from `01`
24+
- Triage state is recorded as a `Status:` line near the top of each file (see `triage-labels.md` for the role strings)
1125
- Comments and conversation history append to the bottom of the file under a `## Comments` heading
1226

1327
## When a skill says "publish to the issue tracker"
1428

15-
Create a new file under `.scratch/<feature-slug>/` (creating the directory if needed).
29+
Create a GitHub issue with `gh issue create`. Use `.scratch/<feature-slug>/`
30+
only for supporting working notes that are not ready to be public.
1631

1732
## When a skill says "fetch the relevant ticket"
1833

19-
Read the file at the referenced path. The user will normally pass the path or the issue number directly.
34+
If the reference is a number or URL, read the GitHub issue with
35+
`gh issue view`. If the reference is a path, read the file at that path.

‎docs/explanation.md‎

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
# Architecture And Design Decisions
2+
3+
Chat SDK Go's design is documented in two places, and this page is the index
4+
over both:
5+
6+
- [`CONTEXT.md`](../CONTEXT.md) — the ubiquitous language and architecture
7+
document. It defines every domain term precisely (with the synonyms to
8+
avoid), states the architectural invariants as explicit relationships, and
9+
records resolved ambiguities against upstream Vercel Chat SDK behavior.
10+
- [`docs/adr/`](adr/) — Architecture Decision Records. Every significant
11+
decision has one, including the decisions *not* to build something.
12+
13+
## Reading CONTEXT.md
14+
15+
If you want to understand the system, read `CONTEXT.md` top to bottom; it is
16+
the single most information-dense document in the repository. Its `Language`
17+
section groups the vocabulary by area — runtime lifecycle, the platform
18+
adapter boundary, Linear session lifecycle, threads and routing, the
19+
event/message model, dispatch and concurrency, observability, state and
20+
history, content and formatting, and tenancy and identity. The
21+
`Relationships` section is the closest thing to a formal specification of the
22+
runtime's invariants, and `Flagged ambiguities` explains where and why the
23+
design deliberately diverges from Vercel Chat SDK.
24+
25+
## Decision Records
26+
27+
| ADR | Decision | Status |
28+
| --- | --- | --- |
29+
| [0001](adr/0001-linear-app-actor-slice.md) | Linear app-actor slice before a full Linear adapter | Accepted |
30+
| [0002](adr/0002-async-dispatch.md) | Deferred runtime dispatch (ack-then-work) | Accepted |
31+
| [0003](adr/0003-slash-commands.md) | Command Events and slash command routing | Accepted |
32+
| [0004](adr/0004-interactive-components.md) | Interaction Events and native content instead of a card DSL | Accepted |
33+
| [0005](adr/0005-rate-limit-handling.md) | Rate-limit retry lives in adapters, with typed `RateLimited` errors | Accepted |
34+
| [0006](adr/0006-multi-tenant-install.md) | Multi-tenant installs via app-implemented `InstallStore`; OAuth flows stay app-owned | Accepted |
35+
| [0007](adr/0007-teams-adapter.md) | Microsoft Teams adapter approach (Bot Framework, direct HTTP) | Proposed — gated on a spike |
36+
| [0008](adr/0008-linear-full-adapter.md) | Full Linear agent activity surface (thought/action/elicitation/error/plans) | Accepted |
37+
| [0009](adr/0009-message-history.md) | Message history stays application-owned; optional storage-free `HistoryReader` | Accepted |
38+
| [0010](adr/0010-observability.md) | Optional `Observer` seam; no OpenTelemetry in core | Accepted |
39+
| [0011](adr/0011-resumable-streaming.md) | Resumable streaming deferred from core, not foreclosed | Proposed |
40+
| [0012](adr/0012-concurrency-strategy.md) | Concurrency strategy expansion (`queue` implemented; `burst`/`debounce`/`concurrent` staged) | Accepted (staged) |
41+
| [0013](adr/0013-linear-generic-comments.md) | Linear generic issue/comment participation | Accepted |
42+
| [0014](adr/0014-nats-state-adapter.md) | NATS JetStream state adapter | Accepted |
43+
44+
## The Short Version
45+
46+
For readers who want the model in five paragraphs:
47+
48+
**The runtime coordinates conversations; it does not own your product.**
49+
`chat.Chat` verifies webhooks through adapters, normalizes platform payloads
50+
into events, dedupes them, serializes work per thread with token-owned lock
51+
leases, and routes to your single-slot handlers. Everything your product
52+
stores — transcripts, user records, workflow state — lives in your database,
53+
keyed by the opaque `ThreadID`.
54+
55+
**Adapters own the platform boundary.** Signature verification, payload
56+
normalization, outbound rendering, rate-limit retries, and platform quirks
57+
live inside the adapter. Platform-specific power is reached deliberately via
58+
typed adapter access (`chat.AdapterAs`), never by making raw platform structs
59+
the normal API.
60+
61+
**State is required and small.** Subscriptions, dedupe marks, and locks —
62+
that is all. Memory for development; Redis, Postgres, or NATS for production.
63+
64+
**Events are broader than messages.** A slash command and a button click are
65+
normalized events with their own hooks, not messages. All events ride the
66+
same dispatch spine.
67+
68+
**Semantic compatibility, not feature parity.** Vercel Chat SDK's
69+
conversation model is the precedent; its TypeScript API shapes are not. Where
70+
Go idioms or operational safety argue otherwise, this SDK deliberately
71+
diverges and documents the divergence.
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
# How To Choose A State Backend
2+
3+
Runtime state is required: the runtime stores subscribed-thread membership,
4+
event dedupe marks, and thread lock leases in a `chat.State`. It is
5+
coordination state, not product state — keep your application's own data in
6+
your own database keyed by `ThreadID`.
7+
8+
Four implementations ship today:
9+
10+
| Backend | Module | Use for |
11+
| --- | --- | --- |
12+
| Memory | `github.com/coder/chat/state/memory` (in the core module) | Tests and local demos. Lost on restart. |
13+
| Redis | `github.com/coder/chat/state/redis` | Production, horizontally scaled deployments. |
14+
| Postgres | `github.com/coder/chat/state/postgres` | Production, when Postgres is already your coordination store. |
15+
| NATS JetStream | `github.com/coder/chat/state/nats` | Production, when you already run NATS with JetStream. |
16+
17+
Redis, Postgres, and NATS live in separate Go modules so applications that only
18+
need core, Slack, or memory state do not pull their dependencies.
19+
20+
## Memory
21+
22+
```go
23+
import "github.com/coder/chat/state/memory"
24+
25+
bot, err := chat.New(ctx,
26+
chat.WithState(memory.New()),
27+
chat.WithAdapter(adapter),
28+
)
29+
```
30+
31+
Memory state is for tests and local development only. Subscriptions and dedupe
32+
data vanish when the process exits, so a restarted bot forgets which threads it
33+
was in and may re-handle redelivered events.
34+
35+
## Redis
36+
37+
```sh
38+
go get github.com/coder/chat/state/redis
39+
```
40+
41+
```go
42+
import (
43+
"github.com/redis/go-redis/v9"
44+
45+
chatredis "github.com/coder/chat/state/redis"
46+
)
47+
48+
redisState, err := chatredis.New(ctx, chatredis.Options{
49+
Client: redis.NewClient(&redis.Options{Addr: os.Getenv("REDIS_ADDR")}),
50+
// Prefix defaults to "chat".
51+
})
52+
```
53+
54+
The runnable example, including a `compose.yaml` for a local Redis, is
55+
[`examples/slack-redis-state`](../../examples/slack-redis-state/README.md).
56+
57+
## Postgres
58+
59+
```sh
60+
go get github.com/coder/chat/state/postgres
61+
```
62+
63+
```go
64+
import (
65+
"github.com/jackc/pgx/v5/pgxpool"
66+
67+
chatpostgres "github.com/coder/chat/state/postgres"
68+
)
69+
70+
pool, err := pgxpool.New(ctx, os.Getenv("DATABASE_URL"))
71+
if err != nil {
72+
return err
73+
}
74+
75+
pgState, err := chatpostgres.New(ctx, chatpostgres.Options{
76+
Pool: pool,
77+
// Namespace defaults to "chat".
78+
})
79+
```
80+
81+
The Postgres state initializes its own schema (subscription, event, and lock
82+
tables) on startup. The runnable example is
83+
[`examples/slack-postgres-state`](../../examples/slack-postgres-state/README.md).
84+
85+
## NATS JetStream
86+
87+
```sh
88+
go get github.com/coder/chat/state/nats
89+
```
90+
91+
```go
92+
import (
93+
natsgo "github.com/nats-io/nats.go"
94+
95+
chatnats "github.com/coder/chat/state/nats"
96+
)
97+
98+
conn, err := natsgo.Connect(os.Getenv("NATS_URL"))
99+
if err != nil {
100+
return err
101+
}
102+
103+
natsState, err := chatnats.New(ctx, chatnats.Options{
104+
Conn: conn,
105+
// Prefix defaults to "chat"; DedupeTTL and ThreadLockTTL default to the
106+
// runtime defaults (24h and 2m) and must match your RuntimeOptions.
107+
})
108+
```
109+
110+
NATS state stores subscriptions, dedupe marks, and locks in three JetStream
111+
Key-Value buckets with bucket-level TTLs (see
112+
[ADR 0014](../adr/0014-nats-state-adapter.md)). Because JetStream TTLs are
113+
per-bucket, the dedupe and lock TTLs are fixed at construction time. The
114+
runnable example is
115+
[`examples/slack-nats-state`](../../examples/slack-nats-state/README.md).
116+
117+
## How To Decide
118+
119+
- Writing tests or following the tutorial: use memory.
120+
- Already running Redis: use Redis. Same for Postgres and NATS — the backends
121+
are contract-equivalent, so pick the one you already operate.
122+
- Running more than one bot replica: any of the durable backends works; all
123+
three implement the same token-owned lock lease and dedupe contract, which is
124+
what makes horizontal scaling safe.
125+
126+
All backends are exercised by the same conformance suite; Redis and Postgres
127+
integration tests run against real backends via Testcontainers, and NATS tests
128+
run against an embedded JetStream server.

‎docs/how-to/deferred-dispatch.md‎

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
# How To Defer Long-Running Work (Ack-Then-Work)
2+
3+
Chat platforms expect webhooks to be acknowledged quickly — Slack retries
4+
after 3 seconds, Linear expects agent activity within ~10 seconds. If your
5+
handler calls an LLM or does anything slow, the default synchronous dispatch
6+
mode will run it *before* acknowledging the webhook, and the platform will
7+
retry or time out.
8+
9+
`DispatchDeferred` splits dispatch in two (see
10+
[ADR 0002](../adr/0002-async-dispatch.md)):
11+
12+
1. **Prelude, before ack**: signature verification, normalization, dedupe
13+
marking, and thread lock acquisition run synchronously on the request
14+
context.
15+
2. **Detached tail, after ack**: your handler runs on a runtime-managed
16+
detached work context while the platform already has its 2xx. The runtime
17+
keeps extending the thread lock lease in the background until the handler
18+
returns.
19+
20+
## Enable It
21+
22+
```go
23+
bot, err := chat.New(ctx,
24+
chat.WithState(state),
25+
chat.WithAdapter(adapter),
26+
chat.WithRuntimeOptions(chat.RuntimeOptions{
27+
Dispatch: chat.DispatchDeferred,
28+
DetachTimeout: 5 * time.Minute,
29+
Concurrency: chat.ConcurrencyQueue,
30+
}),
31+
)
32+
```
33+
34+
- `Dispatch: chat.DispatchDeferred` turns on ack-then-work. The default is
35+
`chat.DispatchSync`, which runs the handler before acknowledging.
36+
- `DetachTimeout` bounds how long a detached handler may run after the webhook
37+
request has ended.
38+
- `Concurrency: chat.ConcurrencyQueue` is the natural companion: while a
39+
detached handler holds the thread lock, follow-up events on the same thread
40+
wait instead of being dropped, and only the most recent superseded follow-up
41+
runs. The default `chat.ConcurrencyDrop` acknowledges and drops conflicting
42+
events instead.
43+
44+
## Write Handlers For The Detached Context
45+
46+
Your handler code does not change shape — it still receives a
47+
`context.Context` — but under `DispatchDeferred` that context is the detached
48+
work context, not the HTTP request context:
49+
50+
```go
51+
bot.OnNewMention(func(ctx context.Context, ev *chat.MessageEvent) error {
52+
// The platform already has its ack. Take your time (within
53+
// DetachTimeout): call the LLM, run tools, then post.
54+
answer, err := generate(ctx, ev.Message.Text)
55+
if err != nil {
56+
return err
57+
}
58+
_, err = ev.Thread.Post(ctx, chat.Markdown(answer))
59+
return err
60+
})
61+
```
62+
63+
Rules that keep this safe:
64+
65+
- Use the `ctx` you are given for every call. It carries the detach timeout
66+
and is what `Shutdown` uses to drain in-flight work.
67+
- Handler errors after ack are recorded and observed, not retried by the
68+
platform. If your work must not be lost, make it idempotent and consider
69+
your own queue.
70+
- `Shutdown(ctx)` waits for detached tails to finish (bounded by the context
71+
you pass it), so a rolling deploy does not sever half-finished replies.
72+
73+
## When Not To Use It
74+
75+
Stay with `DispatchSync` when handlers are fast (a quick reply, a state
76+
lookup) — synchronous dispatch keeps the failure story simpler because a
77+
handler error still happens before the platform ack. Streaming token
78+
transports are a non-goal of the core runtime; deferred dispatch plus one
79+
finished message is the supported long-generation pattern (see
80+
[ADR 0011](../adr/0011-resumable-streaming.md)).

0 commit comments

Comments
 (0)