Reeve
Core Concepts

Multi-Agent Routing

Multi-agent routing: isolated agents, channel accounts, and bindings

Multi-Agent Routing

Goal: multiple isolated agents (separate workspace + agentDir + sessions), plus multiple channel accounts (e.g. two WhatsApps) in one running Gateway. Inbound is routed to an agent via bindings.

What is “one agent”?

An agent is a fully scoped brain with its own:

  • Workspace (files, AGENTS.md/SOUL.md/USER.md, local notes, persona rules).
  • State directory (agentDir) for auth profiles, model registry, and per-agent config.
  • Session store (chat history + routing state) under ~/.reeve/agents/<agentId>/sessions.

Auth profiles are per-agent. Each agent reads from its own:

~/.reeve/agents/<agentId>/agent/auth-profiles.json

Main agent credentials are not shared automatically. Never reuse agentDir across agents (it causes auth/session collisions). If you want to share creds, copy auth-profiles.json into the other agent's agentDir.

Skills are per-agent via each workspace’s skills/ folder, with shared skills available from ~/.reeve/skills. See Skills: per-agent vs shared.

The Gateway can host one agent (default) or many agents side-by-side.

Workspace note: each agent’s workspace is the default cwd, not a hard sandbox. Relative paths resolve inside the workspace, but absolute paths can reach other host locations unless sandboxing is enabled. See Sandboxing.

Paths (quick map)

  • Config: ~/.reeve/reeve.json (or REEVE_CONFIG_PATH)
  • State dir: ~/.reeve (or REEVE_STATE_DIR)
  • Workspace: ~/reeve (or ~/reeve-<agentId>)
  • Agent dir: ~/.reeve/agents/<agentId>/agent (or agents.list[].agentDir)
  • Sessions: ~/.reeve/agents/<agentId>/sessions

Single-agent mode (default)

If you do nothing, Reeve runs a single agent:

  • agentId defaults to main.
  • Sessions are keyed as agent:main:<mainKey>.
  • Workspace defaults to ~/reeve (or ~/reeve-<profile> when REEVE_PROFILE is set).
  • State defaults to ~/.reeve/agents/main/agent.

Agent helper

Use the agent wizard to add a new isolated agent:

reeve agents add work

Then add bindings (or let the wizard do it) to route inbound messages.

Verify with:

reeve agents list --bindings

Multiple agents = multiple people, multiple personalities

With multiple agents, each agentId becomes a fully isolated persona:

  • Different phone numbers/accounts (per channel accountId).
  • Different personalities (per-agent workspace files like AGENTS.md and SOUL.md).
  • Separate auth + sessions (no cross-talk unless explicitly enabled).

This lets multiple people share one Gateway server while keeping their AI “brains” and data isolated.

One WhatsApp number, multiple people (DM split)

You can route different WhatsApp DMs to different agents while staying on one WhatsApp account. Match on sender E.164 (like +15551234567) with peer.kind: "dm". Replies still come from the same WhatsApp number (no per‑agent sender identity).

Important detail: direct chats collapse to the agent’s main session key, so true isolation requires one agent per person.

Example:

{
  agents: {
    list: [
      { id: "alex", workspace: "~/reeve-alex" },
      { id: "mia", workspace: "~/reeve-mia" }
    ]
  },
  bindings: [
    { agentId: "alex", match: { channel: "whatsapp", peer: { kind: "dm", id: "+15551230001" } } },
    { agentId: "mia",  match: { channel: "whatsapp", peer: { kind: "dm", id: "+15551230002" } } }
  ],
  channels: {
    whatsapp: {
      dmPolicy: "allowlist",
      allowFrom: ["+15551230001", "+15551230002"]
    }
  }
}

Notes:

  • DM access control is global per WhatsApp account (pairing/allowlist), not per agent.
  • For shared groups, bind the group to one agent or use Broadcast groups.

Routing rules (how messages pick an agent)

Bindings are deterministic and most-specific wins:

  1. peer match (exact DM/group/channel id)
  2. guildId (Discord)
  3. teamId (Slack)
  4. accountId match for a channel
  5. channel-level match (accountId: "*")
  6. fallback to default agent (agents.list[].default, else first list entry, default: main)

Multiple accounts / phone numbers

Channels that support multiple accounts (e.g. WhatsApp) use accountId to identify each login. Each accountId can be routed to a different agent, so one server can host multiple phone numbers without mixing sessions.

Concepts

  • agentId: one “brain” (workspace, per-agent auth, per-agent session store).
  • accountId: one channel account instance (e.g. WhatsApp account "personal" vs "biz").
  • binding: routes inbound messages to an agentId by (channel, accountId, peer) and optionally guild/team ids.
  • Direct chats collapse to agent:<agentId>:<mainKey> (per-agent “main”; session.mainKey).

Example: two WhatsApps → two agents

~/.reeve/reeve.json (JSON5):

