Ilya Nemtsev
07/29/2026, 3:21 PMFlow-based API, and per-platform adapters do the unglamorous work of message normalization, rich-text dialects, threading, reconnection, and rate limits.
Five adapters ship in 0.1.0: Telegram, Discord, Matrix, Twitch, and Slack. The same handler runs on all of them:
kotlin
val gateway = chatGateway {
install(TelegramAdapter(token = System.getenv("TG_TOKEN")))
install(DiscordAdapter(token = System.getenv("DISCORD_TOKEN")))
install(SlackAdapter(botToken = System.getenv("SLACK_BOT_TOKEN"), appToken = System.getenv("SLACK_APP_TOKEN")))
}
gateway.start()
gateway.messages.collect { msg ->
if (msg.isDirectedAtBot) msg.reply(agent.run(msg.text))
}
The flagship feature is streaming LLM replies. reply(tokens: Flow<String>) progressively edits a single message as tokens arrive, throttled to each platform's safe edit rate, giving you the "message types itself" effect without hand-rolling an edit scheduler and rate-limit dance per platform:
kotlin
val tokens: Flow<String> = llm.stream(prompt) // any Flow<String>
msg.reply(tokens) // one message, progressively edited
Where editing isn't available (Twitch) it degrades to a single buffered send with a live typing indicator, and if the token flow dies mid-stream the partial message is finalized with what arrived.
A few design decisions to know:
- No webhook server anywhere. Every adapter is outbound-only (long-poll, /sync, EventSub WS, Socket Mode), so a bot runs from a laptop or behind NAT with no public URL and no tunnel.
- No formatting-injection surface. Rich text is a typed AST rendered through each platform's structured API (Telegram entities, Slack mrkdwn), never generated markup. A user who names themselves **bold** stays plain text instead of shouting.
- Capabilities are queried, not assumed. channel.supports(Capability.EDITING). Unsupported operations degrade to no-ops rather than throwing, so there's no lowest-common-denominator API.
- Testable with no network. kurier-testing ships a `FakeAdapter`: no tokens, no sleeps, synchronization is structural.
- Android-safe by construction. core is pure Kotlin with coroutines as its only dependency, JVM 17 target, no reflection-heavy deps. It's written to be mechanically promotable to KMP commonMain.
Working example below: kurier-concierge runs a Koog agent with a RAG knowledge base and streams its answers to Telegram, Discord, and Slack from one process.
kotlin
implementation("com.eventslooped:kurier-runtime:0.1.0")
implementation("com.eventslooped:kurier-adapter-telegram:0.1.0")
This is the first public release. The public surface is locked by a binary-compatibility check and breaking changes go through a deprecation cycle, but for pre-1.0 the API still evolves additively, so feedback on the core shape is exactly what I'm after.
This is library was born as part of a larger project I am building with Koog. Please show some love with some stars/suggestions.
I will do LINE client next, but that one is more tricky because LINE servers need to be able to reach me via webhook.
github.com/inemtsev/kurier
github.com/inemtsev/kurier-concierge