npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

telegram-agent-kit

v0.2.0

Published

Render markdown to Telegram, stream agent replies into live drafts, and drive a snapshot/rollback turn-loop — runtime-agnostic.

Readme

telegram-agent-kit

Wire LLM agents to Telegram: render Markdown, stream replies into a live native draft, and drive a snapshot/rollback turn-loop — over thin injected interfaces, with zero runtime dependencies in the core.

npm license types

ESM-only. Runs on Node 18+, Bun, Deno, and the browser (the formatting core).


Why this exists

Connecting an LLM agent to a Telegram bot looks simple until you hit the edges:

  • LLMs emit arbitrary Markdown — Telegram speaks a small, strict HTML subset. A single unclosed tag makes the Bot API reject the whole message.
  • Token streaming into a live draft means you render partial Markdown many times a second, where marks and fences are routinely mid-token.
  • A failed turn must roll back cleanly so the conversation thread isn't corrupted.

telegram-agent-kit solves these once. Notably, it's the only JS package that renders Markdown to Telegram-HTML with totality (never throws on arbitrary LLM output) and a streaming/partial mode (auto-closes unclosed marks and fences, so a live draft never flashes broken markup).

It is runtime-agnostic: you supply all I/O through small injected interfaces, and the kit owns the orchestration. No HTTP client, no framework, no globals.

Features

  • Markdown → Telegram-HTML that never throws, with a partial mode for live drafts.
  • Live draft streaming — a throttle / keepalive / typing-heartbeat / drain state machine that animates one native Telegram draft from a growing string.
  • Turn-loop orchestrationsnapshot → stream → animate → finalize → reply, with rollback on error and a guarantee it never throws out.
  • Resilient send path — automatic chunking, surrogate-safe splitting, and deterministic 400 fallbacks (rich → HTML → plain text; photo → text).
  • Optional deepagents adapter on a separate subpath, so the core never pulls in langchain.

Installation

npm install telegram-agent-kit

The optional telegram-agent-kit/deepagents subpath needs its peers — install them only if you use it:

npm install @langchain/core deepagents

Quick start

Implement a thin BotClient over the Bot API, then drive one turn per incoming message. The example uses the deepagents adapter, but any AgentStream works.

import { runTelegramTurn, TelegramApiError, type BotClient } from 'telegram-agent-kit';
import { toAgentStream } from 'telegram-agent-kit/deepagents';

// 1. Raw Bot API primitives — one HTTP call each. Throw TelegramApiError on a
//    Bot API error so the kit's deterministic-400 fallbacks can fire.
const client: BotClient = {
  sendMessage:        (p, signal) => call('sendMessage',        { chat_id: p.chatId, text: p.text, parse_mode: p.parseMode }, signal),
  sendRichMessage:    (p, signal) => call('sendMessage',        { chat_id: p.chatId, text: p.markdown },                       signal),
  sendPhoto:          (p, signal) => call('sendPhoto',          { chat_id: p.chatId, photo: p.url, caption: p.caption, parse_mode: p.parseMode }, signal),
  sendChatAction:     (p, signal) => call('sendChatAction',     { chat_id: p.chatId, action: p.action ?? 'typing' },          signal),
  sendMessageDraft:   (p, signal) => call('sendMessageDraft',   { chat_id: p.chatId, draft_id: p.draftId, text: p.text },     signal),
  sendRichMessageDraft:(p, signal) => call('sendRichMessageDraft',{ chat_id: p.chatId, draft_id: p.draftId, text: p.markdown },signal),
};

// 2. Drive one turn.
await runTelegramTurn({
  chatKey: { chatId, agentId: 'main' },
  userText,
  draftId: updateId,        // non-zero, unique per turn — reused for every draft write
  rich: true,
  client,
  agentStream: toAgentStream(agent),   // your deepagents agent
  checkpointer: {
    snapshot: (threadId)     => saver.snapshotId(threadId),
    rollback: (threadId, id) => saver.rollbackThread(threadId, id),
  },
  threadStore: {
    resolve: (key)      => threads.resolve(key.chatId, key.agentId),
    touch:   (key, now) => threads.touch(key.chatId, key.agentId, now),
  },
  log: console,
});

// Thin transport helper.
async function call(method: string, body: unknown, signal?: AbortSignal) {
  const res = await fetch(`https://api.telegram.org/bot${TOKEN}/${method}`, {
    method: 'POST',
    headers: { 'content-type': 'application/json' },
    body: JSON.stringify(body),
    signal,
  });
  if (!res.ok) {
    const j = await res.json().catch(() => ({}));
    throw new TelegramApiError(res.status, j.description);
  }
}

Note: sendRichMessage / sendRichMessageDraft map to your bot's rich-text transport. If your bot has no rich endpoint, point them at plain sendMessage and set rich: false — the kit then renders via HTML only.

How it works

The kit is three layers plus one optional adapter. The dependency direction is strictly Bridge → Draft → Formatting; lower layers never import higher ones, and the core never imports the adapter.