{
  agents: {
    list: [
      {
        id: "home",
        default: true,
        name: "Home",
        workspace: "~/reeve-home",
        agentDir: "~/.reeve/agents/home/agent",
      },
      {
        id: "work",
        name: "Work",
        workspace: "~/reeve-work",
        agentDir: "~/.reeve/agents/work/agent",
      },
    ],
  },

  // Deterministic routing: first match wins (most-specific first).
  bindings: [
    { agentId: "home", match: { channel: "whatsapp", accountId: "personal" } },
    { agentId: "work", match: { channel: "whatsapp", accountId: "biz" } },

    // Optional per-peer override (example: send a specific group to work agent).
    {
      agentId: "work",
      match: {
        channel: "whatsapp",
        accountId: "personal",
        peer: { kind: "group", id: "1203630...@g.us" },
      },
    },
  ],

  // Off by default: agent-to-agent messaging must be explicitly enabled + allowlisted.
  tools: {
    agentToAgent: {
      enabled: false,
      allow: ["home", "work"],
    },
  },

  channels: {
    whatsapp: {
      accounts: {
        personal: {
          // Optional override. Default: ~/.reeve/credentials/whatsapp/personal
          // authDir: "~/.reeve/credentials/whatsapp/personal",
        },
        biz: {
          // Optional override. Default: ~/.reeve/credentials/whatsapp/biz
          // authDir: "~/.reeve/credentials/whatsapp/biz",
        },
      },
    },
  },
}

Example: WhatsApp daily chat + Telegram deep work

Split by channel: route WhatsApp to a fast everyday agent and Telegram to an Opus agent.

{
  agents: {
    list: [
      {
        id: "chat",
        name: "Everyday",
        workspace: "~/reeve-chat",
        model: "anthropic/claude-sonnet-4-5"
      },
      {
        id: "opus",
        name: "Deep Work",
        workspace: "~/reeve-opus",
        model: "anthropic/claude-opus-4-5"
      }
    ]
  },
  bindings: [
    { agentId: "chat", match: { channel: "whatsapp" } },
    { agentId: "opus", match: { channel: "telegram" } }
  ]
}

Notes:

  • If you have multiple accounts for a channel, add accountId to the binding (for example { channel: "whatsapp", accountId: "personal" }).
  • To route a single DM/group to Opus while keeping the rest on chat, add a match.peer binding for that peer; peer matches always win over channel-wide rules.

Example: same channel, one peer to Opus

Keep WhatsApp on the fast agent, but route one DM to Opus:

{
  agents: {
    list: [
      { id: "chat", name: "Everyday", workspace: "~/reeve-chat", model: "anthropic/claude-sonnet-4-5" },
      { id: "opus", name: "Deep Work", workspace: "~/reeve-opus", model: "anthropic/claude-opus-4-5" }
    ]
  },
  bindings: [
    { agentId: "opus", match: { channel: "whatsapp", peer: { kind: "dm", id: "+15551234567" } } },
    { agentId: "chat", match: { channel: "whatsapp" } }
  ]
}

Peer bindings always win, so keep them above the channel-wide rule.

Family agent bound to a WhatsApp group

Bind a dedicated family agent to a single WhatsApp group, with mention gating and a tighter tool policy:

{
  agents: {
    list: [
      {
        id: "family",
        name: "Family",
        workspace: "~/reeve-family",
        identity: { name: "Family Bot" },
        groupChat: {
          mentionPatterns: ["@family", "@familybot", "@Family Bot"]
        },
        sandbox: {
          mode: "all",
          scope: "agent"
        },
        tools: {
          allow: ["exec", "read", "sessions_list", "sessions_history", "sessions_send", "sessions_spawn", "session_status"],
          deny: ["write", "edit", "apply_patch", "browser", "canvas", "nodes", "cron"]
        }
      }
    ]
  },
  bindings: [
    {
      agentId: "family",
      match: {
        channel: "whatsapp",
        peer: { kind: "group", id: "120363999999999999@g.us" }
      }
    }
  ]
}

Notes:

  • Tool allow/deny lists are tools, not skills. If a skill needs to run a binary, ensure exec is allowed and the binary exists in the sandbox.
  • For stricter gating, set agents.list[].groupChat.mentionPatterns and keep group allowlists enabled for the channel.

Per-Agent Sandbox and Tool Configuration

Starting with v2026.1.6, each agent can have its own sandbox and tool restrictions:

{
  agents: {
    list: [
      {
        id: "personal",
        workspace: "~/reeve-personal",
        sandbox: {
          mode: "off",  // No sandbox for personal agent
        },
        // No tool restrictions - all tools available
      },
      {
        id: "family",
        workspace: "~/reeve-family",
        sandbox: {
          mode: "all",     // Always sandboxed
          scope: "agent",  // One container per agent
          docker: {
            // Optional one-time setup after container creation
            setupCommand: "apt-get update && apt-get install -y git curl",
          },
        },
        tools: {
          allow: ["read"],                    // Only read tool
          deny: ["exec", "write", "edit", "apply_patch"],    // Deny others
        },
      },
    ],
  },
}

