Getting started

Core concepts

You type a Chat SDK–shaped API. That desugars into a small core algebra. Interpreters run it. Developers never have to import the algebra.

Two layers, one program

B  (what you type)     cx.on_message({ channel: "telegram", kind: "dm" }, fn)
        │ desugar
        ▼
A  (the program)       App([ Rule(pred, overlap, handler) ])
        │ interpret
        ▼
world                  Memory | your process | hosted gateway
B  (what you type)     cx.onMessage({ channel: "telegram", kind: "dm" }, fn)
        │ desugar
        ▼
A  (the program)       App([ Rule(pred, overlap, handler) ])
        │ interpret
        ▼
world                  Memory | your process | hosted gateway

cx.app is the current program as inspectable data. You can test it without Telegram: feed a fake message in, look at what the bot would do.

Caspian

The public entry point. It builds rules from on_message / onMessage and on_action / onAction, holds channels, and drives inbound (handle, poll, listen, run).

from caspian import Caspian

cx = Caspian()                      # self-host; no key
cx = Caspian(api_key=KEY)           # hosted: needed for via="hosted" and run()
import { Caspian } from "caspian-sdk"

const cx = new Caspian()                      // self-host; no key
await cx.run({ apiKey: KEY })                 // hosted poll; add() still needs via: "hosted"

Adapters

One driver per channel. An adapter is not the language, the facade, OAuth, or the overlap queue. It is:

Adding WhatsApp is a new adapter, not a new handler type. TypeScript packs live at caspian-sdk/telegram, caspian/discord, caspian/slack, …

Thread

The handle passed to handlers. Methods enqueue commands; they are not an HTTP client. thread.post answers the inbound turn. thread.send starts a message that is not a reply. Thread ids look like telegram:… / slack:… — never a raw platform chat id in tools.

Relationship memory lives with the runner. TypeScript: thread.recent() / thread.state. Python: thread.history / thread.set_state. A crash or a serverless invocation does not wipe the relationship.

Overlap

When three messages arrive while the bot is still answering the first, the rule names the policy; the runner owns the queue. Default for text is queue (hold extras). Buttons, voice, and typing should use drop (run now or skip). See Overlap for debounce, parallel, ctx.skipped, and how adapters pick the key.

Hosted vs self-host

HostedSelf-host
IdentityCaspian (or shared bot / Meet room)Your token
Inbound ownerGatewayYour URL, poll, or socket
Your looprun() or handle("gateway", …)handle(channel, …) / poll / listen
API keyRequiredNot required

One inbound owner per connection — never both. Send-only: inbound: false.

Three different POSTs

KindWho → whom
Provisioning callbackDiscord/Slack → Caspian (flips connection to active)
Platform webhookTelegram → Caspian or your URL
Event deliveryCaspian → your app (hosted: the only URL you expose)