Telegram topic gateway for pi agents.
The gateway watches Telegram bot updates, extracts chat_id and message_thread_id, looks up that pair in config/telepi.yaml, and routes the message to the bound pi session. Unknown topics are ignored so another machine can own them.
- Create a Telegram bot with @BotFather and enable topics for it; telepi routes messages between those bot topics.
- Copy
.env.exampleto.envand setTELEPI_BOT_TOKEN. - Copy
config/telepi.example.yamltoconfig/telepi.yamland adjust; after that, manage it only through thetelepiCLI. npm install
The gateway scopes pi execution per topic with:
--session-id <id>for stable session identity--session-dir <dir>for shared session storage--skill <path>for agent-specificSKILL.mdinstructions--extension <path>for agent-specific pi extensions--model <provider/model>from per-topic config; topics without their own model inherit from the agent's main topic
The gateway also runs pi from the entity folder, so project-local context files such as AGENTS.md are scoped to that entity.
npm install
npm run telepi -- validate
npm run telepi -- telegram:me
npm run telepi -- agents
npm run telepi -- topics
npm run telepi -- iconsCreate an agent entity:
npm run telepi -- agent:create --id helper --name Helperagent:create creates a seed Telegram topic by default, using manager.chat_id from config/telepi.yaml. Extra topics inherit the agent branding unless --style, --avatar, or --color is supplied.
Telegram topic icons are not arbitrary emoji. They must be one of Telegram's forum topic icon stickers; telepi icons lists each available emoji and its custom_emoji_id. If an agent avatar matches one of those emoji, Telepi uses it as icon_custom_emoji_id; otherwise the topic falls back to its color.
Create and bind an additional Telegram topic:
npm run telepi -- topic:create --chat-id <chat_id> --name Helper --agent helperBind an existing topic:
npm run telepi -- topic:bind --chat-id <chat_id> --topic-id <topic_id> --name Helper --agent helperSet a per-topic model (the agent's main topic acts as the default for its other topics). Append a pi thinking level to configure effort for both normal turns and compaction:
npm run telepi -- topic:set-model --name Helper --model <provider/model>
npm run telepi -- topic:set-model --name Helper --model <provider/model>:highSupported levels are off, minimal, low, medium, high, xhigh, and max (subject to model support).
Start a topic with a fresh pi session without deleting old transcript files:
npm run telepi -- session:list --topic Helper --files
npm run telepi -- session:unlink --name Helper --reason "fresh conversation"
npm run telepi -- session:restore --name Helper --session-id helper-1001session:unlink changes only the topic mapping's active session_id; the old .telepi/pi-sessions/*.jsonl files are preserved and recorded under sessions.unlinked in config/telepi.yaml.
Compact a long-running session:
npm run telepi -- session:compact --topic Helper [--instructions "..."] [--model <provider/model>]By default there is no active-session size limit. Configure one through the CLI:
npm run telepi -- session:limit --size 32MiB
npm run telepi -- session:limit # show the current limit
npm run telepi -- session:limit --unlimitedAfter a successful compaction, a transcript at or above the configured limit is checkpointed into a fresh session containing only pi's retained tail, the successful compaction entry, and subsequent context entries. The topic is relinked only after the checkpoint produces the same model-facing message context. The original transcript remains unchanged in session history and the new header records it as parentSession.
Send a direct Telegram message to a topic without invoking pi or the gateway:
npm run telepi -- topic:send --topic Helper --text "..." [--reply-to <message-id>] [--quote]Send a prompt into a topic from the command line (used by timers and scripts):
npm run telepi -- topic:prompt --topic Helper --text "..." [--no-echo] [--from "Name"] [--source "provenance"]The gateway also owns a few Telegram slash commands directly:
/help— show gateway-owned commands./compact [instructions]— compact the current topic's pi session./retry— rerun the last non-command prompt remembered for that chat/topic.
For cron jobs, webhooks, or local scripts that should not shell out to the CLI, enable a prompt inbox with environment variables in the gateway service:
TELEPI_PROMPT_INBOX_DIR=/absolute/path/to/prompt-inbox
TELEPI_PROMPT_INBOX_TOPIC=Helper # optional default topic name
TELEPI_PROMPT_INBOX_INTERVAL_MS=60000 # optional; minimum 1000The inbox processes one .txt file at a time. Files may start with Topic: Helper to override the default topic; empty files are deleted, busy topics leave files queued, and invalid files are renamed to .failed to avoid loops.
Run the gateway:
npm run gatewayWhile a routed pi session is running, the gateway sends Telegram typing chat actions scoped to the topic. It does not rename topics for transient status; topic edits are reserved for real metadata changes.
Agent runs have no destructive timeout by default. A model request or long-running tool can legitimately remain silent, so absence of pi events is not treated as failure; the topic stays busy until the run finishes or the user cancels it. Operators can explicitly set idle_timeout_ms for legacy idle-stop behavior or hard_timeout_ms for an absolute maximum runtime.
By default, Telegram receives only assistant text messages. To expose more pi stream message kinds, add an explicit allowlist under telegram, an agent, or a specific topic; topic settings override agent settings, and agent settings override global Telegram settings:
telegram:
display_messages:
- assistant/message
- custom
- custom/something
- toolCategory entries such as assistant, custom, or tool include all subtypes in that category. Specific entries such as assistant/reasoning or custom/something include only that subtype. Agent overrides can be managed without hand-editing config:
telepi agent:display-messages --id <agent> --set assistant/message assistant/reasoning
telepi agent:display-messages --id <agent> --clearEnabled assistant/reasoning updates are coalesced into one italic Telegram message per user turn. Later reasoning summaries edit and append to that message instead of creating a noisy sequence; a continuation is used only if Telegram's message-size limit is reached.
Outbound messages are converted from model markdown to Telegram MarkdownV2 using a vendored copy of telegram-markdown-v2 (see vendor/). If conversion fails or inflates a chunk past Telegram's 4096-char limit, the chunk is sent as plain text instead — models are never restricted in what they can output.
Inbound Telegram photos, image documents, and other uploads are downloaded under .telepi/cache/ and listed in the prompt with their local path, filename, MIME type, and size. Agents view them with pi's read tool — vision-capable models see image content by reading the file path; nothing is inlined as base64.
Outbound images are sent by a pi extension tool:
telepi_send_image(file_path, caption?)
The gateway loads extensions/telepi-telegram.ts for each routed session and passes the current chat_id, topic_id, and message id through environment variables, so agents normally only need to provide file_path and optionally caption.
extensions/telepi-telegram.ts also registers telepi_buttons(text, buttons) for agents that should offer tappable choices:
buttonsis a flat list of up to 24 entries; rows are laid out automatically.- Each entry is a plain string (label doubles as the payload),
{label, data}when the routed-back payload should differ, or{label, url}for link buttons. - A tap is answered, the keyboard is removed (one-shot), and the label/
datais routed back into the same topic session as a normal user reply.
Button state lives in .telepi/cache/button-callbacks.jsonl, one record per sent message.
Install as a systemd user service after adjusting paths in systemd/telepi-gateway.service if this repo is not at ~/telepi, Node is not in ~/.nix-profile/bin, or pi is not in ~/.npm/bin. The unit sets an explicit runtime PATH and TELEPI_PI_BIN so it does not depend on a login shell or a desktop session importing environment variables after boot:
mkdir -p ~/.config/systemd/user
cp systemd/telepi-gateway.service ~/.config/systemd/user/
systemctl --user daemon-reload
systemctl --user enable --now telepi-gatewayOnly one long-polling process can use a Telegram bot token at a time; stop any other consumer of the same token first.