Note: setupCommand lives under sandbox.docker and runs once on container creation. Per-agent sandbox.docker.* overrides are ignored when the resolved scope is "shared".

Benefits:

  • Security isolation: Restrict tools for untrusted agents
  • Resource control: Sandbox specific agents while keeping others on host
  • Flexible policies: Different permissions per agent

Note: tools.elevated is global and sender-based; it is not configurable per agent. If you need per-agent boundaries, use agents.list[].tools to deny exec. For group targeting, use agents.list[].groupChat.mentionPatterns so @mentions map cleanly to the intended agent.

See Multi-Agent Sandbox & Tools for detailed examples.

Agent Registry

Reeve's gateway can host a registry of agents, each with a specific role in the three-tier architecture. A typical production registry:

Agent IDRoleDomainWorkspace
mainCoordinatorUser interface, routing, delegation~/reeve
agentpikManagerAgentPik product (backend + frontend)~/reeve-agentpik
freyaManagerFreya product~/reeve-freya
textlandsManagerTextlands product~/reeve-textlands
reeve-backendManagerReeve core development~/reeve-backend
clara-healthManagerClara health tracking~/reeve-clara-health
sierra-healthManagerSierra health tracking~/reeve-sierra-health

Each agent has its own workspace with AGENTS.md, SOUL.md, MEMORY.md, skills, and session store. Managers hold domain context — repo structure, recent changes, architecture decisions — that the coordinator doesn't need.

Slack Channel Bindings

Agents can be bound to specific Slack channels for domain-scoped conversations:

{
  bindings: [
    // Each product's Slack channel routes to its manager
    { agentId: "agentpik", match: { channel: "slack", peer: { id: "C_AGENTPIK" } } },
    { agentId: "freya", match: { channel: "slack", peer: { id: "C_FREYA" } } },
    { agentId: "textlands", match: { channel: "slack", peer: { id: "C_TEXTLANDS" } } },
    { agentId: "reeve-backend", match: { channel: "slack", peer: { id: "C_REEVE_DEV" } } },

    // DMs and unmatched channels → coordinator
    { agentId: "main", match: { channel: "slack" } },
  ]
}

This gives each product its own Slack channel where the manager agent has full domain context. The coordinator handles DMs and general channels.

Three-Tier Architecture Flow

Multi-agent routing powers the three-tier architecture:

User ──→ Coordinator (main)

              ├── sessions_send("agentpik", "Build auth feature")
              │       → Manager (agentpik)
              │              │
              │              ├── sessions_spawn(task="Implement auth API")
              │              │       → Worker (sub-agent)
              │              │
              │              └── sessions_spawn(task="Write auth tests")
              │                      → Worker (sub-agent)

              └── sessions_send("freya", "Check deployment status")
                      → Manager (freya)

                             └── sessions_spawn(task="Run deploy check")
                                     → Worker (sub-agent)

sessions_send vs sessions_spawn

Two tools for delegation, with different semantics:

sessions_sendsessions_spawn
TargetNamed agent (by agentId)New sub-agent (ephemeral)
SessionRoutes to agent's main sessionCreates a new :subagent: session
ContextAgent has its own persistent contextFresh context, just the task
Use caseCoordinator → Manager routingManager → Worker delegation
LifetimeAgent persists across messagesSub-agent is one-shot
Tool restrictionsSubject to role enforcementWorkers: unrestricted

Rule of thumb:

  • Coordinator → Manager: Use sessions_send to route domain requests to the manager who holds context
  • Manager → Worker: Use sessions_spawn to delegate implementation to a fresh worker
  • Coordinator → Worker: Use sessions_spawn for non-domain tasks (general research, one-off edits)

Pipeline Integration

Pipeline V3 and V3.5 integrate with multi-agent routing:

  • Pipeline PM acts as a specialized manager that orchestrates a team of worker agents
  • Worker agents spawned by the pipeline get session IDs like pipeline-Architect-1, which are automatically excluded from enforcement
  • Cross-repo pipelines (V3.5) route tasks to the correct manager's domain based on target_repo
  • The coordinator can spawn a pipeline, then monitor its progress via heartbeat checks on .pipeline/progress.json

Example flow:

User: "Build user auth for AgentPik"
  → Coordinator routes to agentpik manager
    → Manager spawns pipeline:
        PipelinePM(task="user auth", repos={"backend": ..., "frontend": ...})
          → Worker: Architect (designs)
          → Worker: Backend Dev (implements API)
          → Worker: Frontend Dev (implements UI)
          → Worker: QA (validates)

Role Enforcement

The Role Enforcer Plugin automatically enforces tier boundaries:

  • Coordinator can read + research + delegate, but can't edit code or run mutations
  • Managers can investigate + delegate, but can't edit any files or run implementation commands
  • Workers are unrestricted — they do the actual work

This is codified enforcement of the operating model, not just guidelines. See the Role Enforcer docs for full configuration.

On this page