Anvia Channels enters beta
Five TypeScript packages that connect Discord, Slack, and Telegram to Anvia agents and to ordinary application code, released today in beta.
By Anvia TeamToday we are releasing Anvia Channels in beta: five TypeScript packages that connect Discord, Slack, and Telegram to Anvia agents — and to ordinary application code that has no agent at all.
The packages are on npm as version 0.1.x, MIT licensed, and published with provenance through npm trusted publishing. This post explains what they do, which decisions they make for you, and which decisions they deliberately leave with your application.
The gap between an agent and a conversation
An Anvia agent is only useful where the conversation actually happens. For a lot of products, that is a Discord server, a Slack workspace, or a Telegram chat — places where users already are, already authenticated, and already typing.
Connecting an agent to those platforms is genuinely more work than it sounds. Each platform has its own SDK, its own event shape, its own length limits, its own way of expressing buttons, threads, typing indicators, and file uploads. A first version of such a bridge is easy to write. Keeping it correct across reconnects, duplicate deliveries, rate limits, and mid-stream edits is not.
Anvia Channels is that bridge, split into the parts that are shared and the parts that are per-platform.
Five packages, two layers
The foundation is @anvia/channel: platform-neutral addresses, portable messages, normalized events, and delivery helpers. It has no dependency on @anvia/core and no dependency on any platform SDK, so application code written against it works with more than one adapter.
On top of that sit three adapters — @anvia/discord (Gateway input, REST delivery), @anvia/slack (Socket Mode input, Web API delivery), and @anvia/telegram (long polling or webhook input, Bot API delivery) — plus @anvia/channel-agent, which connects one adapter to one Anvia agent so you do not write your own receive-filter-prompt-run-stream-send loop.
Run an agent where people already talk
Create an adapter, hand it and an existing agent to createChannelAgent(), and start the service:
import { createChannelAgent } from "@anvia/channel-agent";
import { telegram } from "@anvia/telegram";
const channel = telegram({ token: process.env.TELEGRAM_BOT_TOKEN! });
const service = createChannelAgent({
channel,
agent,
streaming: { placeholder: "Thinking…" },
});
await service.start();
process.once("SIGTERM", () => {
void service.stop();
});
The bridge owns the mechanics by default:
- Filtering: direct messages, group messages that mention the bot, and replies to the bot are handled; bot-authored events never start a run. Override
shouldHandlefor product-specific routing. - Sessions: runs inside one conversation or thread are serialized, so two messages from the same chat never interleave.
- Streaming: when the model advertises streaming, the placeholder message is edited in place as text arrives. Adapters without edit support buffer the response into a single send instead.
- Long output: final messages are split per platform limits, with actions and attachments landing on the last part.
- Multimodal input: files become multimodal prompts, with authenticated bytes loaded only during prompt preparation and bounded by an explicit attachment policy.
- Approvals and questions: when a run suspends for a tool approval or question, the continuation is stored server-side, native buttons are used where the platform supports them, and text replies remain the fallback.
SqliteChannelAgentInteractionStorekeeps paused interactions across restarts. - Shutdown:
stop()aborts new work, stops the adapter, drains queued conversations, and cleans up dangling streaming placeholders.
Conversation memory stays where it belongs: the agent's memory store is configured through @anvia/core, and the bridge only chooses the session scope — private history per sender by default, or shared history across a channel with channelConversationSession.
Proactive delivery, no agent required
Half of the useful traffic in a chat platform is not a conversation at all. It is an alert, a scheduled report, a monitor that noticed something. For that direction, a worker needs an adapter and sendChannelMessage() — receiving never has to be started:
import { sendChannelMessage } from "@anvia/channel";
import { discord } from "@anvia/discord";
const channel = discord({ token: process.env.DISCORD_BOT_TOKEN! });
await sendChannelMessage(
channel,
{ platform: "discord", conversationId: process.env.DISCORD_CHANNEL_ID! },
{
text: monitoringReport,
actions: [{ id: "incident:ack", label: "Acknowledge", style: "primary" }],
},
);
sendChannelMessage() is the boundary helper: it splits long text according to the selected platform and sends the parts sequentially. If a later part fails, it throws PartialDeliveryError carrying the sent prefix and the failed part, so callers can resume without resending what was already delivered.
One event shape across three platforms
Every adapter runtime-validates its external payloads before normalizing them into a ChannelEvent discriminated union — message, action, message-edited, message-deleted, and reaction — with the original validated value preserved as event.raw. Platform SDK types stay inside the adapter packages.
The standard adapters advertise their exact support through channel.capabilities:
- Discord: Gateway receive, files, buttons, replies and threads, typing, plus edits, deletion, and reactions.
- Slack: Socket Mode receive, files, Block Kit buttons, replies and threads, plus edits, deletion, and reactions — no typing indicator, because Slack does not expose one.
- Telegram: polling or webhook receive, files, inline keyboards, replies and threads, typing, plus edits, deletion, and reactions.
Generic code checks channel.capabilities before presenting a feature, and a custom text-only adapter may omit capabilities entirely. Building your own adapter is a documented path: implement Channel<RawEvent>, keep the platform SDK behind it, and the bridge works unchanged.
What beta means
The runtime contracts here — addresses, messages, events, capabilities, the agent bridge — are the ones we intend to keep. But this is a 0.1 release, and during beta we may still adjust APIs in response to how the adapters behave against live platforms. Pin exact versions in anything production-like, and read the changelog before upgrading.
Install with pnpm add @anvia/channel @anvia/channel-agent @anvia/telegram (or @anvia/discord, @anvia/slack), browse the repository, follow the end-to-end guide, or run one of the complete example agents for Discord, Slack, and Telegram with OpenAI models and persistent SQLite conversation memory.