agentsclimarketplace

Telegram agent router

Skill Thanane15M/telegram-agent-router

Multi-agent Telegram bot architecture with PostgreSQL-native state. Claude Skill.

Install
npx -y skills add Thanane15M/telegram-agent-router

Assembled from the repository path, not quoted from the project. Check it against their README if it does not work.

One thing to look at

  • 0 stars0 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.

What its author says it does

Copied from the file, not written here

Multi-agent Telegram bot architecture with PostgreSQL-native state management. 10+ specialized agents, intent-based routing, persistent cross-session memory, and production-grade conversation state machines. Activate when building or debugging Telegram bots with multiple agents, routing logic, conversation context, or persistent memory. Trigger phrases: "telegram bot agents", "multi-agent telegram", "route messages to agents", "telegram bot router", "conversation state telegram", "persistent memory bot", "telegram intent classification", "agent handoff telegram", "telegram session state", "specialize telegram bot", "telegram webhook architecture".

SKILL.md

14.2 KB, ~3.0k tokens by cl100k_base, as published. Nobody here has run it

Telegram Agent Router — Multi-Agent Architecture

Core thesis: A Telegram bot is not a chatbot. It is a routing layer over a team of specialists, backed by a PostgreSQL state machine. Every message is a dispatch decision. Every conversation is a session. Every session has memory.

This skill documents the architecture, patterns, and production code for building Telegram bots that coordinate multiple specialized AI agents with PostgreSQL-native state, intent-based routing, and persistent cross-session memory.

Targets production multi-agent Telegram deployments with durable PostgreSQL state.


ARCHITECTURE OVERVIEW

User
 │
 ▼ HTTPS (Telegram sends to your endpoint)
┌────────────────────────────────────────────┐
│  WEBHOOK RECEIVER                          │
│  /telegram/webhook                         │
│  Validates token · Deduplicates · Queues   │
└──────────────┬─────────────────────────────┘
               │
               ▼
┌────────────────────────────────────────────┐
│  INTENT CLASSIFIER                         │
│  command → direct dispatch                 │
│  free text → LLM classification           │
│  ambiguous → confidence check             │
└──────────────┬─────────────────────────────┘
               │
               ▼
┌────────────────────────────────────────────┐
│  ROUTER                                    │
│  Loads session state from PostgreSQL       │
│  Selects target agent                      │
│  Injects memory context                    │
│  Dispatches with typed contract            │
└──────────────┬─────────────────────────────┘
               │
        ┌──────┴──────┐
        ▼             ▼
  [Agent A]      [Agent B]  ···  [Agent N]
  Specialized    Specialized
  prompt +       prompt +
  tools          tools
        │             │
        └──────┬──────┘
               ▼
┌────────────────────────────────────────────┐
│  MEMORY WRITER (PostgreSQL)                │
│  Saves: message, agent used, result        │
│  Updates: session state, user preferences  │
│  Emits: NOTIFY for async post-processing   │
└──────────────┬─────────────────────────────┘
               │
               ▼
         Telegram API
         sendMessage()

THE THREE CONSTRAINTS YOU CANNOT IGNORE

Before any architecture decision, internalize these Telegram realities:

1. Webhook Delivery Must Be Idempotent

Telegram can retry webhook delivery after an unsuccessful or interrupted response. Do not depend on a universal fixed timeout: return a successful response quickly, deduplicate by update_id, and process slow LLM work outside the request cycle.

# ❌ WRONG — LLM call blocks webhook, causes retries
@app.post("/telegram/webhook")
async def webhook(update: dict):
    response = await llm.generate(update["message"]["text"])  # 5-8 seconds
    await send_message(response)
    return {"ok": True}

# ✅ CORRECT — Acknowledge immediately, process asynchronously
@app.post("/telegram/webhook")
async def webhook(update: dict):
    await job_queue.enqueue(update)   # < 5ms
    return {"ok": True}              # Return immediately

# Worker processes the job outside the webhook cycle
async def worker():
    while True:
        job = await dequeue()
        response = await llm.generate(job["text"])   # Takes as long as needed
        await send_message(job["chat_id"], response)

2. The 4096 Character Limit

Every message sent via sendMessage has a hard 4096-character cap.

async def send_safe(chat_id: int, text: str) -> list[dict]:
    """Split long responses respecting Telegram's limit and word boundaries."""
    if len(text) <= 4096:
        return [await send_message(chat_id, text)]

    chunks = []
    while text:
        if len(text) <= 4096:
            chunks.append(text)
            break
        # Split at last newline before limit (don't cut mid-sentence)
        split_at = text[:4096].rfind('\n')
        if split_at == -1:
            split_at = text[:4096].rfind(' ')
        if split_at == -1:
            split_at = 4096
        chunks.append(text[:split_at])
        text = text[split_at:].lstrip()

    results = []
    for chunk in chunks:
        results.append(await send_message(chat_id, chunk))
    return results

