feat: DM-First v2 architecture - Tanko live on CT 112
CI / validate (pull_request) Failing after 1s

Architecture change: DM-First (v2) replaces @mention-in-stream (v1) as primary
agent interaction channel. See ADR-001-dm-topology and ADR-002-dm-routing.

Key changes:
- New DM-first Zulip adapter (adapter_dm.py) deployed on CT 112
  - Registers empty narrow to receive ALL events
  - Demuxes DMs (type: 'private') vs stream events
  - Replies to DMs back in private thread, stream replies to #agent-hub
  - Uses blocking long-poll (dont_block=False) to eliminate rate limiting
- Rewritten ARCHITECTURE.md, CONTEXT.md, PRD.md for v2 DM-First
- New ADRs: ADR-001 (DM Topology), ADR-002 (DM Routing)
- Archived old ADRs: 001, 002, 005 (topic-topology, mention-routing, mention-detection)
- Updated ADRs: 003 (agent-naming), 004 (per-agent-bots), 006 (all-bots-model)
- Updated PI extension (index.ts) with DM-aware patterns
- Tanko agent confirmed responding in both DM and @all-bots in #agent-hub
This commit is contained in:
kagentz-bot
2026-06-21 07:50:30 -04:00
parent b8ae43f652
commit 705c29a4e0
14 changed files with 1552 additions and 260 deletions
+122 -73
View File
@@ -1,110 +1,149 @@
# Zulip Multi-Platform Agent Communication — Architecture
**Version:** 2.0 (DM-First)
**Date:** 2026-06-21
## Overview
A production-ready system enabling 6 AI agents across 3 platforms (Hermes Python, Agent Zero, pi TypeScript) to communicate through Zulip via dedicated per-agent bot users.
A production-ready system enabling 6 AI agents across 3 platforms (Hermes Python, Agent Zero, PI TypeScript) to communicate through Zulip via **Direct Messages (DMs)**. Each agent's bot user receives DMs directly from human users, and all bots subscribe to `#agent-hub` only for `@all-bots` broadcasts.
## Core Design Decision: DM-First
| Old (v1) — @mention in Stream | New (v2) — DM-First |
|-------------------------------|---------------------|
| User @mentions bot in #agent-hub | User sends DM directly to bot |
| Bot detects via `mentioned_users` (unreliable in Zulip 12.0) | Bot detects `message.type === 'private'` (trivial) |
| Bot must capture origin topic for reply routing | No topic routing needed — Zulip DM thread is 1:1 |
| All 6 bots receive every #agent-hub message (noise) | Only the recipient bot receives the DM |
| Stream subscription required for all agents | DM requires no stream subscription |
## Architecture Diagram
```
┌─────────────────────────────────────────────────────────────┐
│ Zulip Server (CT 117) │
│ chat.sysloggh.net:443 │
│ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌─── │
│ │tanko-bot │ │mumuni-bot│ │koonimo │ │koby-bot │ │...
│ │ │ │ │ │-bot │ │
└────┬─────┘ └────┬─────┘ └────┬─────┘ └────┬─────┘ └───
└───────┼────────────────────────┼──────────────────────────
┌────▼────┐ ┌────▼────┐ ┌────▼────┐ ┌────▼────┐
Hermes │ │ Hermes │ │ Hermes │ │ Hermes
│ Plugin │ │ Plugin │ │ Plugin │ │ Plugin
(Python)│ │ (Python)│ │ (Python)│ │ (Python)
│ │ │ │ │ │ │
│ Tanko │ Mumuni │ Koonimo │ │ Koby
│ CT 112 │ │ CT 114 │ │ CT 113 │ CT 111
└─────────┘ └─────────┘ └─────────┘ └─────────┘
│ Zulip Server (CT 117)
│ chat.sysloggh.net:443
│ ┌─────────────────────────────────────────────────────┐
│ │ Each Agent Connects via TWO Channels: │
│ │ │ │
│ DM (PRIMARY) #agent-hub (BROADCAST) │
│ │ ┌────────────────────┐ ┌─────────────────────┐ │ │
│ │ Chat with owner @all-bots detection │ │
│ │ │ No @mention parse │ │ Content-based regex │ │ │
│ No topic capture (no dedicated user) │ │
│ │ │ Trivial detection │ 6 agents respond │ │
└────────────────────┘ └─────────────────────┘ │
│ └──────────────────────────────────────────────────────┘
│ Bot Users (6): tanko-bot, mumuni-bot, koonimo-bot,
│ koby-bot, kagentz-bot, abiba-bot │
│ (No dedicated @all-bots bot — see ADR-006) │
└──────┬──────────┬──────────┬──────────┬──────────┬───────────┘
│ │ │ │ │
┌────▼────┐ ┌───▼───┐ ┌───▼───┐ ┌───▼───┐ ┌───▼───────┐
│ Hermes │ │Hermes │ │Hermes │ │ PI │ │ Agent Zero│
│ Plugin │ │Plugin │ │Plugin │ │Extens.│ │ Plugin │
│ (Python)│ │Python │ │Python │ │(TS) │ │ (Python) │
│ │ │ │ │ │ │ │ │ │
│ Tanko │ │Mumuni │ │Koonimo│ │ Abiba │ │ kagentz │
│ CT 112 │ │CT 114 │ │CT 113 │ │CT 100 │ │ CT 105 │
└─────────┘ └───────┘ └───────┘ └───────┘ └───────────┘
┌──────────────┐ ┌──────────────┐
│ Agent Zero │ │ pi │
│ Plugin │ │ Extension │
│ (Python) │ │ (TypeScript) │
│ │ │ │
│ kagentz │ │ abiba │
│ CT 105 │ │ CT 100 │
└──────────────┘ └──────────────┘
Note: Koby (Hermes) also on a Hermes CT, not shown for brevity
```
## Message Flow (Normal)
## Message Flow: DM (Primary)
```
User creates topic "project-x" in #agent-hub
└── User types: @tanko-bot analyze this data
└── Zulip fires event to tanko-bot's event queue
└── Hermes Zulip Plugin on CT 112
├── Detects: tanko-bot in mentioned_users
├── Strips @mention from message
├── Constructs MessageEvent
└── Routes to Tanko agent via BasePlatformAdapter
└── Tanko agent processes, returns response
└── Plugin posts to #agent-hub > project-x
User opens DM to @tanko-bot in Zulip
User types: "deploy the API update"
└── Zulip pushes event to tanko-bot's event queue ONLY
└── Hermes Zulip Plugin on CT 112
├── Checks: message.type === 'private' ✅
├── No @mention parsing needed
├── No topic/stream routing needed
├── Constructs MessageEvent with sender_id + content
└── Routes to Tanko agent for processing
└── Tanko processes, returns response
└── Plugin sends DM reply via:
POST /api/v1/messages {
"type": "private",
"to": [sender_id],
"content": "Starting deployment..."
}
```
## Message Flow (@all-bots)
## Message Flow: @all-bots Broadcast (Secondary)
```
User types: @all-bots status report
└── Zulip fires event to ALL 6 bots (same narrow)
└── Each plugin independently:
├── Detects: All Bots in mentioned_users
├── Forwards to its agent
└── Posts response to same topic
└── 6 independent responses appear in the topic
User posts in #agent-hub > any-topic:
"@**all-bots** status report please"
└── Zulip pushes event to ALL 6 bot event queues
└── Each plugin on each CT independently:
├── Checks: message.type === 'stream' ✅
├── Checks: regex /@\*\*(?:all-bots|All Bots)\*\*/i.test(content) ✅
├── Ignores: all non-@all-bots stream messages
└── Forwards @all-bots content to its agent
└── Agent processes, returns response
└── Plugin posts to same #agent-hub > topic
└── 6 independent responses appear in the topic
```
## Plugin Communication Logic
- **Isolation**: Each agent (Tanko, Mumuni, etc.) runs as an independent process on its own Compute Target (CT).
- **State**: Agents are stateless; all "memory" is handled via the RA-H OS Knowledge Graph and the Hermes persistent memory tool.
- **Communication**: Agents communicate via Zulip. There is no direct A2A (Agent-to-Agent) messaging layer. Instead, each agent listens to the `#agent-hub` and responds to specific mentions or `@all-bots` pings.
- **Swarm Development**: The "Swarm" refers to our collaborative development methodology where multiple agents/developers work on different components of the plugin simultaneously.
- **Communication**: Primary channel is Zulip DMs. Secondary broadcast channel is `#agent-hub` for `@all-bots` notifications only.
- **No @mention routing**: DM-based routing eliminates the `mentioned_users` field dependency (unreliable in Zulip 12.0).
- **Single stream subscription**: Each bot subscribes to `#agent-hub` exclusively for `@all-bots` detection. All other messages in `#agent-hub` are ignored.
## Platform Plugin Contracts
### Hermes (Python) — BasePlatformAdapter
- Path: `hermes-zulip-plugin/src/hermes_zulip/`
- Implements: `BasePlatformAdapter` (Hermes Gateway)
- Config: `config.yaml` per-agent
- Entry point: `plugin.yaml` (Hermes manifest)
| Field | Value |
|-------|-------|
| Path | `hermes-zulip-plugin/src/hermes_zulip/` |
| Interface | `BasePlatformAdapter` (Hermes Gateway) |
| Config | `config.yaml` per-agent |
| Entry point | `plugin.yaml` (Hermes manifest) |
| DM detection | `message.type === 'private'` |
| @all-bots detection | Regex `/@\*\*(?:all-bots|All Bots)\*\*/i` on stream events |
### Agent Zero — A0 Plugin System
- Path: `agent-zero-plugin/src/`
- Implements: Agent Zero plugin API
- Config: `config.yaml` per-agent
| Field | Value |
|-------|-------|
| Path | `agent-zero-plugin/src/` |
| Interface | Agent Zero plugin API |
| Config | `config.yaml` per-agent |
| DM detection | `message.type === 'private'` |
| @all-bots detection | Regex `/@\*\*(?:all-bots|All Bots)\*\*/i` on stream events |
### pi (TypeScript) — pi Extension API
- Path: `pi-zulip-extension/src/`
- Implements: pi extension (TypeScript module in `~/.pi/agent/extensions/`)
- Config: `config.yaml` per-agent
- Reference: pi extension docs at `/usr/lib/node_modules/@earendil-works/pi-coding-agent/docs/extensions.md`
## Reliability
- **Reconnection**: Zulip SDK handles backoff/reconnect natively
- **Retry**: 3 attempts with 5s backoff on agent timeout
- **Error Response**: User-visible message on failure
- **Monitoring**: Health endpoint (`/health`) on each plugin (port 9200)
- **Logging**: Per-plugin log files
### PI (TypeScript) — PI Extension API
| Field | Value |
|-------|-------|
| Path | `pi-zulip-extension/src/` |
| Interface | PI extension (TypeScript module in `~/.pi/agent/extensions/`) |
| Config | `config.yaml` per-agent |
| DM detection | `event.type === 'private'` or `message.type === 'private'` |
| @all-bots detection | Regex `/@\*\*(?:all-bots|All Bots)\*\*/i` on stream events |
## Config Template
```yaml
# config.yaml — Per-agent Zulip plugin configuration
# config.yaml — Per-agent Zulip plugin configuration (DM-First)
agent:
name: "tanko"
display_name: "Tanko"
zulip_bot_name: "tanko-bot"
owner_email: "jerome@sysloggh.net"
private_topic: "tanko-private"
zulip:
server_url: "https://chat.sysloggh.net"
email: "tanko-bot@chat.sysloggh.net"
api_key: "${ZULIP_API_KEY}" # From env var
all_bots_user_id: 1234
api_key: "${ZULIP_API_KEY}" # From env var or config
stream: "agent-hub" # Only used for @all-bots broadcasts
stream_only_for_broadcast: true
error_handling:
timeout_seconds: 30
@@ -116,9 +155,19 @@ monitoring:
health_port: 9200
```
## Reliability
- **Reconnection**: Zulip SDK handles backoff/reconnect natively
- **Retry**: 3 attempts with 5s backoff on agent timeout
- **Error Response**: User-visible message in DM on failure
- **Monitoring**: Health endpoint (`/health`) on each plugin (port 9200)
- **Logging**: Per-plugin log files (or journald for Hermes systemd services)
## Prerequisites
1. Zulip server running on CT 117
2. Zulip bot users created for all 6 agents + All Bots
3. `#agent-hub` stream created with all bots subscribed
1. Zulip server running on CT 117 (`chat.sysloggh.net`)
2. Zulip bot users created for all 6 agents (no dedicated "All Bots" bot)
3. `#agent-hub` stream created with all 6 bots subscribed
4. SSH key access to all 6 agent CTs
5. Gitea repo initialized at `git@git.sysloggh.net:SyslogSolution/zulip-platform-plugins.git`
6. Each plugin configured with per-agent `config.yaml`
+76 -26
View File
@@ -1,59 +1,109 @@
# Zulip Platform Plugins — Domain Vocabulary
This document captures the shared language and architectural invariants for the multi-platform Zulip agent communication system.
**Version:** 2.0 (DM-First)
**Date:** 2026-06-21
This document captures the shared language and architectural invariants for the multi-platform Zulip agent communication system under the DM-first architecture.
## Domain Terms
| Term | Definition |
|------|------------|
| **Agent** | An AI agent operating on a platform (Hermes/Agent Zero/pi) that can receive and respond to Zulip messages |
| **Agent** | An AI agent operating on a platform (Hermes/Agent Zero/PI) that can receive and respond to Zulip messages |
| **Adapter / Plugin** | A platform-native component that bridges Zulip events to the agent framework |
| **@mention** | A Zulip mention (`@**Tanko Bot**`) that triggers the named agent to respond |
| **@all-bots** | A dedicated Zulip bot user that, when @mentioned, triggers ALL agents across all platforms |
| **Topic** | A Zulip conversation thread within a stream |
| **Private topic** | An agent-named topic (e.g., `tanko-private`) where only the bot and its owner participate |
| **DM (Direct Message)** | A Zulip private message from a user directly to a bot. The primary communication channel for agent-owner interaction |
| **@all-bots** | A content-based mention detection pattern in `#agent-hub` stream. Detected via regex matching `@**all-bots**` or `@**All Bots**` in message content |
| **#agent-hub** | The shared stream used exclusively for `@all-bots` broadcasts. Agents ignore all non-@all-bots messages here |
| **Stream** | A Zulip channel for topic-based discussions. Only used for `@all-bots` broadcasts in this architecture |
| **Topic** | A Zulip conversation thread within a stream. Not used for agent routing in DM-first architecture |
| **Owner** | The human user associated with an agent bot, stored in per-agent config |
## Naming Conventions
- Agent Zulip bot display names: lowercase, dash-separated, `-bot` suffix — e.g., `tanko-bot`, `mumuni-bot`
- Agent Zulip bot emails: `@chat.sysloggh.net` — e.g., `tanko-bot@chat.sysloggh.net`
- Private topics: `{name}-private` — e.g., `tanko-private`
- DM address: `@**tanko-bot**` (same format as @mention, but sent as private message)
- Owner privacy: agent should never expose owner name to other agents or users
- No private topics, no dedicated `@all-bots` bot user
## Agent Registry
| Agent | Platform | Zulip Bot | CT | Private Topic |
|-------|----------|-----------|-----|---------------|
| tanko | Hermes (Python) | tanko-bot | amdpve CT 112 | tanko-private |
| mumuni | Hermes (Python) | mumuni-bot | minipve CT 114 | mumuni-private |
| koonimo | Hermes (Python) | koonimo-bot | amdpve CT 113 | koonimo-private |
| koby | Hermes (Python) | koby-bot | amdpve CT 111 | koby-private |
| kagentz | Agent Zero | kagentz-bot | amdpve CT 105 | kagentz-private |
| abiba | pi (TypeScript) | abiba-bot | amdpve CT 100 | abiba-private |
| Agent | Platform | Zulip Bot | CT | DM Access | @all-bots Detection |
|-------|----------|-----------|-----|-----------|---------------------|
| tanko | Hermes (Python) | tanko-bot | amdpve CT 112 | ✅ Native Hermes | Regex on #agent-hub |
| mumuni | Hermes (Python) | mumuni-bot | minipve CT 114 | ❌ Not deployed | Regex on #agent-hub |
| koonimo | Hermes (Python) | koonimo-bot | amdpve CT 113 | ❌ Not deployed | Regex on #agent-hub |
| koby | Hermes (Python) | koby-bot | amdpve CT 111 | ❌ Not deployed | Regex on #agent-hub |
| kagentz | Agent Zero | kagentz-bot | amdpve CT 105 | ❌ Not deployed | Regex on #agent-hub |
| abiba | PI (TypeScript) | abiba-bot | amdpve CT 100 | ⚠️ Extension deployed but broken | Regex on #agent-hub |
## DM-First Communication Flow
```
User sends DM to @tanko-bot
→ Zulip fires event ONLY to tanko-bot's event queue
→ Event has: {type: 'message', message: {type: 'private', ...}}
→ Bot detects: message.type === 'private'
→ No @mention parsing, no topic routing needed
→ Bot processes message
→ Bot replies via POST /api/v1/messages {type: 'private', to: [sender_id]}
```
**Key invariants:**
- Bot receives events ONLY when the bot is the DM recipient
- `message.type === 'private'` is trivially detectable with no Zulip SDK version dependency
- Reply goes to the same DM thread — Zulip handles threading
- No `mentioned_users` field is checked or used
- No stream subscription needed for DM communication
## @all-bots Resolution
@all-bots refers to all 6 agents across all frameworks (Hermes, Agent Zero, pi). When @all-bots is mentioned in any topic:
1. Each per-agent bot receives the event (since they all share `narrow: stream agent-hub`)
2. Each bot checks: "Is `All Bots` in `mentioned_users`?"
3. If yes, bot forwards the message to its agent
4. @all-bots is a **dedicated Zulip bot user** with display name `All Bots` and email `all-bots@chat.sysloggh.net`
`@all-bots` uses **content-based detection** on `#agent-hub` stream messages — no dedicated bot user:
```regex
/@\*\*(?:all-bots|All Bots)\*\*/i
```
1. Each bot subscribes to `#agent-hub` stream exclusively for `@all-bots` detection
2. Each bot receives every message in `#agent-hub` (Zulip push model)
3. Each bot checks: "Does content match the @all-bots regex?"
4. If match: bot forwards the full message to its agent for processing
5. If no match: bot ignores the message entirely
6. There is NO dedicated "All Bots" Zulip bot user
## Response Rules
- **Own topic**: Agent always responds (to its private topic)
- **Collaboration topic** (any topic that isn't a private topic): Agent responds only when @mentioned
- **@all-bots**: All agents respond
- **Private topics**: Only the agent owner + bot are participants; bot enforces this via config owner_email matching
## Private Topic ACL (v1 — documented limitation)
- **DM (primary)**: Agent always responds to the sender's DM — no routing logic needed
- **#agent-hub (broadcast only)**: Agent responds only when `@all-bots` is detected in message content
- **Non-@all-bots messages**: All agents ignore any message in `#agent-hub` that does not contain `@all-bots`
- **No private topics, no @mention of individual agents in streams** — DMs replace both patterns
In v1, private topic access is enforced by matching `sender_email == config.owner_email` on each message. **This is not cryptographically authenticated** — Zulip does not enforce sender identity. A user who knows the owner email can impersonate them. This is an acceptable limitation for v1 internal use. v2 should use Zulip invite-only stream permissions or shared-secret challenge-response.
## Response Rules for Agent-Owner DM
- Owner sends DM to bot → agent processes and replies
- Bot processes ALL DMs from ANY Zulip user (API key is the only access gate)
- Agent owner email in config is for identification, not ACL enforcement
- Future: rate limiting and spam protection via Zulip's built-in rate limiting
## Monorepo Structure
All 3 platform plugins live in a single Gitea repository at `git@git.sysloggh.net:SyslogSolution/zulip-platform-plugins.git`
```
zulip-platform-plugins/
├── hermes-zulip-plugin/ # Hermes Python adapter (4 agents)
├── pi-zulip-extension/ # PI TypeScript extension (abiba)
├── agent-zero-plugin/ # Agent Zero Python plugin (kagentz)
├── scripts/ # deploy.sh, rollback.sh
├── docs/ # ADRs, ARCHITECTURE.md, PRD.md, CONTEXT.md
├── config.yaml.example # Template for per-agent config
└── .gitea/ # PR template, workflows (if CI enabled)
```
## Guardian Directives
- All destructive actions require user confirmation before execution
- Plugin updates are version-controlled through Gitea
- Config files are per-agent and pushed alongside code
- DM-first architecture means no @mention-related Zulip SDK features are relied upon
- Pi extension on CT 100 has a broken Zulip client connection that needs repair
+121 -79
View File
@@ -1,54 +1,55 @@
# Product Requirements Document: Zulip Platform Plugins
**Status:** Draft v1.0
**Date:** 2026-06-18
**Authors:** Agent Zero (via grill-with-design on Tabiri infrastructure)
**Status:** Draft v2.0
**Date:** 2026-06-21
**Authors:** Agent Zero (via Tabiri infrastructure)
---
## Problem Statement
The Sysloggh agent mesh consists of **6 autonomous agents** across **3 distinct frameworks** (Hermes Python, Agent Zero, PI/openclaw TypeScript) deployed on **5 Proxmox CTs**. These agents operate in isolation — there is no unified inter-agent communication channel. Each agent processes tasks independently, and cross-agent coordination requires manual intervention.
The Sysloggh agent mesh consists of **6 autonomous agents** across **3 distinct frameworks** (Hermes Python, Agent Zero, PI TypeScript) deployed on **5 Proxmox CTs**. These agents operate in isolation — there is no unified inter-agent communication channel. Each agent processes tasks independently, and cross-agent coordination requires manual intervention.
The existing Zulip infrastructure (Zulip 12.0-1 on CT 117, Docker-based) provides a mature async messaging platform, but no agent is wired into it. A partial implementation (`zulip-plugin/adapter.py`) exists with event streaming and dedup logic, but was never deployed. The gateway adapter is compiled but not running on any agent CT, and the required Python packages are not installed.
The existing Zulip infrastructure (Zulip 12.0-1 on CT 117, Docker-based) provides a mature async messaging platform with built-in DM and stream support. All 6 agent bot users exist on the server and `#agent-hub` stream is created. Only Tanko (v1 Hermes native Zulip) is operational.
**The gap:** Agents cannot collaboratively route tasks, share context, or coordinate via Zulip threads.
**The gap:** Agents cannot reliably receive user requests or communicate via Zulip DMs.
---
## Solution
A multi-platform **Zulip agent communication layer** consisting of one plugin per agent framework, each running co-located on its agent's CT, connecting its agent to the shared Zulip messaging infrastructure at `chat.sysloggh.net`.
A multi-platform **Zulip agent communication layer** where each agent communicates primarily via **Zulip Direct Messages (DMs)**. Agents also subscribe to `#agent-hub` stream for `@all-bots` broadcasts only.
### Architecture at a Glance
```
┌──────────────────────────────────────────────────────────────────
Zulip Server (CT 117)
chat.sysloggh.net:443
Stream: #agent-hub
Bot Users: tanko-bot, mumuni-bot, koonimo-bot, koby-bot,
kagentz-bot, abiba-bot, all-bots
└────┬──────┬──────┬──────┬──────┬──────┬──────┬──────────────────┘
│ │ │
┌────┴┐ ┌─────┐ ┌┴────┐ ┌┴────┐ ┌┴────┐ ┌┴────┐ ┌──────────┐
Tnako│ │Mumuni│ │Kooni│ │Koby │ │Kage │ │Abiba│ │@all-bots
Herm │ │Herm │ │Herm │ │Herm │ │A0 PI │ (special)│
CT112│ │CT... │ │CT.. │ │CT.. │ │CT.. │ │CT.. │ │
└─────┘ └──────┘ └─────┘ └─────┘ └─────┘ └─────┘ └──────────┘
│ │ │ │ │
┌────┴┐ ┌───┴──┐ ┌┴────┐ ┌┴────┐ ┌┴────┐ ┌┴────┐
Config│ │Config│ │Config│ │Config│ │Config│ │Config
tanko │ │mumuni│ │kooni │ │koby │ │kage │ │abiba
.yaml │ │.yaml │ │.yaml │ │.yaml │ │.yaml │ │.yaml
└─────┘ └──────┘ └─────┘ └─────┘ └─────┘ └─────┘
┌─────────────────────────────────────────────────────────────┐
│ Zulip Server (CT 117) │
│ chat.sysloggh.net:443 │
┌─────────────────────────────────────────────────────┐
Each Agent Has Two Channels:
│ │ │ │
│ │ DM (PRIMARY) #agent-hub (BROADCAST) │ │
│ │ ┌─────────────────┐ ┌──────────────────┐ │ │
│ │ Direct 1:1 chat │ │ @all-bots only │ │
│ │ No @mention │ │ Cross-agent msgs │ │ │
│ │ No topic parsing │ │ Broadcast only
│ │ └──────────────────┘ └──────────────────┘ │ │
│ └─────────────────────────────────────────────────────┘
│ │
Bot Users: tanko-bot, mumuni-bot, koonimo-bot, koby-bot,
kagentz-bot, abiba-bot
(No dedicated @all-bots bot user — content-based match)
└────────────────────────────────────────────────────────────┘
```
### Core Principles
- **DM-first:** Agent-owner communication uses Zulip DMs exclusively — no @mention parsing, no topic routing, no `mentioned_users` dependency
- **#agent-hub for broadcast only:** The `#agent-hub` stream is subscribed solely for detecting `@all-bots` notifications
- **Co-located:** Each plugin runs on the same CT as its agent, uses platform-native interfaces
- **@mention-routed:** Messages reach the correct agent via Zulip @mention detection
- **Owner-gated:** Private topics are restricted per-agent owner via config
- **Owner-gated:** Each bot DM is gated by API key — only the bot's credentials grant access
- **Version-controlled:** All plugin code lives in a Gitea monorepo, pushed to CTs via deploy script
- **Graceful degradation:** Users always receive feedback, even when an agent is unavailable
@@ -58,52 +59,72 @@ A multi-platform **Zulip agent communication layer** consisting of one plugin pe
### Agent Communication
1. As a **human user**, I want to **@mention an agent in any Zulip topic**, so that the agent receives my message and responds in-thread.
1. As a **human user**, I want to **DM an agent's bot directly**, so that the agent receives my message and responds in-thread.
2. As a **human user**, I want to **@mention @all-bots**, so that all 6 agents across all frameworks receive the message.
2. As a **human user**, I want to **mention @all-bots in #agent-hub**, so that all 6 agents across all frameworks receive the broadcast.
3. As an **agent**, I want to **receive messages addressed to me**, so that I can process and respond to user requests.
3. As an **agent**, I want to **receive DMs addressed to me**, so that I can process and respond without parsing @mentions or stream context.
### Private Channels
4. As a **human user**, I want a **private topic per agent** (e.g., `#agent-hub > tanko-private`), so that I can have confidential conversations with a single agent.
5. As a **system administrator**, I want **per-agent owner configuration**, so that only designated users can access each agent's private topic.
4. As an **agent**, I want to **detect @all-bots broadcasts in #agent-hub**, so that I can participate in multi-agent coordination.
### Deployment & Operations
6. As a **system administrator**, I want to **version-control all plugin code in a single Gitea repository**, so that updates are tracked, auditable, and pushable across all CTs.
5. As a **system administrator**, I want to **version-control all plugin code in a single Gitea repository**, so that updates are tracked, auditable, and pushable across all CTs.
7. As a **system administrator**, I want a **deploy script** that pulls the latest plugin code and restarts services on all CTs, so that updates are consistent and repeatable.
6. As a **system administrator**, I want a **deploy script** that pulls the latest plugin code and restarts services on all CTs, so that updates are consistent and repeatable.
### Reliability
8. As a **human user**, I want to see **a friendly error message** if an agent is unavailable, rather than a silent timeout.
7. As a **human user**, I want to see **a friendly error message** if an agent is unavailable, rather than a silent timeout.
9. As a **system administrator**, I want **health check endpoints** on each plugin, so that I can monitor plugin status from the harness or monitoring infra.
8. As a **system administrator**, I want **health check endpoints** on each plugin, so that I can monitor plugin status from the harness or monitoring infra.
---
## Implementation Decisions
## Architecture Decisions
### Platform Plugin Contracts
### DM-First Communication
Each of the 3 frameworks uses its **native plugin system** — not a unified bridge or A2A overlay for intra-agent communication. Cross-framework @all-bots messages flow through Zulip: all 7 bot users are subscribed to the same `#agent-hub` stream, so every bot receives every message. Each bot checks if it's @mentioned, then either processes or ignores.
Each agent communicates via Zulip **Direct Messages**. When a user sends a DM to an agent's bot:
### Framework-Specific Implementation
```
User DMs @tanko-bot
→ Zulip pushes event ONLY to tanko-bot's event queue
→ Event has: {type: 'message', message: {type: 'private', ...}}
→ Bot detects message.type === 'private'
→ No @mention parsing, no topic/stream capture needed
→ Bot processes message.content
→ Bot replies to same DM via:
{type: 'private', to: [user_id], content: 'response'}
```
| Framework | Plugin Type | Interface | Reference Architecture |
|-----------|-------------|-----------|----------------------|
| **Hermes Python** (4 agents) | Hermes Gateway `BasePlatformAdapter` | `on_event(message) → Response` | Existing `zulip-plugin/` in homelab |
| **PI/openclaw TypeScript** (1 agent) | openclaw Extension | Extension API (similar to existing Discord/GChat/Mattermost/Matrix) | `/a0/usr/projects/homelab/openclaw/extensions/` |
| **Agent Zero** (1 agent) | A0 Plugin | Agent Zero plugin system | Custom A0 plugin pattern |
**Advantages over stream-based @mention:**
- Only the recipient bot receives the DM — no broadcast noise to other 5 agents
- No `mentioned_users` field dependency (unreliable in Zulip 12.0)
- No topic routing — DMs are inherently 1:1 threading
- `message.type: 'private'` is trivially detectable
### @mention Detection
### @all-bots Broadcast
- Use Zulip SDK's `mentioned_users` field on message events (ADR-005)
- Each bot checks if its own `user_id` is in the `mentioned_users` array
- For @all-bots: bot checks if dedicated `All Bots` user is in `mentioned_users` (ADR-006)
- @all-bots spans all 7 bots across all frameworks (ADR-012)
`@all-bots` in `#agent-hub` uses **content-based detection** (regex) — no dedicated bot user needed:
```
User posts in #agent-hub > topic:
"@**all-bots** status report please"
→ Each bot receives event via #agent-hub subscription
→ Each bot checks: /@\*\*(?:all-bots|All Bots)\*\*/i.test(content)
→ If match: bot forwards to its agent
→ 6 independent responses appear in the topic
```
### Plugin Architecture
| Framework | Plugin Type | Interface | DM Detection | @all-bots Detection |
|-----------|-------------|-----------|--------------|--------------------|
| **Hermes Python** (4 agents) | Gateway PlatformAdapter | `on_event(message)` | `message.type === 'private'` | Regex on stream events |
| **PI TypeScript** (1 agent) | PI Extension | Extension API | `event.type === 'private'` | Regex on stream events |
| **Agent Zero** (1 agent) | A0 Plugin | A0 plugin system | `message.type === 'private'` | Regex on stream events |
### Config Schema (per-agent `config.yaml`)
@@ -115,11 +136,12 @@ agent:
bot_email: "tanko-bot@chat.sysloggh.net"
bot_api_key: "<api_key>"
owner_email: "jerome@sysloggh.net"
private_topic: "tanko-private"
zulip:
server_url: "https://chat.sysloggh.net"
stream: "agent-hub"
stream: "agent-hub" # Only used for @all-bots broadcasts
client:
type: "sdk" # "sdk" for zulip-js/zulip-python, "rest" for raw API
error_handling:
timeout_seconds: 30
@@ -131,16 +153,38 @@ monitoring:
health_port: 9200
```
### Private Topic ACL
---
- Bot subscribes to its private topic (`tanko-private`) at startup
- Owner is matched via `config.yaml > owner_email` (ADR-011)
- Messages in private topic: only respond if sender email matches owner
- Messages in public topics: respond to @mentions from any sender
## Implementation Decisions
### Platform Plugin Contracts
Each of the 3 frameworks uses its **native plugin system** — not a unified bridge or A2A overlay for intra-agent communication. Cross-framework `@all-bots` messages flow through Zulip: all 6 bots are subscribed to `#agent-hub` stream. Each bot checks for `@all-bots` via content-based regex match, then processes.
### DM Event Processing
```python
# Simplified DM processing logic (all platforms)
def on_event(event):
message = event["message"]
# DM case (primary)
if message["type"] == "private":
user_id = message["sender_id"]
content = message["content"]
agent.process(content, reply_to=lambda resp: send_dm(user_id, resp))
return
# Stream case — only process if @all-bots
if message["type"] == "stream":
if re.search(r'@\*\*(?:all-bots|All Bots)\*\*', message["content"]):
agent.process_broadcast(message)
# Ignore all other stream messages
```
### Error Handling Strategy
- Agent timeout: plugin sends graceful degradation message to Zulip thread (ADR-009)
- Agent timeout: plugin sends graceful degradation message to DM thread (ADR-009)
- Zulip server down: SDK auto-reconnects with exponential backoff
- Invalid message format: plugin logs error, does not crash
- Health endpoint (`/health` on port 9200): returns status, uptime, last_message_time
@@ -148,28 +192,28 @@ monitoring:
### Naming Convention
- Bot users: `{name}-bot` (lowercase, dash separator) — `tanko-bot`, `mumuni-bot`, etc. (ADR-003)
- Private topics: `{name}-private``tanko-private`, `mumuni-private`, etc. (ADR-001)
- Display names: capitalize properly — "Tanko Bot", "Mumuni Bot", "All Bots"
- DM address: `@**tanko-bot**` (same format as @mention)
- Display names: capitalize properly — "Tanko Bot", "Mumuni Bot"
---
## Testing Decisions
### Unit Tests
- @mention detection logic: correct extraction of bot from `mentioned_users`
- Private topic ACL: owner matches, non-owner denied
- DM detection: `message.type === 'private'` correctly triggers processing
- @all-bots detection: regex matches `@**all-bots**` and `@**All Bots**` in stream content
- Config parsing: all fields present, graceful defaults
- Error response: timeout triggers correct grace message
### Integration Tests
- Each plugin connects to Zulip and receives/sends test messages
- @all-bots message reaches all 7 bots
- Private topic message only reaches owner agent
- Each plugin receives DM and sends reply via Zulip API
- @all-bots message in #agent-hub reaches all 6 bots
- Non-@all-bots messages in #agent-hub are correctly ignored
- Health endpoint returns 200 with valid JSON
- Plugin survives container restart (Zulip reconnection)
### Deployment Tests
- `deploy.sh` successfully pulls latest and restarts services on all CTs
- Plugin survives container restart (Zulip reconnection)
- Config YAML validation before deployment
---
@@ -187,18 +231,16 @@ monitoring:
---
## Further Notes
### Design Decisions Summary
## Design Decisions Summary
| # | Decision | Rationale |
|---|----------|-----------|
| ADR-001 | Topic-per-agent with `-private` suffix | Clear naming, easy ACL enforcement |
| ADR-002 | @mention-based routing (not topic-based) | User confirmed, flexible for any topic |
| ADR-001 | DM-first topology | DMs are simpler, more reliable, and require no @mention parsing |
| ADR-002 | DM-based routing (not @mention) | `message.type === 'private'` is trivially detectable, no `mentioned_users` dependency |
| ADR-003 | Bot naming: `{name}-bot` | Consistent, no space issues in Zulip API |
| ADR-004 | One bot per agent, co-located on CT | Isolated failures, platform-native |
| ADR-005 | SDK `mentioned_users` field | Zulip API native, always accurate |
| ADR-006 | Dedicated `All Bots` user | Explicit @all-bots, not noisy @everyone |
| ADR-005 | [Archived] @mention via SDK — no longer needed | DM-first eliminates @mention dependency entirely |
| ADR-006 | @all-bots via content-based regex | No dedicated bot user needed, works across Zulip versions |
| ADR-007 | Platform-native plugin contracts | Reliability, platform handles state/retries |
| ADR-008 | Native message formats per platform | No translation layer needed |
| ADR-009 | Graceful degradation + user-visible errors | No silent failures |
@@ -209,11 +251,11 @@ monitoring:
### Prerequisites
1. Zulip server operational at `https://chat.sysloggh.net`
2. Zulip bot users created (7 total: tanko-bot, mumuni-bot, koonimo-bot, koby-bot, kagentz-bot, abiba-bot, all-bots)
2. Zulip bot users created (6 total: tanko-bot, mumuni-bot, koonimo-bot, koby-bot, kagentz-bot, abiba-bot)
3. `#agent-hub` stream created and all bots subscribed
4. Gitea repo `zulip-platform-plugins` created and monorepo pushed
5. Hermes gateway framework verified on Tanko/Mumuni/Koonimo/Koby CTs
6. openclaw extension API verified on Abiba CT
6. PI extension API verified on Abiba CT
7. Agent Zero plugin system verified on Kagentz CT
### Deployment Workflow
+64
View File
@@ -0,0 +1,64 @@
# ADR 1: DM-First Communication Topology
**Date**: 2026-06-21 (supersedes 2026-04-28 topic-based topology)
### Decision
Each agent communicates primarily via **Zulip Direct Messages (DMs)**. The `#agent-hub` stream is retained as an **optional broadcast-only channel** for inter-agent coordination, `@all-bots` announcements, and cross-agent visibility. Agent-private topics (`<agent>-private`) are **deprecated** in favor of native Zulip DMs.
### Context
Zulip bots are regular user accounts — any user can send a **private message** to any bot by composing a DM with the bot as the sole recipient. The bot's event queue then receives an event with `message.type: 'private'`, which requires **no @mention detection, no topic parsing, no stream subscription checks**.
The original design used topics under `#agent-hub` with `@mention` routing, but this introduced several failure modes:
| Problem | Impact |
|---------|--------|
| `mentioned_users` field not populated by Zulip 12.0 SDK | @mention detection silently fails |
| Topic routing required capturing origin topic + stream for reply | Added parsing complexity |
| All 6 bots subscribed to same stream | Every bot receives every message, wasting resources |
| `type:'stream'` events different from `type:'private'` | Required additional event filtering logic |
**DMs solve all of this natively:**
- Every DM is inherently for the bot recipient — no @mention needed
- Reply in the same DM thread — Zulip handles threading
- Only the recipient bot sees the message — no broadcast noise
- `message.type: 'private'` is trivially detectable
### Architecture Change
#### Before (Deprecated):
```
User @mentions @tanko-bot in #agent-hub > tanko-private
→ Zulip fires event to ALL 6 bot event queues
→ Each bot checks mentioned_users for its user_id
→ Only matching bot processes the event
→ Bot replies to same stream+topic
```
#### After (DM-First):
```
User DMs @tanko-bot directly
→ Zulip fires event ONLY to tanko-bot's event queue
→ Bot receives message.type: 'private'
→ No @mention detection, no topic parsing
→ Bot processes and replies in same DM thread
```
### Consequences
- **Positive:** Dramatically simpler event processing — no @mention detection, no topic routing, no mentioned_users dependency, no stream subscription requirement
- **Positive:** Each bot only receives its own messages — less noise, less resource usage
- **Positive:** Zulip's native DM threading handles all reply routing automatically
- **Positive:** No need to subscribe all 6 bots to `#agent-hub` stream (except for `@all-bots` broadcast capability)
- **Negative:** Single #agent-hub stream topic no longer shows all agent activity in one timeline
- **Negative:** Cross-agent visibility requires `@all-bots` broadcast or agents explicitly CC each other
- **Neutral:** `#agent-hub` stream is retained for: `@all-bots` broadcasts, inter-agent coordination, and user-initiated multi-agent topics
### Migration Path
1. All plugins listen for `type: 'private'` events as primary communication channel
2. `#agent-hub` stream listener is secondary — only for `@all-bots` broadcast detection
3. Existing `<agent>-private` topics are deprecated but not deleted (grace period)
4. Owners communicate with their agents via DM, not via private topics
5. `@all-bots` continues to work in `#agent-hub` for cross-platform broadcasts
-15
View File
@@ -1,15 +0,0 @@
# ADR 1: Topic & Stream Topology
**Date**: 2026-04-28
### Decision
Each agent gets a private topic under `#agent-hub` named `<agent>-private` for owner-only communication; all other topics are collaboration topics where agents respond only when @mentioned.
### Context
We need a clear separation between agent-private communication (user-agent DMs) and collaboration (multi-agent discussions). Zulip topics are the right granularity — no separate streams needed. The `-private` suffix convention is simple and self-documenting.
### Consequences
- All 6 agents co-exist in `#agent-hub` stream
- Users can create any new topic and @mention agents
- Private topics are invitation-only (bot + owner)
- No need to manage multiple streams
+51
View File
@@ -0,0 +1,51 @@
# ADR 2: DM-First Routing
**Date**: 2026-06-21 (supersedes 2026-04-28 @mention-based routing)
### Decision
Agents are triggered primarily by **Zulip Direct Messages (DMs)**. Users send a DM to any agent's bot user (e.g., `@tanko-bot`) to communicate with that agent. Stream-based `@mention` routing is **deprecated** for agent-owner communication but retained for `@all-bots` broadcasts and multi-agent coordination in `#agent-hub`.
### Context
The original design required users to `@mention` an agent in `#agent-hub` stream topics. This proved unreliable because:
1. Zulip 12.0 SDK may not populate `mentioned_users` on stream message events
2. Every bot subscribed to `#agent-hub` receives every message — wasteful
3. Topic routing (capturing origin stream + topic for reply) adds parsing complexity
4. Distinguishing `type: 'stream'` vs `type: 'private'` required extra event filtering
DMs eliminate all of these concerns:
- Only the recipient bot receives the DM — no broadcast noise
- No @mention parsing needed — the DM itself is the signal
- No topic/stream capture needed — reply in the same DM thread
- `message.type: 'private'` is trivially detectable in the event queue
### DM Event Flow
```
User sends DM to @tanko-bot
→ Zulip pushes event to tanko-bot's event queue
→ Event has: {type: 'message', message: {type: 'private', ...}}
→ Bot checks: message.type === 'private' (trivial)
→ Bot reads message.content, processes, responds
→ Bot sends reply to same DM via /api/v1/messages with
{type: 'private', to: [user_id], content: 'response'}
```
### @all-bots Exception
The stream-based approach is retained **only** for `@all-bots` broadcasts:
- `@all-bots` in `#agent-hub` → all bots detect the mention and respond
- This remains necessary for cross-platform broadcasts
- Detection can use Zulip SDK `mentioned_users` or content-based `@**All Bots**` matching
### Consequences
- **Positive:** Dramatically simpler routing logic — no @mention parsing, no topic/stream tracking
- **Positive:** Each bot only processes its own DMs — no wasted event processing
- **Positive:** Zulip's native DM threading handles all reply threading automatically
- **Positive:** No dependency on `mentioned_users` field behavior in Zulip 12.0
- **Negative:** Owners must DM their agent directly instead of using stream topics
- **Negative:** Cross-agent visibility in stream topics is reduced (mitigated by `@all-bots`)
- **Neutral:** `@all-bots` in `#agent-hub` still uses the original @mention detection pattern
-14
View File
@@ -1,14 +0,0 @@
# ADR 2: @mention-Based Routing
**Date**: 2026-04-28
### Decision
Agents are triggered exclusively by Zulip @mentions. No topic-based assignment. Users @mention an agent in any topic, and the agent's bot responds.
### Context
Topic-based routing is too rigid — it requires users to know which topic maps to which agent. @mention routing is natural in Zulip (users already know how to @mention). It also enables multi-agent conversations in a single topic.
### Consequences
- Each bot must parse @mentions from every message in `#agent-hub`
- Routing complexity moves from topic name matching to @mention resolution
- Users can freely create topics and involve agents ad-hoc
+6 -10
View File
@@ -1,15 +1,11 @@
# ADR 3: Agent Naming Convention
# ADR 3: Agent Naming Convention (Unchanged)
**Date**: 2026-04-28
**Date**: 2026-04-28 | **Status**: Still active — DM architecture does not affect naming
### Decision
Each agent bot has a display name in `{name}-bot` format with no spaces, dashes only: `tanko-bot`, `mumuni-bot`, `koonimo-bot`, `koby-bot`, `kagentz-bot`, `abiba-bot`.
Each agent bot has a display name in `{name}-bot` format: `tanko-bot`, `mumuni-bot`, etc.
### Context
Zulip @mentions work best with clean, predictable names. No spaces avoids ambiguity in @mention parsing. The `-bot` suffix makes it clear these are automated agents, not human users.
### Consequences
- @mention format: `@**tanko-bot**`
### Consequences (Unchanged)
- DM address: `@**tanko-bot**` (same as @mention format)
- Email format: `tanko-bot@chat.sysloggh.net`
- Users must type @tanko-bot (not @tanko)
- Consistent naming across all 6 agents
- Consistent naming across all 6 agents
+7 -10
View File
@@ -1,15 +1,12 @@
# ADR 4: One Zulip Bot Per Agent
# ADR 4: One Zulip Bot Per Agent (Unchanged)
**Date**: 2026-04-28
**Date**: 2026-04-28 | **Status**: Still active — DM architecture needs per-agent bots even more
### Decision
Each agent has its own dedicated Zulip bot user, co-located on the agent's CT. Each bot runs independently as a platform-native plugin.
Each agent has its own dedicated Zulip bot user, co-located on the agent's CT.
### Context
A single bot creates a single point of failure and requires message routing to the correct agent. Per-agent bots provide full isolation — if tanko's bot fails, mumuni's bot continues working. Co-location means zero A2A overhead for the local agent.
### Consequences
- 6 Zulip bot users to create and manage
### Consequences (Unchanged)
- 6 Zulip bot users
- 6 independent event streams (one per CT)
- @all-bots requires each bot to know about `All Bots` user
- Adding a new agent = create Zulip bot + deploy plugin to CT
- @all-bots still requires each bot to know about `All Bots` user
- DM isolation: only recipient bot sees the message (improvement over stream-based approach)
+37 -8
View File
@@ -1,15 +1,44 @@
# ADR 6: @all-bots via Dedicated Zulip Bot User
# ADR 6: @all-bots via Content-Based Detection
**Date**: 2026-04-28
**Date**: 2026-06-21 (supersedes 2026-04-28 dedicated bot user approach)
### Decision
@all-bots is implemented as a dedicated Zulip bot user with display name `All Bots` and email `all-bots@chat.sysloggh.net`. All 6 per-agent bots subscribe to the same event narrow and detect when `All Bots` is in `mentioned_users`.
`@all-bots` is detected via **content-based mention matching** on messages in the `#agent-hub` stream. Each bot subscribes to `#agent-hub` and uses regex to detect `@**all-bots**` or `@**All Bots**` in message content. A dedicated "All Bots" Zulip bot user is **not required**.
### Context
@all-bots needs to trigger all agents regardless of platform. A dedicated bot user is explicit and intentional — it prevents @everyone or stream-wide pings from accidentally triggering all agents. Each bot independently detects the @mention and forwards to its agent.
The original design required a 7th Zulip bot user (`all-bots@chat.sysloggh.net`) with display name `All Bots`. This user was never created during initial bot provisioning. Additionally:
- Bot users cannot reliably detect @mentions of other bot users in Zulip 12.0 (ADR-005 archived)
- Content-based regex matching (`@**all-bots**`) works on any Zulip message regardless of SDK version
- Under the DM-first architecture (ADR-001), bot-event stream subscription to `#agent-hub` is the **only** stream-level listener needed — all other communication uses DMs
### Behavior
```
User posts in #agent-hub > topic:
"@**all-bots** status report please"
→ Each bot receives event via #agent-hub subscription
→ Each bot checks: /@\*\*(?:all-bots|All Bots)\*\*/i.test(content)
→ If match: bot forwards message to its agent
→ 6 independent responses appear in the topic
```
### Active Stream Subscription
Under DM-first architecture, each agent's plugin maintains **one** stream subscription:
- **Stream:** `#agent-hub`
- **Purpose:** Only for detecting `@all-bots` broadcasts
- **Filter:** Ignore all non-`@all-bots` messages in the stream
- **Primary channel:** DMs for all agent-owner communication
### Consequences
- Must create a 7th Zulip bot user for `All Bots`
- Each agent bot's config must include `All Bots` user ID for detection
- @all-bots is opt-in: only topics that explicitly @all-bots trigger broadcast
- Cross-platform broadcast works without a central dispatcher
- **Positive:** No need to create/manage a 7th dedicated bot user
- **Positive:** Content-based detection works reliably across Zulip versions (no SDK dependency)
- **Positive:** Same regex works for case-insensitive matching (`all-bots` / `All Bots`)
- **Negative:** Slightly more processing per message (regex check)
- **Negative:** If `@all-bots` syntax changes in Zulip, regex must update
- **Neutral:** All 6 bots still independently detect and respond — same multi-response pattern