Metadata-Version: 2.4
Name: waggle-chat
Version: 0.10.8
Summary: Communication channel between AI agents and chat platforms (Telegram, Matrix, …)
Project-URL: Homepage, https://github.com/Tiny-Hive/waggle
Project-URL: Repository, https://github.com/Tiny-Hive/waggle.git
Project-URL: Issues, https://github.com/Tiny-Hive/waggle/issues
Author: Your Name (Claude)
License: MIT
License-File: LICENSE
Keywords: agent,channel,claude-code,matrix,mcp,telegram
Classifier: Development Status :: 3 - Alpha
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Requires-Python: >=3.11
Requires-Dist: aiohttp>=3.10
Requires-Dist: mcp>=1.0
Requires-Dist: pydantic>=2.7
Requires-Dist: pyyaml>=6.0
Provides-Extra: dev
Requires-Dist: mypy>=1.11; extra == 'dev'
Requires-Dist: pytest-asyncio>=0.24; extra == 'dev'
Requires-Dist: pytest>=8; extra == 'dev'
Requires-Dist: ruff>=0.6; extra == 'dev'
Provides-Extra: matrix
Requires-Dist: matrix-nio>=0.25; extra == 'matrix'
Provides-Extra: telegram
Requires-Dist: aiogram>=3.13; extra == 'telegram'
Provides-Extra: telegram-test
Requires-Dist: pyrogram>=2.0.106; extra == 'telegram-test'
Requires-Dist: telethon>=1.43; extra == 'telegram-test'
Requires-Dist: tgcrypto>=1.2.5; extra == 'telegram-test'
Description-Content-Type: text/markdown

<p align="center">
  <img src="docs/assets/banner.png" alt="waggle banner" width="100%" />
</p>

# 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`](./docs/REQUIREMENTS.md).

## Inspiration & why a separate project

The design is inspired by [PleasePrompto/ductor](https://github.com/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

```bash
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](https://docs.claude.com/en/docs/claude-code) is on `PATH` — install it separately:

```bash
npm install -g @anthropic-ai/claude-code
```

### Dev install from source

```bash
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)

```bash
# 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

```bash
# 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`](./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:

```bash
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:

```yaml
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`](./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.

```bash
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`](./docs/API.md) for the full contract (response shapes,
error codes, AcceptEvent / RejectEvent JSON for the dashboard).

## Troubleshooting

**Bot doesn't reply.** Check:

```bash
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:

```bash
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`](./docs/DESIGN.md). For the
test catalogue see [`docs/TESTS.md`](./docs/TESTS.md). For closing reports
of each shipped batch see [`docs/reports/`](./docs/reports/).

## License

[MIT](./LICENSE).
