Skip to main content

Communication channel between AI agents and chat platforms (Telegram, Matrix, โ€ฆ)

Project description

waggle banner

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:

  1. 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.
  2. 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.
  3. 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-house in 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-path storage class (or change storageClassName to 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


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

waggle_chat-0.8.6.tar.gz (1.4 MB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

waggle_chat-0.8.6-py3-none-any.whl (64.3 kB view details)

Uploaded Python 3

File details

Details for the file waggle_chat-0.8.6.tar.gz.

File metadata

  • Download URL: waggle_chat-0.8.6.tar.gz
  • Upload date:
  • Size: 1.4 MB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for waggle_chat-0.8.6.tar.gz
Algorithm Hash digest
SHA256 34377950f1d0de65b1f9c4a0c084f86de86cb8b9b3baaf977816d69fede45db3
MD5 887788022f071b63ce4c9fb4ef88d764
BLAKE2b-256 e848e05bd7d9bcb059077c184f5a0a9c4cda8bd6b0f3e9d2cee3522d9c7f5a17

See more details on using hashes here.

Provenance

The following attestation bundles were made for waggle_chat-0.8.6.tar.gz:

Publisher: release.yml on Tiny-Hive/waggle

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file waggle_chat-0.8.6-py3-none-any.whl.

File metadata

  • Download URL: waggle_chat-0.8.6-py3-none-any.whl
  • Upload date:
  • Size: 64.3 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for waggle_chat-0.8.6-py3-none-any.whl
Algorithm Hash digest
SHA256 8df70e5f917fb3f3c1f92cc6e792fca1c49ed0e1a820698f0418bc365d98bee5
MD5 3fcb77ac98948d8d03687a451d9791a3
BLAKE2b-256 147e9fe081b35c4ff090bc552619253244988b4ebc8b59d1acd6036a409548f5

See more details on using hashes here.

Provenance

The following attestation bundles were made for waggle_chat-0.8.6-py3-none-any.whl:

Publisher: release.yml on Tiny-Hive/waggle

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page