3. The Rate Limit

Telegram allows max 30 messages/second globally and 1 message/second per chat. A busy bot processing messages faster than this will receive 429 errors.

import asyncio
from collections import defaultdict, deque

class RateLimiter:
    def __init__(self):
        self._locks: dict[int, asyncio.Lock] = defaultdict(asyncio.Lock)
        self._last_sent: dict[int, float] = {}
        self._global_lock = asyncio.Lock()
        self._global_sends: deque[float] = deque()

    async def send(self, chat_id: int, text: str):
        async with self._locks[chat_id]:
            loop = asyncio.get_running_loop()
            now = loop.time()
            elapsed = now - self._last_sent.get(chat_id, 0)
            if elapsed < 1.0:
                await asyncio.sleep(1.0 - elapsed)

            async with self._global_lock:
                now = loop.time()
                while self._global_sends and now - self._global_sends[0] >= 1.0:
                    self._global_sends.popleft()
                if len(self._global_sends) >= 30:
                    await asyncio.sleep(1.0 - (now - self._global_sends[0]))
                    now = loop.time()
                    while self._global_sends and now - self._global_sends[0] >= 1.0:
                        self._global_sends.popleft()
                self._global_sends.append(loop.time())

            await _send_message_api(chat_id, text)
            self._last_sent[chat_id] = loop.time()

POSTGRESQL SCHEMA — THE COMPLETE STATE MACHINE

-- See references/postgres-schema.md for complete DDL with indexes and RLS

Core tables: bot_sessions, bot_messages, bot_agent_registry, bot_memory, bot_job_queue, bot_dispatch_log


ROUTING DECISION TREE

Incoming message
      │
      ├── Is it a COMMAND? (starts with /)
      │         │
      │         ├── Known command → Direct dispatch to registered agent
      │         └── Unknown command → Error + help menu
      │
      ├── Is there an ACTIVE SESSION with locked agent?
      │         │
      │         └── Yes → Continue with same agent (multi-turn flow)
      │
      ├── Is it a CALLBACK QUERY? (inline keyboard button)
      │         │
      │         └── Yes → Dispatch to agent that owns the callback prefix
      │
      └── Free text → Intent classification
                │
                ├── HIGH confidence (> 0.8) → Dispatch to classified agent
                ├── MEDIUM confidence (0.5-0.8) → Ask for clarification
                └── LOW confidence (< 0.5) → Default to orchestrator agent

Full implementation: references/router-patterns.md


AGENT CONTRACT — THE TYPED INTERFACE

Every agent must implement this interface. Non-negotiable.

from dataclasses import dataclass, field
from typing import Optional

@dataclass
class AgentContext:
    user_id: int
    chat_id: int
    session_id: str
    session: dict
    message: str
    recent_history: list[dict]      # Last N messages for context
    user_memory: list[dict]         # Persistent facts about this user
    active_flow: Optional[str] = None  # Multi-step flow identifier

@dataclass
class AgentResponse:
    text: str                        # Response to send to user
    agent_name: str                  # Which agent produced this
    memory_writes: list[dict] = field(default_factory=list)
    lock_session: Optional[str] = None
    unlock_session: bool = False
    follow_up_job: Optional[dict] = None
    flow_state_update: Optional[dict] = None

class AgentBase:
    name: str
    description: str                 # Used by LLM orchestrator for routing
    commands: list[str]              # /commands this agent handles
    intent_keywords: list[str]       # Keywords for fast-path routing
    max_context_messages: int = 10   # How much history to load

    async def handle(self, ctx: AgentContext) -> AgentResponse:
        raise NotImplementedError

    def can_handle(self, message: str) -> float:
        """Returns confidence 0.0-1.0 that this agent should handle message."""
        raise NotImplementedError

Full patterns: references/agent-design.md


MEMORY ARCHITECTURE

Two-tier memory model: Working memory (session-scoped) + Long-term memory (user-scoped).

# Working memory: active in every request, auto-pruned
# Long-term memory: explicit writes, retrieved semantically

async def build_context(
    user_id: int,
    chat_id: int,
    session: dict,
    message: str,
) -> AgentContext:
    async with pool.acquire() as conn:
        # Working memory: last N messages this session
        history = await conn.fetch("""
            SELECT role, content, agent_name, created_at
            FROM bot_messages
            WHERE session_id = $1
            ORDER BY created_at DESC
            LIMIT $2
        """, session["id"], CONTEXT_WINDOW_MESSAGES)

        # Long-term memory: relevant facts about user
        memories = await conn.fetch("""
            SELECT key, value, confidence
            FROM bot_memory
            WHERE user_id = $1
              AND (expires_at IS NULL OR expires_at > NOW())
            ORDER BY accessed_at DESC
            LIMIT 20
        """, user_id)

        return AgentContext(
            user_id=user_id,
            chat_id=chat_id,
            session_id=session["id"],
            session=session,
            message=message,
            recent_history=[dict(r) for r in reversed(history)],
            user_memory=[dict(m) for m in memories],
            active_flow=session.get("locked_flow"),
        )

