Communication channel between AI agents and chat platforms (Telegram, Matrix, โฆ)
Project description
waggle ๐
Communication channel between AI agents and chat platforms. Like the bee waggle dance โ a precise, repeatable signal that gets the message across.
waggle is a thin runtime that wires one agent to many chat platforms. It carries messages between the two, nothing more โ it does not decide what the agent should remember, when it should speak, or which tools it can call.
The product question we are answering: "Where should the chat-side plumbing of an AI agent live?" Today it is half inside the agent, half inside ad-hoc plugins. waggle pulls that plumbing into one well-scoped layer that one human can control.
Design pillars
- One agent, many channels. The same agent answers from Telegram, Matrix, Discord, โฆ and always carries a single, unified context โ never split into per-chat or per-topic conversations.
- Strict scope: the chat layer only. Scheduling, tool registries, multi-agent supervision, persistent message archives โ all out of scope. They live upstream or downstream of waggle, not inside it.
- Owner is sovereign. Exactly one human is the owner. Only the owner can restart the agent, reset a session, or compact memory. Everyone else just chats.
- External world can knock. Alerting tools, CI, or schedulers can push prompts into the agent's active chat through authenticated triggers โ but they can never invoke owner-only operations.
- Tweakable settings, stable runtime. Most configuration changes apply hot. Restarts are rare and predictable.
Mental model
external trigger โโ
โโโ waggle โโ agent โโ waggle โโ chat platforms
chat platforms โโโโ
waggle sits in the middle. Inputs (chat messages, external triggers) flow in. Outputs (replies, reactions, edits) flow out. Owner-only operations (restart, status, โฆ) are a side-channel only the configured owner can reach.
Capabilities at a glance
waggle MUST:
- Reach users across multiple chat platforms (Telegram day 1; Matrix / Discord / โฆ later) and run several in parallel for the same agent
- Receive every incoming message labelled with platform, chat, and sender; surface attachments to the agent; persist accepted messages to an external store for the dashboard to render
- Send replies, reactions, edits, and selectable-choice prompts (inline keyboards / quick replies) where the platform supports them โ degrade gracefully where it doesn't
- Accept authenticated external triggers (alerts, CI, schedulers) that inject a prompt into the agent's active chat
- Enforce a dual allow-list (group and user); silently drop rejected messages and emit the rejection event to an external observability channel
- Read all settings from a single configuration source; allow most changes to apply hot
- Expose owner-only operations to a single configured human: agent supervision (status, restart, pause), session management (fresh session, abort turn), and CLI conversation-compact
- Proactively notify the owner when the agent's session or conversation changes by itself (auto-compaction, crash restart, timeout abort, recovery reset, โฆ)
Non-goals
- Cron / scheduling โ use a separate scheduler that fires our inject trigger
- Managing more than one agent โ waggle owns exactly one
- Tool registry / MCP server โ waggle is the chat layer, not the tool layer
- CLI switching (Claude / Codex / Gemini) โ that is the agent's concern
- Persistent message history beyond what the chat platform itself stores
- Splitting forum-group topics into separate conversations โ the agent always has a unified context across every chat
- Exposing owner-only operations to anyone other than the owner
For the full specification with examples and constraints, see docs/REQUIREMENTS.md.
Inspiration & why a separate project
The design is inspired by PleasePrompto/ductor, an excellent multi-transport agent runtime that controls Claude Code / Codex / Gemini from Telegram & Matrix. Reading ductor's source clarified a lot of the right-shape ideas โ the channel-context tag, the webhook inject pattern, the transport-agnostic core.
We chose to build waggle separately rather than fork or reuse ductor for three reasons:
- Scope discipline. ductor bundles transport, session management, memory, cron, sub-agents and multi-CLI proxying into one runtime. We want only the leftmost slice โ the channel layer โ and to keep every other concern out by construction. Forking would mean stripping more than we add.
- Deployment shape. ductor targets a single local machine via
pipx. Our deployment is multi-pod Kubernetes with one agent per StatefulSet and per-bot RBAC; the channel runtime needs to be embedded as a sidecar or library inside each agent, not run as a separate user-installed daemon. - Wire-format ownership. We want the channel tag, the inject contract, and the auth model to be ours to evolve without staying compatible with an upstream that has a broader audience and different priorities.
ductor remains a reference and a benchmark โ we will deliberately reuse its proven shapes (transport interface, inject wire format) where they fit.
Install
pip install "waggle-chat[telegram]"
That's it. The [telegram] extra pulls in aiogram. Use [matrix] for matrix-nio, [telegram-test] for the pyrogram test harness, [dev] for tests + linters. The Python module is imported as waggle (the PyPI distribution name is waggle-chat because waggle was already taken on PyPI by an unrelated 2018 project).
waggle assumes the Anthropic claude-code CLI is on PATH โ install it separately:
npm install -g @anthropic-ai/claude-code
Dev install from source
git clone https://github.com/Tiny-Hive/waggle.git
cd waggle
pip install -e ".[telegram,dev]"
pytest tests/ # 230+ unit tests across 13 modules
Without PyPI (pin to a GitHub release / tag)
# from a specific GitHub release
pip install "waggle-chat[telegram] @ https://github.com/Tiny-Hive/waggle/releases/download/v0.7.12/waggle_chat-0.7.12-py3-none-any.whl"
# or straight from the git tag
pip install "waggle-chat[telegram] @ git+https://github.com/Tiny-Hive/waggle.git@v0.7.12"
Quick start
# 1. Get a Telegram bot token from @BotFather
# 2. Find your numeric user_id (talk to @userinfobot)
export WAGGLE_TG_BOT_TOKEN=<your bot token>
export WAGGLE_OWNER_ID=<your numeric user_id>
# Single-file echo demo (no claude โ just proves the pipeline)
python -m waggle.cli # or: python examples/echo_bot.py
/start the bot in Telegram and send a message โ it should echo back. The
echo bot exercises the full waggle pipeline (access gate, rate limit, accept
events) but skips the claude subprocess. Use it as a smoke test before
deploying the real agent.
Deploy to Kubernetes
A reference manifest lives at deploy/k8s/waggle-dev.yaml.
It assumes:
- A namespace exists (
geisha-housein our deployment) - Three secrets:
waggle-dev-tg-token(bot_token),waggle-dev-anthropic(auth_token for the LLM gateway),waggle-dev-inject(shared_secret for the /inject endpoint) - A
local-pathstorage class (or changestorageClassNameto your default)
Apply:
kubectl -n <ns> create secret generic waggle-dev-tg-token \
--from-literal=bot_token="$WAGGLE_TG_BOT_TOKEN"
kubectl -n <ns> create secret generic waggle-dev-anthropic \
--from-literal=auth_token="$ANTHROPIC_AUTH_TOKEN"
kubectl -n <ns> create secret generic waggle-dev-inject \
--from-literal=shared_secret="$(openssl rand -hex 32)"
kubectl apply -f deploy/k8s/waggle-dev.yaml
The bundled Dockerfile produces a python:3.12-slim image with Node 22 + the
claude CLI installed. Tag and push it to your registry, then update the image
ref in the manifest.
Configuration
waggle reads a single YAML file (mounted at /etc/waggle/config.yaml in the
K8s manifest). Sample:
transports:
- name: tg
kind: telegram
bot_token: env:WAGGLE_TG_BOT_TOKEN # env-ref โ resolved at load
drop_pending_updates: true # ignore messages while bot was offline
access:
users: [123456789] # DM allow-list
groups:
-1001234567890: # group chat_id (negative)
users: [] # empty = anyone in group OK
require_mention: true # only @bot or reply-to-bot reach the agent
rate_limit:
user_per_minute: 30
chat_per_minute: 60
window_seconds: 60.0
owner:
user_id: 123456789 # must be in access.users
events:
sink:
kind: http # stdout | http
url: https://obs.example.com/ingest
secret: env:WAGGLE_OBS_SECRET # โฅ16 chars
timeout_seconds: 5.0
inject:
enabled: true
bind: "0.0.0.0:8080"
shared_secret: env:WAGGLE_INJECT_SECRET # โฅ16 chars
default_chat_id: 123456789 # fallback target
Hot reload โ editing this file (or the underlying ConfigMap) picks up
changes within ~1s for access.* blocks. transports, owner, inject
require a pod restart.
For the full grammar and validators, see waggle/config.py.
Owner operations
Send these directly to the bot from your owner account (the user_id
configured in owner.user_id):
| Command | Effect |
|---|---|
/status |
Liveness, turn count, last-inbound preview |
/pause |
Drop new inbound silently (won't reach claude) |
/resume |
Restore normal flow |
/abort |
Kill the in-flight turn, restart claude immediately |
/restart |
Restart claude after a 5-second cancel window |
/new |
Drop session context after a 5-second cancel window (fresh conversation) |
/compact |
Pipe /compact to claude (uses claude's own conversation-compact) |
While /restart or /new is pending, send the literal text cancel to
abort it. Non-owner senders fall through to claude as a normal prompt.
External triggers (/inject)
Push a prompt into the agent's active chat from any external system (alertmanager, CI, scheduler, dashboard). The endpoint is bearer-auth + fire-and-forget โ your caller doesn't block on the agent's reply.
curl -X POST http://waggle.svc.cluster.local:8080/inject \
-H "Authorization: Bearer $WAGGLE_INJECT_SECRET" \
-H "Content-Type: application/json" \
-d '{"prompt": "alertmanager: prod 5xx > 1%", "source": "alertmanager"}'
# โ {"submitted": true, "chat_id": "123456789"}
See docs/API.md for the full contract (response shapes,
error codes, AcceptEvent / RejectEvent JSON for the dashboard).
Troubleshooting
Bot doesn't reply. Check:
kubectl -n <ns> logs <pod> --tail=50 | grep -E "submit_inbound|tool reply|is_error"
The orchestrator logs every accepted inbound (โ chat=โฆ user=โฆ: โฆ) and every
turn result (โ chat=โฆ is_error=โฆ). If you see the inbound but no
reply, the issue is in the claude subprocess. If you don't see the inbound,
check the access gate (next).
Bot ignores a user. The pod logs a one-line reject event for every filtered inbound โ search for the user's id:
kubectl -n <ns> logs <pod> | grep "<user_id>" | grep "reject"
Likely culprits: user not in access.users, group not in access.groups,
or rate-limit budget exhausted (reason: rate-limited-*). Edit the
configmap and the change applies hot within 1s.
Bot not receiving messages in a group. Telegram's bot privacy mode blocks bots from seeing non-mention messages in groups by default. Promote the bot to admin in that group, OR disable privacy via @BotFather โ Bot Settings โ Group Privacy โ Turn off.
Pod restarts. waggle's claude subprocess is launched with --continue
so the conversation picks up where it left off. The accept event for the
last in-flight inbound was already persisted; the agent's reply may not have
been delivered. The orchestrator's _respawn_if_dead() notifies the owner
when the subprocess died between turns.
/inject returns 401. The bearer header doesn't match
inject.shared_secret. Verify the secret is โฅ16 chars in the configmap and
that the env-ref resolves correctly (env:WAGGLE_INJECT_SECRET requires
the env var to be set on the pod).
Layout
waggle/
โโโ waggle/ # Python package
โ โโโ orchestrator.py # turn loop, gate, dispatch
โ โโโ access.py # allow-list + hot-reload
โ โโโ rate_limit.py # sliding-window limiter
โ โโโ events.py # AcceptEvent / RejectEvent + sinks (stdout, http)
โ โโโ owner_ops.py # /status, /pause, /resume, /abort, /restart, /new, /compact
โ โโโ inject.py # POST /inject HTTP endpoint
โ โโโ status_indicator.py + typing_indicator.py
โ โโโ tools_server.py # MCP tools claude calls (reply, react, โฆ)
โ โโโ clients/telegram.py
โโโ deploy/k8s/ # K8s manifest example
โโโ docs/ # REQUIREMENTS, ROADMAP, DESIGN, API, TESTS
โโโ examples/ # Single-file runnable examples
โโโ scripts/ # E2E smoke tests against live Telegram
โโโ tests/ # 194 unit tests across 13 modules
For deeper architecture see docs/DESIGN.md. For the
test catalogue see docs/TESTS.md. For closing reports
of each shipped batch see docs/reports/.
License
MIT.
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 waggle_chat-0.10.12.tar.gz.
File metadata
- Download URL: waggle_chat-0.10.12.tar.gz
- Upload date:
- Size: 1.5 MB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
b8f2148eebf607bfe9dd531cf6d8c40bcc499c462cb00390ca9603a3d7439b37
|
|
| MD5 |
387375d585901d6e3fa43ab32460b1ef
|
|
| BLAKE2b-256 |
86e8ff1a756f1391389d8586b6ab2a91056d85bf6323c01a457b10682c21b92c
|
Provenance
The following attestation bundles were made for waggle_chat-0.10.12.tar.gz:
Publisher:
release.yml on Tiny-Hive/waggle
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
waggle_chat-0.10.12.tar.gz -
Subject digest:
b8f2148eebf607bfe9dd531cf6d8c40bcc499c462cb00390ca9603a3d7439b37 - Sigstore transparency entry: 1549804982
- Sigstore integration time:
-
Permalink:
Tiny-Hive/waggle@f864ccc9992807b13e4e8d4649b17fa7879b254f -
Branch / Tag:
refs/tags/v0.10.12 - Owner: https://github.com/Tiny-Hive
-
Access:
private
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@f864ccc9992807b13e4e8d4649b17fa7879b254f -
Trigger Event:
push
-
Statement type:
File details
Details for the file waggle_chat-0.10.12-py3-none-any.whl.
File metadata
- Download URL: waggle_chat-0.10.12-py3-none-any.whl
- Upload date:
- Size: 76.9 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
8c0664cee8e373d81ac72c7845c3ce5aa9b5494557aeef3bd586b8934dbbd0fb
|
|
| MD5 |
926ae6f1e0ea6f111f483be139baf3ed
|
|
| BLAKE2b-256 |
e47ea3d11f58942b65ac62ec97a57eecc147ec1cdd902556beac6bbe8a9552e3
|
Provenance
The following attestation bundles were made for waggle_chat-0.10.12-py3-none-any.whl:
Publisher:
release.yml on Tiny-Hive/waggle
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
waggle_chat-0.10.12-py3-none-any.whl -
Subject digest:
8c0664cee8e373d81ac72c7845c3ce5aa9b5494557aeef3bd586b8934dbbd0fb - Sigstore transparency entry: 1549805117
- Sigstore integration time:
-
Permalink:
Tiny-Hive/waggle@f864ccc9992807b13e4e8d4649b17fa7879b254f -
Branch / Tag:
refs/tags/v0.10.12 - Owner: https://github.com/Tiny-Hive
-
Access:
private
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@f864ccc9992807b13e4e8d4649b17fa7879b254f -
Trigger Event:
push
-
Statement type: