Python
Package caspian. Python 3.10+. Same program surface as TypeScript — Caspian, rules, Thread — not a second API.
Install
pip install caspian-sdk
# optional sockets:
pip install "caspian-sdk[discord]"
pip install "caspian-sdk[slack-socket]"Public imports: Caspian, Thread, Message, Action, Button, Attachment, HandlerContext, ProvisionError, Result, ToolSet.
Caspian
from caspian import Caspian
cx = Caspian() # self-host; no Caspian key
cx = Caspian(api_key=KEY) # hosted: via="hosted" and run()
# api_key="", base_url="", webhook_secret="", dispatch=Truecx.app | Inspectable App of rules. Test without a network. |
cx.channels.add(channel, *, via, …) | Always pass via="hosted" or via="self-host". Self-host needs no Caspian key. |
cx.on_message(options) | Decorator or on_message(opts, fn). |
cx.on_action(options) | Button / callback handler. |
cx.use(rule) | Raw core Rule. |
cx.tools(thread=None, *, preset=…) | "messenger" or "outbound". |
cx.interpret() | MemoryInterpreter, no HTTP. |
cx.handle(channel, body, headers=None) | Self-host webhook, or handle("gateway", …) hosted push. |
cx.poll(channel, *, max_iterations=None, interval=1.0) | Telegram getUpdates. Same pipeline as handle. |
cx.listen(channel="discord", *, max_events=None) | Discord socket or Slack Socket Mode. Blocks. |
cx.run(*, max_iterations=None, interval=1.0) | Hosted: GET /v1/events then handle("gateway", …). Needs api_key. |
channels.add kwargs that matter: bot_token, webhook_url, inbound=True, webhook_secret, signing_secret, app_secret, api_key (Linear / iMessage — that platform’s token, not Caspian’s), display_name, plus channel extras (app_token for Slack Socket Mode).
Handlers
Message handler: (thread, message, ctx) — sync or async def. Action handler: (thread, action, ctx). ctx.skipped is the burst overlap collapsed before this turn. See Overlap.
Options: channel (name or list), kind (dm / group / channel), command, overlap (queue / debounce / drop / parallel / stream), bound (default 16), ack (user-visible reply before the handler). on_action adds data.
Default overlap for both on_message and on_action is queue. Pass overlap: "drop" on buttons and voice.
Thread
Methods enqueue commands. They are not an HTTP client.
thread.post(text, *, actions=())
thread.send(text, *, actions=()) # not a reply to this turn
thread.reply(reply_to, text, *, actions=())
thread.send_blocks(blocks, *, text="", actions=())
thread.send_media(attachment, *, caption="")
thread.typing()
thread.edit(message_id, text, *, actions=())
thread.delete(message_id)
thread.react(message_id, emoji)
thread.pin / unpin(message_id)
thread.forward(to_thread_id, message_id)
thread.mark_read(message_id="")
thread.initiate(text, *, actions=())
thread.schedule(text, send_at, *, actions=())
thread.history(*, limit=20, before="")
thread.subscribe()
thread.set_state(key, value)
with thread.stream(*, min_chars=24, throttle=0.5) as out:
out.append(chunk)thread.post answers the inbound turn (email In-Reply-To). thread.send starts a standalone message. thread.reply quotes a specific id.
Message, Action, Button
Message: kind, thread_id, text, chat_kind, sender, message_id,
attachments, blocks, reply_to, topic_id, metadata, raw
Action: kind, thread_id, data, sender, message_id, interaction_id, metadata, raw
Button: label, data="", url="", style="default"|"primary"|"danger"
Attachment: type, url, file_id, filename, mime_type, size_bytes, captionErrors
ProvisionError on paperwork. handle / listen / poll / run return a list of Result (per-event failure). Hosted HTTP classifies 401/403 AuthRequired, 402 InsufficientCredit, 429 RateLimited.