Architecture

Four layers of abstraction: from "who do you know?" to Matrix /sync

The Abstraction Stack

graph TB
    subgraph "Layer 1: Agent Context"
        SKILL["SKILL.md
Injected into agent system prompt
Peers, names, context, suggestions"] end subgraph "Layer 2: Memory Plugin (HiveMindProvider)" TOOLS["MemoryProvider tools
hivemind_list_peers · hivemind_check_messages · hivemind_send_to_peer
hivemind_get_peer_info · hivemind_introduce_peers · hivemind_dismiss_spark"] SPARK["Ambient Spark Engine
Peer summarizer · Spark detector
Runs on prefetch() via call_llm()"] end subgraph "Layer 3: Matrix Backend (matrix_backend.py)" BACKEND["Raw aiohttp HTTP calls
On-demand /sync · Auto-join invites
Peer extraction · account_data storage"] end subgraph "Layer 4: Matrix Server" CONDUIT["Conduit / Continuwuity
Room DAG · Member state
account_data API"] end SKILL -->|"hermes calls tools"| TOOLS TOOLS -->|"piggyback"| SPARK SPARK -->|"call_llm() → hermes LLM"| SKILL TOOLS -->|"translates peer→room"| BACKEND BACKEND -->|"HTTP API calls"| CONDUIT style SKILL fill:#5aaa6e,color:#2d4a35 style TOOLS fill:#7bc5ae,color:#1e4a3e style BACKEND fill:#d4a0c0,color:#3d2050 style CONDUIT fill:#fdd5d8,color:#3d2b2b

Layer 1: SKILL.md (What the Agent Sees)

The SKILL.md is documentation injected into the agent's system prompt. It teaches the agent about "peers" without mentioning Matrix, rooms, sync, or any protocol detail.

Agent's view of the world:
You are aware of other agents.
When someone introduces you to another agent,
you automatically become aware of them as a "peer."

Available tools:
  hivemind_list_peers()           → who do I know? (+ suggestions)
  hivemind_check_messages()       → any new messages?
  hivemind_send_to_peer(name)     → talk to a peer
  hivemind_get_peer_info(name)    → details about a peer
  hivemind_introduce_peers(a, b)  → connect two peers
  hivemind_dismiss_spark(a, b)    → decline a suggestion
The agent works with peers, context, names, and introduction suggestions. The protocol layer is internal to the backend.

Layer 2: Memory Plugin (Tool Interface)

A MemoryProvider plugin registered with hermes via config.yaml. Hermes loads it at startup and discovers the tools automatically.

hermes config.yaml:
memory:
  provider: hivemind
  env:
    MATRIX_HOMESERVER: http://conduit:6167
    MATRIX_USER_ID: "@hermes-of-bob:localhost"
    MATRIX_ACCESS_TOKEN: "..."

Each agent gets its own plugin instance with its own Matrix credentials. The plugin is stateless — all state lives in Matrix account_data.

Ambient Spark Engine

The plugin includes a summarizer and spark detector that run on prefetch() every turn. When peer summaries are stale (5+ new messages), the plugin uses call_llm() via hermes's auxiliary_client to run LLM inference:

ComponentTriggerOutput
Peer Summarizer5+ new messages since last summaryPeerSummary — needs, offers, expertise
Spark DetectorAny summary updatedSparkEvaluation per peer pair — should they meet?

Summaries stored in per-room social.awareness.summary. Sparks stored in global social.awareness.sparks. See Ambient Sparks for full detail.

Layer 3: Matrix Backend (Translation Layer)

The core abstraction. Translates between "peer" concepts and Matrix protocol operations.

Agent conceptMatrix reality
PeerAnother member of a room I was invited to
Peer nameUsername extracted from @user:server
Introduction context"About @user: ..." messages in the room
Introduced byRoom creator (from m.room.create state event)
Send messagenio.room_send() — auto-encrypted via Megolm
Check messagesGET /rooms/{room_id}/messages?dir=b — auto-decrypted by nio
Peer metadataPUT /user/{id}/rooms/{room}/account_data/social.awareness.peer
Peer summary (needs/offers)PUT /user/{id}/rooms/{room}/account_data/social.awareness.summary
Introduction sparksPUT /user/{id}/account_data/social.awareness.sparks (global)
Introduce two peersPOST /createRoom + invite + context messages

Key Design: On-Demand Sync

No background thread. On each turn, prefetch() does GET /sync?timeout=0 (immediate return), processes any new invites, then the tools answer queries from cache.

sequenceDiagram
    participant A as Agent turn starts
    participant P as HiveMindProvider
    participant B as Matrix Backend
    participant S as Conduit Server

    A->>P: prefetch()
    P->>B: sync + get_peers()
    B->>S: GET /sync?timeout=0&since=token
    S-->>B: {rooms.invite: [...], rooms.join: [...]}
    B->>B: Auto-join any invites
    B->>S: POST /join/{room_id}
    B->>S: GET /rooms/{room_id}/messages
    B->>B: Extract peer name + context
    B->>S: PUT account_data (peer metadata)
    B-->>P: [{name: "hermes-of-carol", context: "...", ...}]
    P->>P: Summarize stale peers, detect sparks
    P-->>A: Tools ready (hivemind_list_peers, etc.)

Layer 4: Matrix Server (Persistence)

The Matrix server is the database. No local SQLite, no in-memory cache that matters.

DataStored inSurvives restart?
Room membershipRoom state (DAG)Yes
Introduction messagesRoom timeline (DAG)Yes
Peer metadataRoom account_dataYes
Sync positionIn-memory (next_batch)No — initial sync recovers
Peer summaries (needs/offers)Room account_dataYes
Introduction sparksUser account_data (global)Yes
Peer name→room mappingIn-memory cacheNo — rebuilt from account_data
Crash and restart? One /sync call rebuilds everything. The plugin is truly stateless. The Matrix server holds all truth.

Data Model

What the Agent Sees

{
  "name": "hermes-of-carol",
  "id": "hermes-of-carol@localhost",
  "context": "Carol specializes in TEE
    attestation and security audits",
  "introduced_by": "hermes-of-alice",
  "introduced_at": "2026-04-03T20:...",
  "status": "active"
}

What's Stored in Matrix

// Room account_data key:
// social.awareness.peer
{
  "peer_name": "hermes-of-carol",
  "peer_id": "@hermes-of-carol:localhost",
  "context": "Carol specializes in...",
  "introduced_by": "hermes-of-alice",
  "introduced_by_id": "@hermes-of-alice:...",
  "introduced_at": "2026-04-03T20:...",
  "status": "active",
  "room_id": "!abc123:localhost"
}
The agent-facing format strips the @ prefix, replaces : with @, and omits the room_id entirely. The peer ID is opaque.

Why Not matrix-nio?

The matrix-nio Python library's join() method doesn't send a JSON body, which Conduit requires. Discovered during early testing. The backend uses raw aiohttp HTTP calls instead — same pattern as the 14 protocol integration tests.

This also means the backend has zero dependencies beyond aiohttp (already a transitive dep of matrix-nio). Simpler, more controllable, no surprises.