# Multi-Agent Routing System

How GoClaw decides which agent answers an inbound message: `ResolveAgent` matches the channel, peer, thread, tenant, roles and account against the routing rules in eight priority tiers, then builds the session key.

`internal/routing/routing.go` + `internal/routing/identity_links.go` + `internal/routing/routing_test.go`

## Architecture

```mermaid
flowchart TD
    IN["<b>Inbound Message → RouteInput</b><br/>• Channel, AccountID, PeerKind (direct/group), PeerID<br/>• ParentPeerKind, ParentPeerID (thread inheritance)<br/>• TenantID, PlatformRoles[]"]

    RES["<b>ResolveAgent(input) — 8-tier priority matching</b><br/>1. Peer match (exact channel + peer)<br/>2. Parent peer match (thread inherits parent binding)<br/>3. Tenant + Roles (tenant + role intersection)<br/>4. Roles match (channel role/dept without tenant)<br/>5. Tenant match (tenant without role filter)<br/>6. Account match (account ID)<br/>7. Channel match (channel-wide, no peer filter)<br/>8. Default (config.ResolveDefaultAgentID)"]

    KEY["<b>buildSessionKey(agentID, input)</b><br/>• ResolveIdentity() → cross-channel session sharing<br/>• Multi-agent: agent:channel:peer<br/>• Single-agent: channel:peer (backward compat)<br/>• Output: AgentID + SessionKey + MatchedBy"]

    IN --> RES --> KEY
```

## Feature Comparison

| # | Feature | OpenClaw | GoClaw | Status |
|---|---------|----------|---------|--------|
| R.1 | Multi-agent routing | ✅ | ✅ `ResolveAgent()` — 8-tier priority | Done |
| R.2 | Peer binding (channel+peer) | ✅ | ✅ Exact peer match (kind + ID) | Done |
| R.3 | **Thread inheritance** | — | ✅ ParentPeerID → inherits parent binding | **GoClaw only** |
| R.4 | **Tenant + Role matching** | — | ✅ Tenant + platform role intersection (Discord guild and role IDs; App API) | **GoClaw only** |
| R.5 | Tenant-wide binding | ✅ | ✅ TenantID match (Discord guild, Teams group conversation) | Done |
| R.6 | Account binding | ✅ | ✅ Account ID → specific agent (the bot on Teams, Telegram and Facebook; the sender on Discord) | Done |
| R.7 | Channel-wide binding | ✅ | ✅ All messages on channel → agent | Done |
| R.8 | Default agent fallback | ✅ | ✅ `config.ResolveDefaultAgentID()` | Done |
| R.9 | **Identity-linked sessions** | — | ✅ `ResolveIdentity()` — cross-channel session sharing | **GoClaw only** |
| R.10 | **Multi-agent session keys** | — | ✅ `agent:channel:peer` format | **GoClaw only** |
| R.11 | **Peer kind normalization** | — | ✅ "channel" → "group" alias | **GoClaw only** |
| R.12 | Debug logging | ✅ | ✅ `debug.Log("routing", ...)` | Done |

### What each channel supplies

A binding can only match what the channel puts on the message:

| Channel | Account ID | Peer ID | Tenant ID | Platform roles |
|---------|------------|---------|-----------|----------------|
| Discord | the sender's user ID | `dm:<user ID>` in a DM; `<channel ID>:<user ID>` in a guild channel, for messages, reactions and slash commands alike; the parent is the channel itself, or for a thread the channel it belongs to | guild ID | the member's role IDs |
| MS Teams | the bot's app ID | conversation ID (`;messageid=…` removed) | the conversation ID in a group chat or channel; empty in a 1:1 chat | none |
| Telegram | the bot's ID | chat ID | none | none |
| Facebook | the page ID | sender's page-scoped ID | none | none |
| App API | `app_api` | the employee's user ID | from the caller's identity | from the caller's identity |

A Discord guild message's peer ID includes the user, so a `peer` binding with a bare channel ID matches through the parent: every message in that channel and in its threads. Route a whole Discord server with `tenant_id`. Teams never supplies roles, so a Teams binding with `platform_roles` never matches.

### Config Example

```yaml
agents:
  default: support-bot
  list:
    - id: support-bot
      model: gemini/gemini-2.5-flash
    - id: dev-bot
      model: claude/claude-sonnet-4-5-20250929

bindings:
  - match:
      channel: discord
      peer:
        kind: group
        id: "123456789012345678"           # a Discord channel and its threads
    agent_id: dev-bot

  - match:
      channel: discord
      tenant_id: "112233445566778899"      # a Discord server (guild ID)
      platform_roles: ["998877665544332211"]  # Discord role ID
    agent_id: dev-bot

  - match:
      channel: msteams
      tenant_id: "19:0123456789abcdef0123456789abcdef@thread.v2"  # one Teams group chat or channel
    agent_id: dev-bot

  - match:
      channel: discord                     # the rest of Discord
    agent_id: support-bot
```

**Coverage:** every feature in the table: routing with 8 priorities, identity links, thread inheritance.
