Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

### Issue tracker

Issues and PRDs are tracked as local markdown files under `.scratch/`. See `docs/agents/issue-tracker.md`.
GitHub issues (https://github.com/coder/chat/issues) are the public source of truth for roadmap and bugs; `.scratch/` holds internal working notes only. See `docs/agents/issue-tracker.md`.

### Triage labels

Expand Down
2 changes: 1 addition & 1 deletion CONTEXT.md
Original file line number Diff line number Diff line change
Expand Up @@ -417,7 +417,7 @@ _Avoid_: Full platform schema, strict external SDK model
- **Runtime Options** TTL values must be positive.
- **Runtime Options** include a **Concurrency Strategy** that defaults to drop.
- The runtime implements the drop (default) and queue **Concurrency Strategy** values; burst, debounce, concurrent, lock-scope, and force/steerability remain reserved for future slices.
- A **Thread Lock** must not drop distinct **Webhook Events** for the same **Thread**; it only coordinates their processing.
- A **Thread Lock** coordinates processing of distinct **Webhook Events** for the same **Thread**; it never deduplicates them, and what happens to a conflicting event is decided by the **Concurrency Strategy** (drop acknowledges and drops it; queue coalesces waiters per process and runs the most recent after the lock releases).
- A **Thread Lock** is represented as a **Lock Lease** with an ownership token.
- Releasing or extending a **Lock Lease** must verify the ownership token so an expired holder cannot affect a newer holder.
- A **Lock Conflict** is acknowledged to the platform by default and recorded as unhandled runtime contention.
Expand Down
238 changes: 173 additions & 65 deletions README.md

Large diffs are not rendered by default.

47 changes: 47 additions & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# 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.

## Tutorials

Start here if you are new to the SDK.

- [Your first Slack bot](tutorials/slack-bot.md) — zero to a running Slack bot
in under 30 minutes.

## How-To Guides

Task-oriented guides for people already running a bot.

- [Choose a state backend](how-to/choose-a-state-backend.md) — memory, Redis,
Postgres, or NATS JetStream.
- [Defer long-running work (ack-then-work)](how-to/deferred-dispatch.md) —
acknowledge webhooks fast and run handlers on a detached context.
- [Handle slash commands](how-to/slash-commands.md) — route Slack slash
commands through `OnCommand`.
- [Handle interactive components](how-to/interactive-components.md) — buttons,
menus, Block Kit content, and modals.
- [Install into multiple workspaces (multi-tenant)](how-to/multi-tenant-install.md) —
resolve per-tenant credentials with an `InstallStore`.
- [Run Linear agent sessions](how-to/linear-agent-sessions.md) — build a Linear
agent with thoughts, responses, actions, elicitations, and plans.

## Reference

- [API and package reference](reference.md) — pkg.go.dev pointers, module
layout, and per-adapter capability status.
- [Linear agent capability gaps](linear-agent-capabilities.md) — tracked list
of Linear agent APIs the adapter does not yet wrap.

## 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.

## Non-User Documentation

- [`docs/agents/`](agents/) — instructions for coding agents working on this
repository, not for SDK users.
4 changes: 3 additions & 1 deletion docs/adr/0006-multi-tenant-install.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@ Specifically:

- **Lookup ordering.** During webhook handling the adapter: (1) parses the **Platform Tenant** out of the **Supported Platform Shape**; (2) calls `InstallStore.Lookup`; (3) verifies the signature using install-record material where the platform signs per-install, or the shared app-level signing secret where it does not (Slack); (4) normalizes the tenant-scoped **Event** and hands it to **Runtime Dispatch**. On platforms with a per-install signing secret the tenant must be read from an unverified body for routing only, then re-validated by signature verification before any side effect.

*Implementation note (as built):* the Slack adapter verifies the shared app-level signature **before** parsing the tenant or calling `Lookup`, so a Slack `InstallStore` only ever sees tenants from verified requests. The unverified-routing-read ordering above applies to per-install-signed platforms (Linear), where the store's tenant argument is untrusted routing input by necessity.

- **Not-installed is an Ignored Event.** `ErrInstallNotFound` is acknowledged to the platform without dispatch, consistent with CONTEXT.md's **Ignored Event** definition. Any other **Install Store** error is a transport failure the platform may retry.

- **Out-of-webhook posting uses the same resolver.** **Thread Handle** reconstruction decodes the **Platform Tenant** from the **Thread ID**, calls `InstallStore.Lookup`, and posts. A stored **Thread ID** stays postable while the app holds a valid install record.
Expand All @@ -71,7 +73,7 @@ Deliberate divergences from the upstream Chat SDK and trade-offs:

- The runtime owns the credential-lookup *contract* but never the store, unlike full marketplace SDKs that ship installation persistence.
- The OAuth web flow is explicitly out: the app owns authorize/callback routes, matching how the runtime exposes only **Webhook Handlers** and owns no HTTP server.
- On per-app-signed platforms (Slack) the tenant is parsed from an unverified body before verification. This is a routing read only, re-validated by signature verification before side effects; adapters document the ordering.
- On per-install-signed platforms (Linear) the tenant is parsed from an unverified body before verification. This is a routing read only, re-validated by signature verification before side effects; adapters document the ordering. (On per-app-signed platforms — Slack — the implementation verifies the shared signature first, so lookup happens post-verification.)
- The credential payload is adapter-specific (`any`), trading a normalized token model for honesty about how differently Slack and Linear authorize.

Costs: the app must build and secure the **Install Store**; a misimplemented store (returning a stale or wrong-tenant credential) can post to the wrong workspace, so tenant-correctness tests are required. Adapters carry two construction modes to keep tested.
Expand Down
30 changes: 23 additions & 7 deletions docs/agents/issue-tracker.md
Original file line number Diff line number Diff line change
@@ -1,19 +1,35 @@
# Issue tracker: Local Markdown
# Issue tracker: GitHub Issues

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

## Conventions
https://github.com/coder/chat/issues

Anything user-facing — planned work, accepted/rejected proposals, bug state —
belongs there. Use `gh issue` to read and update it.

## `.scratch/` is internal working notes only

The `.scratch/` directory holds internal working notes: PRDs, implementation
breakdowns, and drafts that agents produce while working. It is not a public
roadmap, it makes no promises, and nothing in it should be treated as
authoritative over GitHub issues. When a scratch note graduates into real
planned work, file a GitHub issue for it.

### Conventions for `.scratch/`

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

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

Create a new file under `.scratch/<feature-slug>/` (creating the directory if needed).
Create a GitHub issue with `gh issue create`. Use `.scratch/<feature-slug>/`
only for supporting working notes that are not ready to be public.
Comment thread
ThomasK33 marked this conversation as resolved.

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

Read the file at the referenced path. The user will normally pass the path or the issue number directly.
If the reference is a number or URL, read the GitHub issue with
`gh issue view`. If the reference is a path, read the file at that path.
71 changes: 71 additions & 0 deletions docs/explanation.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
# Architecture And Design Decisions

Chat SDK Go's design is documented in two places, and this page is the index
over both:

- [`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
Comment thread
ThomasK33 marked this conversation as resolved.
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 (`queue` implemented; `burst`/`debounce`/`concurrent` staged) | Accepted (staged) |
| [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 |

## 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
leases, and routes to your single-slot handlers. Everything your product
stores — transcripts, user records, workflow state — lives in your database,
keyed by the opaque `ThreadID`.

**Adapters own the platform boundary.** Signature verification, payload
normalization, outbound rendering, rate-limit retries, and platform quirks
live inside the adapter. Platform-specific power is reached deliberately via
typed adapter access (`chat.AdapterAs`), never by making raw platform structs
the normal API.

**State is required and small.** Subscriptions, dedupe marks, and locks —
that is all. Memory for development; Redis, Postgres, or NATS for production.

**Events are broader than messages.** A slash command and a button click are
normalized events with their own hooks, not messages. All events ride the
same dispatch spine.

**Semantic compatibility, not feature parity.** Vercel Chat SDK's
conversation model is the precedent; its TypeScript API shapes are not. Where
Go idioms or operational safety argue otherwise, this SDK deliberately
diverges and documents the divergence.
141 changes: 141 additions & 0 deletions docs/how-to/choose-a-state-backend.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
# 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`.

Four implementations ship today:

| Backend | Module | Use for |
| --- | --- | --- |
| 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. |

Redis, Postgres, and NATS live in separate Go modules so applications that only
need core, Slack, or memory state do not pull their dependencies.

## Memory

```go
import "github.com/coder/chat/state/memory"

bot, err := chat.New(ctx,
chat.WithState(memory.New()),
chat.WithAdapter(adapter),
)
```

Memory state is for tests and local development only. Subscriptions and dedupe
data vanish when the process exits, so a restarted bot forgets which threads it
was in and may re-handle redelivered events.

## Redis

```sh
go get github.com/coder/chat/state/redis
```
Comment thread
ThomasK33 marked this conversation as resolved.

```go
import (
"github.com/redis/go-redis/v9"

chatredis "github.com/coder/chat/state/redis"
)

redisState, err := chatredis.New(ctx, chatredis.Options{
Client: redis.NewClient(&redis.Options{Addr: os.Getenv("REDIS_ADDR")}),
Prefix: "mybot", // see "One namespace per bot application" below
})
```

The runnable example, including a `compose.yaml` for a local Redis, is
[`examples/slack-redis-state`](../../examples/slack-redis-state/README.md).

## Postgres

```sh
go get github.com/coder/chat/state/postgres
```

```go
import (
"github.com/jackc/pgx/v5/pgxpool"

chatpostgres "github.com/coder/chat/state/postgres"
)

pool, err := pgxpool.New(ctx, os.Getenv("DATABASE_URL"))
if err != nil {
return err
}

pgState, err := chatpostgres.New(ctx, chatpostgres.Options{
Pool: pool,
Namespace: "mybot", // see "One namespace per bot application" below
})
```

The Postgres state initializes its own schema (subscription, event, and lock
tables) on startup. The runnable example is
[`examples/slack-postgres-state`](../../examples/slack-postgres-state/README.md).

## NATS JetStream

```sh
go get github.com/coder/chat/state/nats
```

```go
import (
natsgo "github.com/nats-io/nats.go"

chatnats "github.com/coder/chat/state/nats"
)

conn, err := natsgo.Connect(os.Getenv("NATS_URL"))
if err != nil {
return err
}

natsState, err := chatnats.New(ctx, chatnats.Options{
Conn: conn,
Prefix: "mybot", // see "One namespace per bot application" below
// DedupeTTL and ThreadLockTTL default to the runtime defaults (24h and
// 2m) and must match your RuntimeOptions.
})
```

NATS state stores subscriptions, dedupe marks, and locks in three JetStream
Key-Value buckets with bucket-level TTLs (see
[ADR 0014](../adr/0014-nats-state-adapter.md)). Because JetStream TTLs are
per-bucket, the dedupe and lock TTLs are fixed at construction time. The
runnable example is
[`examples/slack-nats-state`](../../examples/slack-nats-state/README.md).

## 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.
Loading