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.

Operating modes

waggle supports two runtime modes, configured via WAGGLE_MODE:

Mode Env Architecture Billing
CLI (default) WAGGLE_MODE=cli waggle is parent, spawns claude -p as subprocess (stream-json) Programmatic credit ($200/mo)
Channel WAGGLE_MODE=channel Claude is parent (interactive in tmux), waggle runs as MCP child Subscription quota (Max plan)

Both modes share the same Telegram client, access control, rate limiting, owner ops, inject endpoint, hook receiver, and status indicator. The difference is who spawns whom and which billing lane is used.

Channel mode details

Channel mode declares the claude/channel experimental MCP capability. Inbound Telegram messages are pushed as notifications/claude/channel with {content, meta} params. Claude processes them and calls waggle's MCP tools (reply, react, edit_message, etc.) to respond.

Requires: --dangerously-load-development-channels server:waggle flag on Claude CLI. The entrypoint runs Claude in a tmux session to provide a PTY.

Status indicator

While the agent processes a turn, waggle posts a live-updating status message in the source chat:

๐Ÿค” Thinkingโ€ฆ        โ†’ initial state
๐Ÿ”ง Tool: Bash       โ†’ tool in use
๐Ÿ“ Writing replyโ€ฆ   โ†’ post-tool, composing answer

The indicator carries inline-keyboard buttons ("๐Ÿง  Show thinking", "โœ… Show tasks") and is automatically deleted when the turn completes. In rapid-fire scenarios (multiple messages at once), only one indicator is shown per chat (deferred attach on first tool-start).

Compact indicator

When Claude auto-compacts the conversation context (or when the owner runs /compact), waggle surfaces it on Telegram:

๐Ÿ—œ Compacting contextโ€ฆ   โ†’ PreCompact hook fires
โœ… Context compacted      โ†’ PostCompact hook fires (edits the above)

Requires PreCompact + PostCompact HTTP hooks in Claude Code's settings.json (the waggle entrypoint registers these automatically).

Hook receiver

Internal HTTP server on 127.0.0.1:8091 that receives Claude Code hook events:

Hook Endpoint Purpose
PreToolUse /hooks/tool-start Update status indicator with tool name
PostToolUse /hooks/tool-end Clear current tool label
Stop /hooks/stop Reset task state, delete indicator
PreCompact /hooks/compact-start Post compact indicator
PostCompact /hooks/compact-end Edit to "compact complete"

The hook receiver is internal-only (localhost bind, no auth, no K8s Service). Separate from the external inject endpoint on :8080.

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
โ”‚   โ”œโ”€โ”€ cli.py               # Entry: mode dispatch (cli โ†’ orchestrator, channel โ†’ channel_server)
โ”‚   โ”œโ”€โ”€ orchestrator.py      # CLI mode: turn loop, gate, dispatch, stream-json subprocess
โ”‚   โ”œโ”€โ”€ channel_server.py    # Channel mode: MCP server with claude/channel capability
โ”‚   โ”œโ”€โ”€ channel_tag.py       # <channel> XML tag builder
โ”‚   โ”œโ”€โ”€ 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
โ”‚   โ”œโ”€โ”€ hook_receiver.py     # Internal hook receiver (tool status, compact indicator)
โ”‚   โ”œโ”€โ”€ status_indicator.py  # Live "๐Ÿค” Thinkingโ€ฆ / ๐Ÿ”ง Tool: X" Telegram message
โ”‚   โ”œโ”€โ”€ typing_indicator.py  # Telegram "typingโ€ฆ" bubble
โ”‚   โ”œโ”€โ”€ tools_server.py      # MCP tools claude calls (reply, react, edit, โ€ฆ)
โ”‚   โ””โ”€โ”€ clients/telegram.py  # aiogram polling + outbound methods
โ”œโ”€โ”€ deploy/k8s/              # K8s manifest example
โ”œโ”€โ”€ docs/                    # REQUIREMENTS, ROADMAP, DESIGN, API, TESTS, PRD
โ”œโ”€โ”€ examples/                # Single-file runnable examples
โ”œโ”€โ”€ scripts/                 # E2E test scripts (live Telegram via Pyrogram)
โ””โ”€โ”€ tests/                   # 230+ unit tests + 47-case E2E suite

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.10.28.tar.gz (1.5 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.10.28-py3-none-any.whl (80.2 kB view details)

Uploaded Python 3

File details

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

File metadata

  • Download URL: waggle_chat-0.10.28.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

Hashes for waggle_chat-0.10.28.tar.gz
Algorithm Hash digest
SHA256 23def36e53c4579b9c59ab8fb9216995a85a89b6caeb759e10903925853f2505
MD5 33ec01b16183ce0f796f3f2a0aab4fd1
BLAKE2b-256 cef306242efd0e2a6dfcbc9a7e864bfb11132ce3529e35927af401802527cffb

See more details on using hashes here.

Provenance

The following attestation bundles were made for waggle_chat-0.10.28.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.10.28-py3-none-any.whl.

File metadata

  • Download URL: waggle_chat-0.10.28-py3-none-any.whl
  • Upload date:
  • Size: 80.2 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.10.28-py3-none-any.whl
Algorithm Hash digest
SHA256 b3d7ebee0b1a20f49e6b2cd40b88ed041bfb191020d1c1edfbc7dd41c5a52602
MD5 f8209c9d22d0500abb8be433e5e66930
BLAKE2b-256 3ee9af3fdef18a68526b57634d311bb253b869548d54ecff35b8d0c5fba6d2d3

See more details on using hashes here.

Provenance

The following attestation bundles were made for waggle_chat-0.10.28-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