┌─────────────────────────────────────────────────────────────┐
│  /deepagents  (optional subpath)                            │
│  toAgentStream · streamAgent  →  AgentStream                │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│  Bridge       runTelegramTurn · sendReply · sendText        │
│               + the four interfaces you implement           │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│  Draft        createDraftStreamer (throttle/keepalive/drain)│
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│  Format       mdToTelegramHtml · chunk* · rich helpers      │
│               pure, zero deps, browser-safe                 │
└─────────────────────────────────────────────────────────────┘

Transport vs. orchestration. You provide raw Bot API primitives — one HTTP call each, no chunking, rendering, or fallback. The kit owns all orchestration over them: HTML rendering, chunking, the rich → classic and photo → text 400 fallbacks, and the trailing-cover photo flow.

The four interfaces

You implement these; the kit drives them.

| Interface | Shape | Role | | -------------- | --------------------------------------------------------------------- | ---- | | BotClient | sendMessage, sendRichMessage, sendPhoto, sendChatAction, sendMessageDraft, sendRichMessageDraft | Raw transport. Each throws TelegramApiError on a Bot API error. | | AgentStream | (input, { threadId, signal, configurable }) => AsyncIterable<RenderEvent> | Your agent. threadId must reach it so the checkpointer writes to the snapshotted thread. configurable is an optional pass-through bag forwarded verbatim from runTelegramTurn's configurable option. | | Checkpointer | { snapshot(threadId), rollback(threadId, id) } | Per-thread snapshot/rollback for clean recovery on a failed turn. | | ThreadStore | { resolve(chatKey, now), touch(chatKey, now) } | Maps { chatId, agentId } to a thread id (so two bots over one chat id don't collide). |

A RenderEvent is one of token, tool_start, tool_end, or error. The kit appends token text to the live draft and treats an error event as a rollback.

API reference

Core entry — telegram-agent-kit

Formatting (pure, zero deps)

  • mdToTelegramHtml(md, opts?) — Markdown → Telegram-HTML. Never throws. opts.partial auto-closes unclosed marks/fences for live drafts.
  • chunkText(text) / safeSlice(text, max) / chunkRich(md) — surrogate-safe splitting (classic limit 4096, rich limit 32768).
  • repairRichTables(md) · neutralizeRichMedia(md) · extractTrailingCover(reply) — rich helpers.

Draft engine

  • createDraftStreamer(deps){ start(), push(fullText), finalize(), abort() }.
  • DEFAULT_DRAFT_CONSTANTS / DraftConstants — overridable tunables (throttle, keepalive, typing heartbeat, preview cap, drain, …).

Bridge

  • runTelegramTurn(opts) — orchestrate one turn. Never throws out; every failure is caught and logged. Accepts an optional configurable bag forwarded to your AgentStream as context.configurable, for passing per-turn data (e.g. pendingImages) to the agent without widening the core input type.
  • sendReply(client, chatId, reply, opts, signal?) / sendText(...) — the send path on its own.
  • Types: BotClient, AgentStream, Checkpointer, ThreadStore, RenderEvent, ChatKey, Logger.

Errors

  • TelegramApiError — throw this from BotClient primitives (carries error_code).
  • isBadRequest(err) — true only for a deterministic 400 (rejected, safe to retry on a degraded path).

Optional entry — telegram-agent-kit/deepagents

  • toAgentStream(agent)AgentStream — adapts a deepagents/langgraph agent to the kit's contract. The context.configurable bag is merged into the LangGraph RunnableConfig, but the reserved keys thread_id, thread_ts, checkpoint_id, checkpoint_ns, checkpoint_map, and run_id — plus any __pregel_* LangGraph internal-execution key — are stripped so the kit retains full control over checkpoint routing and execution.
  • streamAgent(agent, input, config, signal?) — lower-level event stream if you need direct control.

@langchain/core and deepagents are type-only, optional peers. The built /deepagents bundle contains no runtime import of either, so the core stays dependency-free.

Design guarantees

These are intentional and enforced by tests:

  • mdToTelegramHtml is total — it never throws on any LLM output. The send path also wraps it and falls back to plain text as defence-in-depth.
  • Deterministic 400 fallbacks keyed off isBadRequest: rich → classic HTML → plain text, and photo → text. Any non-400 error always propagates.
  • runTelegramTurn never throws out — snapshot happens only after a turn isn't skipped, rollback fires only on a real failure, and draft teardown is idempotent.
  • Surrogate-safe splitting — chunking never severs a UTF-16 surrogate pair.

Development

npm run build       # tsup → dist/ (ESM + .d.ts) for both entry points
npm run typecheck   # tsc --noEmit
npm test            # vitest run
npm run lint        # biome check .
npm run format      # biome format --write .

Run a single test:

npx vitest run test/format/md-to-html.test.ts     # one file
npx vitest run -t "renders nested bold"           # by name

Build before testing if you touch /deepagents: one test greps the built bundle to prove it carries no runtime import of the optional peers (it skips when dist/ is absent). CI runs build before test for this reason.

Contributing

Issues and pull requests are welcome. Before opening a PR, please run npm run lint, npm run typecheck, and npm test — and npm run build if your change touches the /deepagents entry. See CHANGELOG.md for the project history.

License

MIT © Ivan Kalinichenko