ACP remote control surface for Telegram etc.
Project description
ACP Router
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
Run
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 acprouter
ACP Agent Command
ACPROUTER_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 acprouter
For 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 acprouter
For 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 acprouter
Use 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.
Construct Programmatically
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.
Current Commands
/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
Selection Rendering
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.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file acprouter-0.2.0.tar.gz.
File metadata
- Download URL: acprouter-0.2.0.tar.gz
- Upload date:
- Size: 193.9 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: uv/0.11.12 {"installer":{"name":"uv","version":"0.11.12","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
7908a43749610bc7e6e819ddc18f5d99eb7759b74abf77fc4de470cf57e3c7b2
|
|
| MD5 |
23c3be9a80c05f5e77802d00c9f73bb8
|
|
| BLAKE2b-256 |
ae2deeb89b86fa5dd17844fa2aea8bfb1e0644e4601455232a3c65fa038f3635
|
File details
Details for the file acprouter-0.2.0-py3-none-any.whl.
File metadata
- Download URL: acprouter-0.2.0-py3-none-any.whl
- Upload date:
- Size: 37.2 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: uv/0.11.12 {"installer":{"name":"uv","version":"0.11.12","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
1f538bd1aadc92f548fa51aa8ac98298545d689df1d3e376055715c38850e409
|
|
| MD5 |
3e6de7015df7a316c3ca8ab083c50430
|
|
| BLAKE2b-256 |
16d901f99248317fae57092daa121a11d7a98af5a5502eaf1ffb338ffc67a0c8
|