:rocket: kurier 0.1.0: one API for chat platforms,...
# feed
i
🚀 kurier 0.1.0: one API for chat platforms, Kotlin-native Writing a chat bot is fun. Writing the same bot a second time, because half your users are on Discord and the other half on Telegram, is not. kurier does for chat platforms roughly what JDBC did for databases: you write the bot once against one typed,
Flow
-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:
Copy code
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:
Copy code
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.
Copy code
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
K 5
🔥 4