acprouter is an ACP client surface for driving ACP agents from Telegram.
Current MVP scope:
- connect to an ACP agent process over stdio
- bind one active ACP session per Telegram chat
- create, load, and list sessions
- stop active runs with
/stop - project ACP approvals, plan updates, tool updates, and agent replies into Telegram
- project ACP-exposed selection state such as current mode, current model, config options, and available slash commands
- answer ACP approval requests with inline buttons
- default to server-owned ACP truth: the ACP server owns tool execution, guardrails, approvals, and projection payloads
- optionally serve ACP client-owned file and terminal requests against the local workspace when the connected ACP server expects them
You can create a Telegram app from here to obtain an API id and hash. Finally, you will need to create a bot from BotFather and keep the bot token.
Environment variables:
| Variable | Required | Purpose |
|---|---|---|
ACPROUTER_COMMAND |
yes | Stdio ACP server command that ACP Router launches and talks to. |
TELEGRAM_API_ID |
yes | Telegram API id for the bot client session. |
TELEGRAM_API_HASH |
yes | Telegram API hash for the bot client session. |
TELEGRAM_BOT_TOKEN |
yes | Telegram bot token from BotFather. |
ACPROUTER_WORKSPACE_ROOT |
no | Workspace root for client-owned file and terminal requests. Defaults to the current working directory. |
ACPROUTER_STATE_DIR |
no | Local state directory for chat/session bindings. Defaults to .acprouter-state under the workspace root. |
ACPROUTER_AGENT_CWD |
no | Working directory for the ACP subprocess. Defaults to the workspace root. |
ACPROUTER_TELEGRAM_SESSION |
no | Telegram client session name. Defaults to acprouter-bot. |
ACPROUTER_TELEGRAM_BUSINESS_CONNECTION_ID |
no | Optional Telegram business connection id. |
ACPROUTER_STDIO_BUFFER_LIMIT_BYTES |
no | Stdio buffer limit for the ACP subprocess. Defaults to 50 MiB. |
ACPROUTER_ENABLE_HOST_TOOLS |
no | Enables ACP client-owned file and terminal methods. Defaults to true. |
ACPROUTER_STREAMING_DEFAULT |
no | Enables incremental Telegram reply edits by default. Defaults to false. |
ACPROUTER_STREAMING_EDIT_INTERVAL_SECONDS |
no | Minimum interval between Telegram streaming edits. Defaults to 1.0. |
ACPROUTER_LOG_LEVEL |
no | Python logging level. Defaults to INFO. |
ACPROUTER_AGENT_COMMAND is still accepted as a legacy name for ACPROUTER_COMMAND, but new
setups should use ACPROUTER_COMMAND. No surface selector is required; ACP Router is Telegram-only.
ACPROUTER_ENABLE_HOST_TOOLS defaults to true. This only affects ACP client-owned file and terminal requests. It does not make ACP Router invent projection truth or override server-owned tool semantics.
When client-owned host tools are enabled:
- file paths must stay inside the configured workspace root
- file paths must stay inside the active session cwd
- command cwd must stay inside the active session cwd
- read, write, and execute requests require Telegram approval
- rejected requests return a reason back to the ACP server
If you explicitly set ACPROUTER_ENABLE_HOST_TOOLS=false, ACP Router returns a rejection reason for ACP client-owned host requests instead of executing them.
ACPROUTER_STREAMING_DEFAULT defaults to false. This is a Telegram render preference, not ACP state. Leave it off to buffer agent chunks and send the final reply once. Turn it on only when you want incremental Telegram edits.
Run:
uv run acprouterACPROUTER_COMMAND must start an ACP server over stdio. It can point at any command that speaks ACP
JSON on stdout and writes logs to stderr.
For a local Pydantic AI agent adapted by ACP Kit, point the command at the agent symbol:
export ACPROUTER_COMMAND='acpkit run examples.pydantic_acp_agent:agent'
uv run acprouterFor a local LangChain graph adapted by ACP Kit, point the command at the graph symbol:
export ACPROUTER_COMMAND='acpkit run examples.langchain_acp_graph:graph'
uv run acprouterFor an in-process Python integration, create an ACP agent object with ACP Kit and pass it directly to the Telegram gateway:
from acprouter import TelegramGateway
from pydantic_acp import create_acp_agent
acp_agent = create_acp_agent(agent=pydantic_agent)
gateway = TelegramGateway.from_acp_agent(acp_agent, telegram_settings())
await gateway.run()See examples/acprouter_with_acpkit_instance.py for the full version.
For a remote ACP server exposed over WebSocket, run the server with ACP Kit or acpremote, then mirror that endpoint back into a local stdio ACP boundary:
# Remote host
acpkit serve examples.pydantic_acp_agent:agent --host 0.0.0.0 --port 8080
# Machine running acprouter
export ACPROUTER_COMMAND='acpkit run --addr ws://remote.example.com:8080/acp/ws'
uv run acprouterUse acpkit serve ... when the target is a Python runtime that ACP Kit should resolve into an
adapter. Use acpremote.serve_acp(...) or acpremote.serve_command(...) when the target already is
an ACP agent or a stdio ACP command and only needs WebSocket transport.
Concrete examples:
examples/pydantic_acp_agent.pyexamples/langchain_acp_graph.pyexamples/acprouter_with_acpkit_instance.pyexamples/acpremote_bridge_server.pyexamples/acprouter_remote_acp.py
More detail is in docs/getting-started/agent-server.md.
Use TelegramGateway.from_settings(...) when acprouter should spawn the ACP subprocess from
ACPROUTER_COMMAND:
from __future__ import annotations as _annotations
import asyncio
from acprouter import AppSettings, TelegramGateway
settings = AppSettings.from_env()
gateway = TelegramGateway.from_settings(settings)
asyncio.run(gateway.run())Use TelegramGateway.from_acp_agent(...) when another part of the process already owns the ACP
agent:
from __future__ import annotations as _annotations
import asyncio
from acprouter import AppSettings, TelegramGateway
settings = AppSettings.from_env()
gateway = TelegramGateway.from_acp_agent(acp_agent, settings)
asyncio.run(gateway.run())Notes:
ACPROUTER_COMMANDmust launch an ACP agent that writes ACP JSON only tostdout.- Agent logs should go to
stderr, otherwise the ACP stdio connection will break.
Contributor setup and local quality commands are documented in CONTRIBUTING.md. Gateway construction details are documented in docs/gateway.md.
/newcreate and bind a fresh ACP session/new <name>create and bind a fresh ACP session, then store the alias for the current chat/sessionshow the active session id for the current chat/mode <mode-id>switch the active session mode/<mode-id>shorthand for switching directly into a mode such as/ask,/plan, or/agent- dynamic ACP commands
ACP Router also registers ACP-exposed selection and command surfaces as Telegram commands when the names are Telegram-safe. For example, ACP mode, model, and select-style config surfaces can appear as commands such as
/agent,/model <provider:model>, or/thinking <medium|high>. If an ACP name is not Telegram-safe, ACP Router publishes a Telegram alias such as/read_onlyfor ACP/read-onlyand routes it back to the original ACP name. /switch <session-id-or-alias>load and bind an existing session/sessionslist available sessions/stopcancel the current run/streaming <true|false>toggle incremental Telegram reply edits for the current chat
When the ACP server exposes mode, model, config, or command-selection state, ACP Router renders a Telegram projection card for that session and turns Telegram-safe selections into commands.
Typical examples:
- ACP current mode becomes visible in the session selections card
- ACP current model becomes visible in the same card
- ACP config options such as
thinking=mediumrender as selection rows - ACP commands with unstructured input hints can appear as commands such as
/thinking medium|high
ACP Router does not invent these surfaces. If the ACP server does not expose them, there is nothing to render or register.