Files
zulip-platform-plugins/docs/PRD.md
T
kagentz-bot 705c29a4e0
CI / validate (pull_request) Failing after 1s
feat: DM-First v2 architecture - Tanko live on CT 112
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
2026-06-21 07:50:30 -04:00

278 lines
13 KiB
Markdown

# Product Requirements Document: Zulip Platform Plugins
**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 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 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 reliably receive user requests or communicate via Zulip DMs.
---
## Solution
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 │
│ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 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
- **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
---
## User Stories
### Agent Communication
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 in #agent-hub**, so that all 6 agents across all frameworks receive the broadcast.
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.
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
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.
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
7. As a **human user**, I want to see **a friendly error message** if an agent is unavailable, rather than a silent timeout.
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.
---
## Architecture Decisions
### DM-First Communication
Each agent communicates via Zulip **Direct Messages**. When a user sends a DM to an agent's bot:
```
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'}
```
**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
### @all-bots Broadcast
`@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`)
```yaml
# config.yaml (one per bot, stored alongside plugin)
agent:
name: "Tanko"
bot_username: "tanko-bot"
bot_email: "tanko-bot@chat.sysloggh.net"
bot_api_key: "<api_key>"
owner_email: "jerome@sysloggh.net"
zulip:
server_url: "https://chat.sysloggh.net"
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
retry_count: 3
graceful_message: "{{agent_name}} is processing your request — please wait..."
monitoring:
health_endpoint_enabled: true
health_port: 9200
```
---
## 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 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
### Naming Convention
- Bot users: `{name}-bot` (lowercase, dash separator) — `tanko-bot`, `mumuni-bot`, etc. (ADR-003)
- DM address: `@**tanko-bot**` (same format as @mention)
- Display names: capitalize properly — "Tanko Bot", "Mumuni Bot"
---
## Testing Decisions
### Unit Tests
- 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 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
- Config YAML validation before deployment
---
## Out of Scope (v1.0)
- **Multi-realm Zulip:** All agents operate within a single Zulip realm
- **Message attachments:** v1 sends and receives text only (no file uploads)
- **Rich formatting:** v1 uses plain markdown responses from agents
- **A2A as primary transport:** Cross-platform communication routes through Zulip, not direct A2A
- **CI/CD pipeline:** Deployment is script-based (`deploy.sh`), not CI/CD
- **Rate limiting:** Zulip's built-in rate limiting is sufficient for initial use
- **Dashboard/monitoring UI:** Health endpoint data consumed by existing harness infra
- **Message history search:** Zulip's native search handles this
---
## Design Decisions Summary
| # | Decision | Rationale |
|---|----------|-----------|
| 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 | [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 |
| ADR-010 | Gitea monorepo + deploy.sh | Single source of truth, versioned |
| ADR-011 | Owner in per-agent config.yaml | Explicit, version-controllable |
| ADR-012 | @all-bots spans all frameworks | Single @all-bots reaches every agent |
### Prerequisites
1. Zulip server operational at `https://chat.sysloggh.net`
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. PI extension API verified on Abiba CT
7. Agent Zero plugin system verified on Kagentz CT
### Deployment Workflow
```
1. Developer pushes plugin changes to Gitea repo
2. Run: ./scripts/deploy.sh
3. deploy.sh SSHes into each CT:
a. git pull
b. Install/update dependencies
c. Restart plugin service (systemd or supervisor)
4. Verify health endpoint on each CT
```
### Risk Mitigation
- **Template bug in Zulip 12.0-1:** Portico-header renders 500 on configs without `REGISTRATION_OPEN` or with Authentik SSO — resolved via template patch `{% if settings.REGISTRATION_OPEN %}` guard
- **Docker container restarts:** Plugin auto-reconnects via Zulip SDK's built-in reconnect
- **API key rotation:** Keys stored in plugin config, updated via Gitea push