Full schema and retrieval patterns: references/postgres-schema.md


CONTEXT COMPRESSION FOR LONG CONVERSATIONS

When history exceeds the LLM context window:

async def compress_history(user_id: int, session_id: str, conn, llm) -> str | None:
    """Summarize old messages to stay within context limits."""
    old_messages = await conn.fetch("""
        SELECT role, content FROM bot_messages
        WHERE session_id = $1
          AND created_at < NOW() - INTERVAL '1 hour'
        ORDER BY created_at
        LIMIT 50
    """, session_id)

    if not old_messages:
        return None

    summary = await llm.generate(
        system="Summarize this conversation history in 3-5 bullet points. "
               "Focus on decisions made, facts learned, tasks completed.",
        user="\n".join(f"{m['role']}: {m['content']}" for m in old_messages)
    )

    # Store summary, mark originals as compressed
    await conn.execute("""
        INSERT INTO bot_memory (user_id, key, value, confidence)
        VALUES ($1, 'conversation_summary_' || $2, $3, 0.9)
        ON CONFLICT (user_id, key) DO UPDATE SET value = EXCLUDED.value
    """, user_id, session_id[:8], summary)

    await conn.execute("""
        UPDATE bot_messages SET compressed = true
        WHERE session_id = $1 AND created_at < NOW() - INTERVAL '1 hour'
    """, session_id)

    return summary

PRODUCTION CHECKLIST

Before going live with a multi-agent Telegram bot:

Webhook
□ Webhook URL uses HTTPS (required by Telegram)
□ Secret token validated on every request
□ HTTP 200 returned in < 500ms (job queued, not processed)
□ Update ID deduplicated (Telegram may send duplicates on retry)
□ Webhook registered with setWebhook (verify with getWebhookInfo)

Routing
□ All /commands registered in agent registry
□ Fallback agent defined for unmatched intent
□ Confidence threshold tuned (0.6-0.8 is typical sweet spot)
□ Multi-turn session lock has a max TTL (prevent stuck sessions)

Memory
□ Long-term memory has TTL or explicit expiry
□ Compression runs for sessions > N messages
□ Sensitive data not stored in plaintext memory

Production
□ Connection pool sized correctly (not > PG max_connections / 3)
□ Rate limiter active (1 msg/sec per chat, 30/sec global)
□ Retry logic with exponential backoff on Telegram API errors
□ Dead letter queue for failed jobs
□ Dispatch log for debugging routing decisions

See references/ for complete implementations:

  • postgres-schema.md — Full DDL, indexes, RLS policies
  • router-patterns.md — Intent classifier, dispatch engine, session FSM
  • agent-design.md — Agent contracts, specialization principles, handoff
  • telegram-constraints.md — Webhook patterns, rate limiting, message splitting
  • anti-patterns.md — 8 production failures and their fixes

Gives 0 of the 12 instructions most agent orchestration skills give in ~3.0k tokens

Counted across 742 of the 995 authors here whose files we hold, read 2026-08-06

  • run the full test suite after integrating changesin 53 of 742, across 20 files
  • reference existing artifacts by path or URLin 52 of 742, across 22 files
  • dispatch one agent per independent problem domainin 50 of 742, across 17 files
  • verify fixes do not conflictin 45 of 742, across 13 files
  • include a suggested skills section in the documentin 45 of 742, across 15 files
  • redact sensitive informationin 41 of 742, across 11 files
  • save to the temporary directory of the operating systemin 39 of 742, across 9 files
  • tailor the document to user-provided focus argumentsin 39 of 742, across 9 files
  • spot check agent changes for systematic errorsin 34 of 742, across 7 files
  • write a handoff document summarising the current conversationin 31 of 742, across 6 files
  • assign each agent a specific scopein 23 of 742, across 8 files
  • provide specific scope and clear goalin 23 of 742, across 5 files

Said here and by no other author read

  • return http 200 from webhook in under 500ms
  • deduplicate webhook deliveries by update id
  • queue slow llm processing outside webhook request cycle
  • split messages exceeding 4096 characters
  • enforce global telegram api rate limits
  • enforce per chat rate limits

Grouped from the skills themselves: near-identical wordings counted once, and counted by distinct author, so one author publishing three of these counts once. Length counted with cl100k_base; the agent that loads this file may tokenize it differently.

Keep looking

Skills are one crate of 328,083. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.