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

13 KiB

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

  1. 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.

  2. 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

  1. As a human user, I want to see a friendly error message if an agent is unavailable, rather than a silent timeout.

  2. 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)

# 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

# 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