README
¶
Chat SDK Go
Build Slack and Linear bots in Go. You write handlers for mentions and thread replies; the runtime verifies webhooks, dedupes redeliveries, serializes work per thread, and retries rate-limited API calls.
- Plain Go.
context.Context,net/http, small interfaces, and returned errors. It is a library, not a framework. - Scales out. Replicas share state on Redis, Postgres, or NATS JetStream, so they dedupe redeliveries and serialize work per thread.
- Handles slow work. Opt in to deferred dispatch to acknowledge the webhook first, then run LLM calls and other long handlers on a detached context.
Hello, Slack
A complete bot that replies to every mention:
package main
import (
"context"
"log"
"net/http"
"os"
"github.com/coder/chat"
"github.com/coder/chat/adapters/slack"
"github.com/coder/chat/state/memory"
)
func main() {
ctx := context.Background()
slackAdapter, err := slack.New(ctx, slack.Options{
SigningSecret: os.Getenv("SLACK_SIGNING_SECRET"),
BotToken: os.Getenv("SLACK_BOT_TOKEN"),
})
if err != nil {
log.Fatal(err)
}
bot, err := chat.New(ctx,
chat.WithState(memory.New()), // swap for Redis, Postgres, or NATS in production
chat.WithAdapter(slackAdapter),
)
if err != nil {
log.Fatal(err)
}
bot.OnNewMention(func(ctx context.Context, ev *chat.MessageEvent) error {
_, err := ev.Thread.Post(ctx, chat.Markdown("**hello** _world_"))
return err
})
webhook, err := bot.Webhook("slack")
if err != nil {
log.Fatal(err)
}
http.Handle("/webhooks/slack", webhook)
log.Fatal(http.ListenAndServe(":8080", nil))
}
Point your Slack app's Event Subscriptions request URL at
https://YOUR_HOST/webhooks/slack, mention the bot, and it replies in a
thread. The tutorial walks through the Slack
app setup in under 30 minutes using
examples/slack-hello-world, the same bot
with server timeouts and environment checks.
Install
Chat SDK Go requires Go 1.26.3 or newer. The core module contains the runtime, the Slack and Linear adapters, and the memory state backend:
go get github.com/coder/chat
The durable state backends are separate modules, so applications that only use the core do not pull their dependencies:
go get github.com/coder/chat/state/redis
go get github.com/coder/chat/state/postgres
go get github.com/coder/chat/state/nats
What You Get
- Thread-scoped conversations.
OnNewMentionstarts a conversation;Thread.Subscribekeeps the bot in it, andOnSubscribedMessagereceives the follow-ups — tutorial. - Shared state you already run. Memory for development; Redis, Postgres, or NATS JetStream in production, all tested by one conformance suite — choose a state backend.
- Ack-then-work dispatch. Handlers run synchronously by default; opt in
to
DispatchDeferredto run them after the webhook is acknowledged, with lock renewal and an admission cap. Five concurrency strategies (drop, queue, debounce, concurrent, burst) decide what happens when events overlap on one thread; debounce and burst require deferred dispatch — defer long-running work. - Slash commands and interactive components. Commands and button clicks have their own hooks; Block Kit and modals go through typed adapter access — slash commands, interactive components.
- Multi-tenant installs. Serve many Slack workspaces or Linear organizations from one deployment; you own the OAuth flow — multi-tenant installs.
- Linear agent sessions. Thoughts, actions, elicitations, plans, responses, and plain issue comments — Linear agent sessions.
- Rate-limit retries. Adapters honor
Retry-Afterwith bounded backoff and return a typedRateLimitederror when they give up — capability status. - Observability without extra dependencies.
sloglogging plus an optionalObserverfor metrics and spans — observability. - Message history on demand.
HistoryReaderfetches recent platform messages for a thread; what you store is up to you — message history.
Adapters
supported adapters are production-grade: hardening tests, rate-limit
handling, multi-tenant installs, and docs. experimental adapters work and
are tested, but their API may still change.
| Adapter | Tier | Notes |
|---|---|---|
Slack (adapters/slack) |
supported |
Hardening tests cover rate-limit retry, multi-tenant installs, history read-through, and interactivity. No live end-to-end Slack test runs in CI. |
Linear (adapters/linear) |
experimental |
Fully implemented and hardened, but Linear's agent API is itself in developer preview; see the capability gaps. |
| Microsoft Teams | spike | Not usable yet. ADR 0007 is waiting on a live-tenant spike (#6). |
Documentation
The docs index lists everything. Good starting points:
- New here? Your first Slack bot, zero to running in under 30 minutes.
- Doing a task? The how-to guides cover state backends, deferred dispatch, commands, interactivity, multi-tenant installs, and Linear.
- Looking something up? The reference and the GoDoc
(
go doc github.com/coder/chat). - Want the why? Architecture and design decisions.
Scope
Chat SDK Go follows Vercel Chat SDK's conversation model — adapters, normalized events, threads, subscriptions — where it fits Go. It is not a TypeScript API port: hooks hold one handler each, construction fails fast, subscriptions are explicit, and message history belongs to your application. See the concept map.
These are left out on purpose, each by a recorded decision (full list):
- Token streaming in the core. Deferred, not ruled out (ADR 0011); long generation posts one finished message.
- LLM orchestration. Prompts and model calls live in your handlers.
- A cross-platform card DSL. Native payloads go through
NativeContentPoster. - Transcript storage, RAG, and embeddings. Runtime state holds only subscriptions, dedupe marks, and locks.
- App-user auth and OAuth web flows. Your application owns these.
Status
Chat SDK Go is pre-1.0, and the public API may change before 1.0. See the releases for what changed. Report bugs and request features in GitHub issues.
Contributing
See CONTRIBUTING.md for how to build, test, and propose changes.
License
Chat SDK Go is released under the MIT License.
Documentation
¶
Overview ¶
Package chat provides a Go-native runtime for routing normalized chat events to application handlers and posting replies through platform adapters.
Index ¶
- Constants
- Variables
- func AdapterAs[T any](c *Chat, adapterName string) (T, bool)
- type Actor
- type Adapter
- type Attr
- type BotKind
- type Chat
- func (c *Chat) OnCommand(handler CommandHandler)
- func (c *Chat) OnInteraction(handler InteractionHandler)
- func (c *Chat) OnNewMention(handler MessageHandler)
- func (c *Chat) OnSubscribedMessage(handler MessageHandler)
- func (c *Chat) Shutdown(ctx context.Context) error
- func (c *Chat) Thread(ctx context.Context, id ThreadID) (*Thread, error)
- func (c *Chat) Webhook(adapterName string) (http.Handler, error)
- type Command
- type CommandEvent
- type CommandHandler
- type ConcurrencyStrategy
- type DispatchFunc
- type DispatchMode
- type DispatchOutcome
- type DispatchSpan
- type EphemeralOptions
- type EphemeralPoster
- type Event
- type HistoryQuery
- type HistoryReader
- type Install
- type InstallStore
- type Interaction
- type InteractionEvent
- type InteractionHandler
- type InteractionKind
- type LockLease
- type LockScope
- type Message
- type MessageEvent
- type MessageFormat
- type MessageHandler
- type NativeContent
- type NativeContentPoster
- type ObservationName
- type Observer
- type Option
- type PostableMessage
- type RetryMetadata
- type RuntimeOptions
- type SentMessage
- type State
- type Thread
- func (t *Thread) ID() ThreadID
- func (t *Thread) Post(ctx context.Context, msg PostableMessage) (*SentMessage, error)
- func (t *Thread) PostEphemeral(ctx context.Context, actor Actor, msg PostableMessage, opts EphemeralOptions) (*SentMessage, error)
- func (t *Thread) Subscribe(ctx context.Context) error
- func (t *Thread) Unsubscribe(ctx context.Context) error
- type ThreadID
- type ThreadRef
Constants ¶
const ( AttrAdapter = "adapter" AttrRoute = "route" AttrReason = "reason" AttrOutcome = "outcome" AttrTenant = "tenant" )
Attribute key constants form the documented, stable, low-cardinality set. Thread ID, message text, and raw actor IDs are deliberately never emitted as default attributes to keep metric cardinality bounded and avoid leaking conversation content.
Variables ¶
var ( ErrUnsupportedCapability = errors.New("chat: unsupported adapter capability") // ErrPreempted is the cancellation cause a stopped handler observes via // context.Cause: its Lock Lease was lost mid-run (released by another // runtime instance, expired, or no longer refreshable), so mutual // exclusion could no longer be guaranteed. ErrPreempted = errors.New("chat: handler preempted") // ErrAdmissionRejected is returned by deferred dispatch when the Admission // Bound rejects a delivery: the instance is at MaxDetached, the delivery's // (adapter, tenant) installation is at MaxDetachedPerTenant, or Runtime // Shutdown has closed admission. The delivery was rejected before // acknowledgement and before dedupe marking — it is never recorded in // Event Identity, so a platform retry is not deduped away. Adapters map it // to a shape-aware overload response: a retry-inducing status for // platform-redelivered shapes, a truthful busy signal for direct // invocations the platform does not redeliver (ADR 0015). ErrAdmissionRejected = errors.New("chat: deferred dispatch admission rejected") )
var ErrInstallNotFound = errors.New("chat: install not found")
ErrInstallNotFound signals that a Platform Tenant is not installed. A Multi-Tenant Adapter treats it as an Ignored Event during webhook handling (acknowledge, do not dispatch) and as a clean error on the out-of-webhook Thread Handle posting path.
Functions ¶
Types ¶
type Attr ¶
Attr is a single low-cardinality observation attribute. Construct attrs with the helper constructors so keys stay within the documented set.
func AdapterAttr ¶
AdapterAttr tags an observation with the originating adapter name.
func OutcomeAttr ¶
func OutcomeAttr(outcome DispatchOutcome) Attr
OutcomeAttr tags an observation with a terminal DispatchOutcome.
func ReasonAttr ¶
ReasonAttr tags an ignored-event observation with why it was ignored.
func RouteAttr ¶
RouteAttr tags an observation with the routing decision (new-mention, subscribed-message, command, interaction).
func TenantAttr ¶
TenantAttr tags an observation with the Platform Tenant.
type Chat ¶
type Chat struct {
// contains filtered or unexported fields
}
func (*Chat) OnCommand ¶
func (c *Chat) OnCommand(handler CommandHandler)
OnCommand installs or atomically replaces the single command handler. A Command Event routes here and never to the message hooks, even in a Subscribed Thread. This intentionally differs from Vercel Chat SDK's multiple-handler hooks.
func (*Chat) OnInteraction ¶
func (c *Chat) OnInteraction(handler InteractionHandler)
OnInteraction installs or atomically replaces the single interaction handler. An Interaction Event routes here and never to the message hooks. This intentionally differs from Vercel Chat SDK's multiple-handler hooks.
func (*Chat) OnNewMention ¶
func (c *Chat) OnNewMention(handler MessageHandler)
OnNewMention installs or atomically replaces the single new-mention handler. This intentionally differs from Vercel Chat SDK's multiple-handler hooks.
func (*Chat) OnSubscribedMessage ¶
func (c *Chat) OnSubscribedMessage(handler MessageHandler)
OnSubscribedMessage installs or atomically replaces the single subscribed-message handler. This intentionally differs from Vercel Chat SDK's multiple-handler hooks.
type Command ¶
type Command struct {
Name string
Text string
Args []string // advisory whitespace split of Text, not a parser
Actor Actor
Raw any // Platform Escape Hatch: response_url, trigger_id, Teams invoke value
}
Command is the normalized payload of a Command Event: a deliberate, parameterized platform invocation (a Slack slash command, a Teams command/invoke). A Command Event is an Event, never a Message.
type CommandEvent ¶
type CommandHandler ¶
type CommandHandler func(context.Context, *CommandEvent) error
type ConcurrencyStrategy ¶
type ConcurrencyStrategy int
const ( // ConcurrencyDrop acknowledges and drops a Lock Conflict. This is the default. ConcurrencyDrop ConcurrencyStrategy = iota // ConcurrencyQueue waits for the in-flight handler, then dispatches the most // recent superseded event for the scope. ConcurrencyQueue // ConcurrencyDebounce coalesces rapid follow-ups: each new routed event for a // scope supersedes the previous waiter and only the final event in a // DebounceInterval quiet period dispatches. Superseded events are surfaced as // skipped through Runtime Observation, never silently. Requires deferred // dispatch (a synchronous webhook cannot park an event past the platform's // acknowledgement deadline). // // "Final" follows the dispatch admission order on this instance: a delivery // delayed in its prelude never displaces a waiter admitted after it. The // quiet period is measured over registered waiters: a delivery stalled in // its prelude (validation, dedupe, routing) for longer than the interval // does not reset the running timer and dispatches separately afterwards — // still serialized by the Thread Lock and never lost, but not coalesced. // // Like queue supersession, coalescing is per runtime instance: events for // one scope delivered to different instances sharing a State are not // superseded across instances (each instance dispatches its own final // event, serialized by the Thread Lock). Per-instance supersession is the // decided v0.x contract (ADR 0015); cross-instance coalescing is rejected // for now behind that ADR's reopening bar. ConcurrencyDebounce // ConcurrencyConcurrent is the explicit opt-out of per-scope serialization: // every routed event dispatches immediately in its own execution, bounded by // MaxConcurrent. No Thread Lock is taken, so the caller accepts interleaved // replies and races on Thread Application State. ConcurrencyConcurrent // ConcurrencyBurst collects routed events for a scope into a batch while a // BurstWindow collection window is open, then dispatches the batch under a // single Thread Lock hold, running every member in join order, each with // its own DetachTimeout execution budget. The window is anchored at its // first member's join and is never extended by later arrivals; a window // reaching MaxBurstBatch seals immediately (the cap-reaching member is the // batch's last member) and the next event opens a rolled window dispatched // strictly after its predecessor. Batch shaping is delivery-preserving // (ADR 0015): boundaries move, but an accepted member is never dropped. // Requires deferred dispatch (a synchronous webhook cannot park an event // past the platform's acknowledgement deadline). // // Parked members hold Admission Bound slots until their terminal // disposition, so burst retention stays inside MaxDetached. A member whose // handler returns an error does not abort its batch; losing the Lock // Lease mid-batch cancels the running member (ErrPreempted) and skips the // remaining members observably rather than running them unserialized. // Like every deferred handler, member cancellation is cooperative: a // handler that ignores its context blocks the members behind it. // // Like queue supersession and debounce coalescing, batching is per runtime // instance (ADR 0015): events for one scope delivered to different // instances sharing a State batch independently, serialized by the Thread // Lock. Cross-instance coalescing is rejected for now behind that ADR's // reopening bar. ConcurrencyBurst )
type DispatchMode ¶
type DispatchMode int
DispatchMode selects whether the routed handler runs before or after the adapter acknowledges the platform.
const ( // DispatchSync runs the handler under the request context and acknowledges // after it returns. This is the default. DispatchSync DispatchMode = iota // DispatchDeferred acknowledges after the prelude and runs the handler on the // detached work context (ack-then-work). DispatchDeferred )
type DispatchOutcome ¶
type DispatchOutcome string
DispatchOutcome is the closed set of terminal outcomes for one Runtime Dispatch, mirroring the terminal branches in dispatch.
const ( OutcomeHandled DispatchOutcome = "handled" OutcomeIgnored DispatchOutcome = "ignored" OutcomeDroppedLockConflict DispatchOutcome = "dropped-lock-conflict" OutcomeDuplicate DispatchOutcome = "duplicate" OutcomeError DispatchOutcome = "error" // OutcomePreempted is the terminal outcome of a handler that stopped because // its Lock Lease was lost mid-run: released by another runtime instance, // expired, or no longer refreshable. OutcomePreempted DispatchOutcome = "preempted" // OutcomeAdmissionRejected is the terminal outcome of a delivery the // Admission Bound rejected before acknowledgement and before dedupe // marking: it never became an Accepted Event and owes only the overload // response. OutcomeAdmissionRejected DispatchOutcome = "admission-rejected" )
type DispatchSpan ¶
type DispatchSpan interface {
End(outcome DispatchOutcome, attrs ...Attr)
}
DispatchSpan is the open span for one Runtime Dispatch; End closes it with the terminal outcome.
type EphemeralOptions ¶
type EphemeralOptions struct {
FallbackToDM bool
}
type EphemeralPoster ¶
type EphemeralPoster interface {
PostEphemeralMessage(context.Context, ThreadRef, Actor, PostableMessage, EphemeralOptions) (*SentMessage, error)
}
type Event ¶
type Event struct {
ID string
Adapter string
Tenant string
ThreadID ThreadID
DirectMessage bool
// At most one of Message, Command, or Interaction is set: a Command Event and
// an Interaction Event are Events, never Messages.
Message *Message
Command *Command
Interaction *Interaction
Retry RetryMetadata
Raw any
}
type HistoryQuery ¶
type HistoryQuery struct {
// Limit is the desired page size. The adapter clamps it to the platform's
// maximum and applies its own default when Limit <= 0.
Limit int
// Before is an optional opaque cursor: a Message.ID returned by a prior page.
// It is a plain string (NOT a fabricated MessageID type); the adapter
// interprets it per its platform read API.
Before string
}
HistoryQuery parameterizes a HistoryReader read. Pagination and ordering are adapter-owned (platform read APIs differ) and documented in each adapter's GoDoc; this struct imposes no portable pagination model.
type HistoryReader ¶
type HistoryReader interface {
ReadHistory(context.Context, ThreadID, HistoryQuery) ([]Message, error)
}
HistoryReader is an Optional Capability: an adapter implements it only when the platform exposes a conversation read API. It is reached exclusively through typed Adapter Access (chat.AdapterAs); it is NOT a method on the core Adapter interface, NOT a Routing Hook input, and is never auto-invoked during Runtime Dispatch.
ReadHistory is a live platform read keyed by the opaque Thread ID. It performs NO runtime storage: it does not write Runtime State, does not dedupe via Event Identity, and does not cache. Returned Messages are normalized with raw platform data preserved via the Platform Escape Hatch (Message.Raw). Ordering, pagination, and page-size clamping are adapter-owned and documented in the adapter's GoDoc.
Stored/long-term conversation context (transcripts, LLM context windows, summaries, RAG corpora) is Thread Application State, owned by the application in its own storage keyed by Thread ID; this capability is a thin live read-through only. Absence of the capability is the explicit ErrUnsupportedCapability result (via AdapterAs returning ok == false), never an empty []Message that masquerades as "no history".
type Install ¶
type Install struct {
// Tenant is the Platform Tenant this record authorizes.
Tenant string
// Credential is an adapter-specific payload reached as a Platform Escape Hatch
// for credentials (e.g. slack.SlackInstall, linear.LinearInstall). It is not a
// normalized token model; each adapter documents and decodes the concrete type
// it requires.
Credential any
// BotActorID is an optional pre-discovered bot identity used for tenant-correct
// self-filtering. When empty the adapter falls back to any identity carried in
// the adapter-specific Credential.
BotActorID string
}
Install is a durable, app-owned install record mapping one Platform Tenant to adapter-specific credentials. It is NOT a normalized cross-platform token model and NOT a live access token the runtime refreshes; the adapter performs any near-expiry exchange itself, keyed by Platform Tenant, per ADR-0001.
type InstallStore ¶
type InstallStore interface {
// Lookup returns the install record for one Platform Tenant under the named
// adapter. The adapter parameter lets one application store serve multiple
// adapters keyed by name. ErrInstallNotFound means the tenant is not installed
// and the adapter treats the webhook as an Ignored Event (acknowledge, do not
// dispatch). Any other error is a transport failure the platform may retry.
Lookup(ctx context.Context, adapter, tenant string) (Install, error)
}
InstallStore is the Optional Capability application code implements and a Multi-Tenant Adapter consumes during webhook handling and Thread Handle reconstruction to resolve per-Platform-Tenant credentials. It is NOT a method on the core Adapter interface and NOT a string registry: the runtime defines the contract; the application owns the store, its encryption, and the OAuth installation flow.
Lookup is called per inbound webhook; the adapter does not cache install records, so an uninstall or credential rotation takes effect as soon as Lookup reflects it. An application that wants to avoid a per-event store hit caches inside its own Lookup, where it also owns invalidation.
type Interaction ¶
type Interaction struct {
Kind InteractionKind
ActionID string
// Value is the activated component's single value: a button's value or the
// selected option value of a single-select component (static_select,
// external_select, overflow, radio_buttons). Empty when the component
// carries no value.
Value string
// Values are the selected option values of a multi-valued component
// (multi_static_select, checkboxes). Nil for single-valued components;
// empty but non-nil when a multi-valued component's selection was cleared.
Values []string
Actor Actor
Raw any // Platform Escape Hatch: response_url, trigger_id, view state
}
Interaction is the normalized payload of an Interaction Event: a Block Kit / card component action. Like a Command Event, an Interaction Event is an Event, never a Message.
type InteractionEvent ¶
type InteractionEvent struct {
Event *Event
Thread *Thread
Interaction *Interaction
}
type InteractionHandler ¶
type InteractionHandler func(context.Context, *InteractionEvent) error
type InteractionKind ¶
type InteractionKind int
InteractionKind classifies an Interaction Event. This slice supports block_actions (button clicks and menu selections) only.
const ( InteractionUnknown InteractionKind = iota // InteractionBlockAction is a Slack block_actions interaction: a button click // or a menu selection. InteractionBlockAction )
type LockScope ¶
type LockScope int
LockScope selects what key the Thread Lock guards. The opaque Thread ID is unchanged; the scope only chooses the serialization key.
const ( // LockScopeThread serializes handlers per Thread. This is the default. LockScopeThread LockScope = iota // LockScopeChannel widens serialization from a single Thread to its whole // channel, for platforms whose model requires channel-wide ordering. A // Thread whose adapter reports no channel falls back to per-Thread locking // rather than sharing one adapter-wide key. LockScopeChannel )
type MessageEvent ¶
type MessageFormat ¶
type MessageFormat int
const ( MessageFormatText MessageFormat = iota MessageFormatMarkdown )
type MessageHandler ¶
type MessageHandler func(context.Context, *MessageEvent) error
type NativeContent ¶
type NativeContent struct {
// Adapter must match the target adapter; a mismatch is an error, never a
// silent portable downgrade.
Adapter string
Payload any
}
NativeContent carries an explicitly platform-native message body (Slack Block Kit blocks, a Teams Adaptive Card) as a Platform Escape Hatch. It is NOT a cross-platform card model and does NOT widen Postable Message; Plain Text and Portable Markdown remain the portable surface. Native content is reached only through the NativeContentPoster Optional Capability via typed Adapter Access.
type NativeContentPoster ¶
type NativeContentPoster interface {
PostNative(context.Context, ThreadRef, NativeContent) (*SentMessage, error)
}
NativeContentPoster is the Optional Capability for posting NativeContent. Adapters that support native rich content implement it; callers reach it via chat.AdapterAs. Absence of the capability is an explicit unsupported result, not a panic.
type ObservationName ¶
type ObservationName string
ObservationName is a closed, stable set of discrete observation points the runtime emits. The set mirrors the decision points the runtime already logs via structured slog, so a log line and a metric cannot disagree about what happened.
const ( // ObsDedupeHit is emitted when a duplicate Event (by Event Identity) is // dropped. ObsDedupeHit ObservationName = "dedupe_hit" // ObsLockConflict is emitted when an Event hits a Lock Conflict. ObsLockConflict ObservationName = "lock_conflict" // ObsLockReleaseFailed is emitted when releasing a Thread Lock fails or finds // the lease already gone. ObsLockReleaseFailed ObservationName = "lock_release_failed" // ObsIgnoredEvent is emitted when a verified, Accepted Event is ignored // (non-message with no payload handler, self, or unrouted). Carries a reason // attribute. ObsIgnoredEvent ObservationName = "ignored_event" // ObsHandlerError is emitted when a routed handler returns an error. Carries a // route attribute. ObsHandlerError ObservationName = "handler_error" // ObsAdapterCall is an adapter-facing observation emitted around a Platform // Adapter API call. It is reached through typed Adapter Access and // adapter-owned Observer wiring, not the core Adapter interface. ObsAdapterCall ObservationName = "adapter_call" // ObsRateLimit is an adapter-facing observation emitted when an adapter // observes platform rate limiting (ADR 0005). Adapter-owned, like // ObsAdapterCall. ObsRateLimit ObservationName = "rate_limit" // ObsAdmissionRejected is emitted when the deferred-dispatch Admission // Bound rejects a delivery (MaxDetached, MaxDetachedPerTenant, or Runtime // Shutdown). Carries adapter and tenant attributes. ObsAdmissionRejected ObservationName = "admission_rejected" )
type Observer ¶
type Observer interface {
// Event records a discrete, counter-style observation with low-cardinality
// attributes.
Event(ctx context.Context, name ObservationName, attrs ...Attr)
// Dispatch opens a span for one Runtime Dispatch and returns a DispatchSpan
// whose End records the terminal outcome and latency. Under deferred dispatch
// the span follows the Detached Work Context so Ack-Then-Work latency is
// measured to handler completion.
Dispatch(ctx context.Context, attrs ...Attr) (context.Context, DispatchSpan)
}
Observer is the optional Observation Hook: a narrow, observe-only seam the runtime calls at the decision points it already logs. It is modeled as an Optional Capability with a no-op default; the core never imports OpenTelemetry, Prometheus, or statsd. An Observer cannot mutate routing, acknowledgement, or handler flow, which is what keeps it distinct from Middleware.
All methods must be best-effort and must not block dispatch. The runtime wraps every call so a panicking or slow Observer can never fail an Accepted Event or alter acknowledgement.
type Option ¶
type Option func(*config)
func WithAdapter ¶
func WithLogger ¶
func WithObserver ¶
WithObserver installs an Observer. The default is an internal no-op observer, so an unconfigured runtime behaves exactly as today: structured slog only, with no third-party metrics dependency.
func WithRuntimeOptions ¶
func WithRuntimeOptions(options RuntimeOptions) Option
type PostableMessage ¶
type PostableMessage struct {
Text string
Format MessageFormat
}
func Markdown ¶
func Markdown(text string) PostableMessage
func Text ¶
func Text(text string) PostableMessage
type RetryMetadata ¶
type RuntimeOptions ¶
type RuntimeOptions struct {
DedupeTTL time.Duration
ThreadLockTTL time.Duration
Concurrency ConcurrencyStrategy
Dispatch DispatchMode
DetachTimeout time.Duration
// LockScope selects the Thread Lock key: per Thread (default) or per
// channel.
LockScope LockScope
// DebounceInterval is the ConcurrencyDebounce quiet period. It must be
// positive under that strategy and is ignored otherwise.
DebounceInterval time.Duration
// MaxConcurrent bounds simultaneous handler executions under
// ConcurrencyConcurrent. It must be positive under that strategy and is
// ignored otherwise.
MaxConcurrent int
// BurstWindow is the ConcurrencyBurst collection window: routed events for
// a scope collect for this long — anchored at the window's first member,
// never extended by later arrivals — before dispatching as one batch. It
// must be positive under that strategy and is ignored otherwise. The
// window is collection time, not execution time: it does not consume the
// batch's lock-wait budget or any member's DetachTimeout.
BurstWindow time.Duration
// MaxBurstBatch caps how many members one burst batch may collect. A
// window reaching the cap seals immediately, with the cap-reaching member
// as the sealed batch's last member; the next event opens a rolled window
// dispatched strictly after its predecessor. The cap shapes batches — it
// never rejects or drops an accepted member (delivery-preserving shaping,
// ADR 0015) — and parked members remain bounded by MaxDetached regardless.
// Zero disables the cap; it must not be negative under the burst strategy
// and is ignored otherwise.
MaxBurstBatch int
// MaxDetached is the deferred-dispatch Admission Bound (ADR 0015): a
// per-instance cap on admitted-but-incomplete deferred deliveries.
// Everything a delivery retains under DispatchDeferred counts against it —
// running detached tails, parked queue/debounce waiters, concurrent
// slot-waiters, and parked burst batch members — and capacity frees only
// when that retention ends. A delivery arriving at the cap is rejected
// with ErrAdmissionRejected
// before acknowledgement and before dedupe marking, so a platform retry is
// never deduped away. It must be positive under DispatchDeferred and is
// ignored under DispatchSync; DefaultRuntimeOptions sets 1024.
//
// Sizing: the bound is a count, not bytes — the runtime cannot measure
// retained platform payloads or handler closures. Budget roughly
// MaxDetached x (platform payload ceiling + one goroutine stack + whatever
// the handler closure pins) against the instance's memory; sustained
// rejection is the platform-visible backpressure signal, so size the cap
// to be reached only under genuine overload and keep front-door rate
// limiting for fleet-level control.
MaxDetached int
// MaxDetachedPerTenant additionally caps any single installation's share
// of MaxDetached, keyed on the delivery's (adapter, tenant) installation
// identity (ADR 0006). It is a ceiling through the same rejection path,
// not a reservation: a hot tenant is capped, but no capacity is guaranteed
// to the others. Zero disables the ceiling; it must not be negative under
// DispatchDeferred and is ignored under DispatchSync.
MaxDetachedPerTenant int
}
func DefaultRuntimeOptions ¶
func DefaultRuntimeOptions() RuntimeOptions
type SentMessage ¶
type State ¶
type State interface {
IsThreadSubscribed(context.Context, ThreadID) (bool, error)
SubscribeThread(context.Context, ThreadID) error
UnsubscribeThread(context.Context, ThreadID) error
MarkEvent(context.Context, string, time.Duration) (bool, error)
AcquireLock(context.Context, string, time.Duration) (LockLease, bool, error)
ExtendLock(context.Context, LockLease, time.Duration) (bool, error)
ReleaseLock(context.Context, LockLease) (bool, error)
Shutdown(context.Context) error
}
type Thread ¶
type Thread struct {
// contains filtered or unexported fields
}
func (*Thread) Post ¶
func (t *Thread) Post(ctx context.Context, msg PostableMessage) (*SentMessage, error)
func (*Thread) PostEphemeral ¶
func (t *Thread) PostEphemeral(ctx context.Context, actor Actor, msg PostableMessage, opts EphemeralOptions) (*SentMessage, error)
Source Files
¶
Directories
¶
| Path | Synopsis |
|---|---|
|
adapters
|
|
|
linear
Package linear provides a Linear app-actor adapter for Chat SDK Go.
|
Package linear provides a Linear app-actor adapter for Chat SDK Go. |
|
slack
Package slack provides the Slack adapter for the chat runtime.
|
Package slack provides the Slack adapter for the chat runtime. |
|
examples
|
|
|
linear-agent-hello-world
command
|
|
|
slack-hello-world
command
|
|
|
internal
|
|
|
ratelimit
Package ratelimit holds the transport-level helpers shared by the per-adapter rate-limit retry loops (ADR 0005).
|
Package ratelimit holds the transport-level helpers shared by the per-adapter rate-limit retry loops (ADR 0005). |
|
state
|
|
|
memory
Package memory provides an in-process chat runtime state implementation for tests and local development.
|
Package memory provides an in-process chat runtime state implementation for tests and local development. |
|
nats
module
|
|
|
postgres
module
|
|
|
redis
module
|