diff --git a/docs/concepts/multi-agent.md b/docs/concepts/multi-agent.md index 59f9ecdfe4e..e915f8da086 100644 --- a/docs/concepts/multi-agent.md +++ b/docs/concepts/multi-agent.md @@ -1,6 +1,7 @@ --- summary: "Multi-agent routing: isolated agents, channel accounts, and bindings" -title: Multi-agent routing +title: "Multi-agent routing" +sidebarTitle: "Multi-agent routing" read_when: "You want multiple isolated agents (workspaces + auth) in one gateway process." status: active --- @@ -23,32 +24,21 @@ Auth profiles are **per-agent**. Each agent reads from its own: ~/.openclaw/agents//agent/auth-profiles.json ``` -`sessions_history` is the safer cross-session recall path here too: it returns -a bounded, sanitized view, not a raw transcript dump. Assistant recall strips -thinking tags, `` scaffolding, plain-text tool-call XML -payloads (including `...`, -`...`, `...`, -`...`, and truncated tool-call blocks), -downgraded tool-call scaffolding, leaked ASCII/full-width model control -tokens, and malformed MiniMax tool-call XML before redaction/truncation. + +`sessions_history` is the safer cross-session recall path here too: it returns a bounded, sanitized view, not a raw transcript dump. Assistant recall strips thinking tags, `` scaffolding, plain-text tool-call XML payloads (including `...`, `...`, `...`, `...`, and truncated tool-call blocks), downgraded tool-call scaffolding, leaked ASCII/full-width model control tokens, and malformed MiniMax tool-call XML before redaction/truncation. + -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`. + +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 loaded from each agent workspace plus shared roots such as -`~/.openclaw/skills`, then filtered by the effective agent skill allowlist when -configured. Use `agents.defaults.skills` for a shared baseline and -`agents.list[].skills` for per-agent replacement. See -[Skills: per-agent vs shared](/tools/skills#per-agent-vs-shared-skills) and -[Skills: agent skill allowlists](/tools/skills#agent-skill-allowlists). +Skills are loaded from each agent workspace plus shared roots such as `~/.openclaw/skills`, then filtered by the effective agent skill allowlist when configured. Use `agents.defaults.skills` for a shared baseline and `agents.list[].skills` for per-agent replacement. See [Skills: per-agent vs shared](/tools/skills#per-agent-vs-shared-skills) and [Skills: agent skill allowlists](/tools/skills#agent-skill-allowlists). 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](/gateway/sandboxing). + +**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](/gateway/sandboxing). + ## Paths (quick map) @@ -87,48 +77,39 @@ openclaw agents list --bindings + Use the wizard or create workspaces manually: -Use the wizard or create workspaces manually: + ```bash + openclaw agents add coding + openclaw agents add social + ``` -```bash -openclaw agents add coding -openclaw agents add social -``` - -Each agent gets its own workspace with `SOUL.md`, `AGENTS.md`, and optional `USER.md`, plus a dedicated `agentDir` and session store under `~/.openclaw/agents/`. + Each agent gets its own workspace with `SOUL.md`, `AGENTS.md`, and optional `USER.md`, plus a dedicated `agentDir` and session store under `~/.openclaw/agents/`. - + Create one account per agent on your preferred channels: -Create one account per agent on your preferred channels: + - Discord: one bot per agent, enable Message Content Intent, copy each token. + - Telegram: one bot per agent via BotFather, copy each token. + - WhatsApp: link each phone number per account. -- Discord: one bot per agent, enable Message Content Intent, copy each token. -- Telegram: one bot per agent via BotFather, copy each token. -- WhatsApp: link each phone number per account. + ```bash + openclaw channels login --channel whatsapp --account work + ``` -```bash -openclaw channels login --channel whatsapp --account work -``` - -See channel guides: [Discord](/channels/discord), [Telegram](/channels/telegram), [WhatsApp](/channels/whatsapp). + See channel guides: [Discord](/channels/discord), [Telegram](/channels/telegram), [WhatsApp](/channels/whatsapp). - - -Add agents under `agents.list`, channel accounts under `channels..accounts`, and connect them with `bindings` (examples below). - + Add agents under `agents.list`, channel accounts under `channels..accounts`, and connect them with `bindings` (examples below). - - -```bash -openclaw gateway restart -openclaw agents list --bindings -openclaw channels status --probe -``` - + ```bash + openclaw gateway restart + openclaw agents list --bindings + openclaw channels status --probe + ``` @@ -140,14 +121,11 @@ With **multiple agents**, each `agentId` becomes a **fully isolated persona**: - **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. +This lets **multiple people** share one Gateway server while keeping their AI "brains" and data isolated. ## Cross-agent QMD memory search -If one agent should search another agent's QMD session transcripts, add -extra collections under `agents.list[].memorySearch.qmd.extraCollections`. -Use `agents.defaults.memorySearch.qmd.extraCollections` only when every agent -should inherit the same shared transcript collections. +If one agent should search another agent's QMD session transcripts, add extra collections under `agents.list[].memorySearch.qmd.extraCollections`. Use `agents.defaults.memorySearch.qmd.extraCollections` only when every agent should inherit the same shared transcript collections. ```json5 { @@ -180,15 +158,15 @@ should inherit the same shared transcript collections. } ``` -The extra collection path can be shared across agents, but the collection name -stays explicit when the path is outside the agent workspace. Paths inside the -workspace remain agent-scoped so each agent keeps its own transcript search set. +The extra collection path can be shared across agents, but the collection name stays explicit when the path is outside the agent workspace. Paths inside the workspace remain agent-scoped so each agent keeps its own transcript search set. ## 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: "direct"`. Replies still come from the same WhatsApp number (no per‑agent sender identity). +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: "direct"`. 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**. + +Direct chats collapse to the agent's **main session key**, so true isolation requires **one agent per person**. + Example: @@ -228,33 +206,50 @@ Notes: Bindings are **deterministic** and **most-specific wins**: -1. `peer` match (exact DM/group/channel id) -2. `parentPeer` match (thread inheritance) -3. `guildId + roles` (Discord role routing) -4. `guildId` (Discord) -5. `teamId` (Slack) -6. `accountId` match for a channel -7. channel-level match (`accountId: "*"`) -8. fallback to default agent (`agents.list[].default`, else first list entry, default: `main`) + + + Exact DM/group/channel id. + + + Thread inheritance. + + + Discord role routing. + + + Discord. + + + Slack. + + + Per-account fallback. + + + `accountId: "*"`. + + + Fallback to `agents.list[].default`, else first list entry, default: `main`. + + -If multiple bindings match in the same tier, the first one in config order wins. -If a binding sets multiple match fields (for example `peer` + `guildId`), all specified fields are required (`AND` semantics). - -Important account-scope detail: - -- A binding that omits `accountId` matches the default account only. -- Use `accountId: "*"` for a channel-wide fallback across all accounts. -- If you later add the same binding for the same agent with an explicit account id, OpenClaw upgrades the existing channel-only binding to account-scoped instead of duplicating it. + + + - If multiple bindings match in the same tier, the first one in config order wins. + - If a binding sets multiple match fields (for example `peer` + `guildId`), all specified fields are required (`AND` semantics). + + + - A binding that omits `accountId` matches the default account only. + - Use `accountId: "*"` for a channel-wide fallback across all accounts. + - If you later add the same binding for the same agent with an explicit account id, OpenClaw upgrades the existing channel-only binding to account-scoped instead of duplicating it. + + ## 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. +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. -If you want a channel-wide default account when `accountId` is omitted, set -`channels..defaultAccount` (optional). When unset, OpenClaw falls back -to `default` if present, otherwise the first configured account id (sorted). +If you want a channel-wide default account when `accountId` is omitted, set `channels..defaultAccount` (optional). When unset, OpenClaw falls back to `default` if present, otherwise the first configured account id (sorted). Common channels supporting this pattern include: @@ -264,297 +259,298 @@ Common channels supporting this pattern include: ## Concepts -- `agentId`: one “brain” (workspace, per-agent auth, per-agent session store). +- `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::` (per-agent “main”; `session.mainKey`). +- Direct chats collapse to `agent::` (per-agent "main"; `session.mainKey`). ## Platform examples -### Discord bots per agent + + + Each Discord bot account maps to a unique `accountId`. Bind each account to an agent and keep allowlists per bot. -Each Discord bot account maps to a unique `accountId`. Bind each account to an agent and keep allowlists per bot. - -```json5 -{ - agents: { - list: [ - { id: "main", workspace: "~/.openclaw/workspace-main" }, - { id: "coding", workspace: "~/.openclaw/workspace-coding" }, - ], - }, - bindings: [ - { agentId: "main", match: { channel: "discord", accountId: "default" } }, - { agentId: "coding", match: { channel: "discord", accountId: "coding" } }, - ], - channels: { - discord: { - groupPolicy: "allowlist", - accounts: { - default: { - token: "DISCORD_BOT_TOKEN_MAIN", - guilds: { - "123456789012345678": { - channels: { - "222222222222222222": { allow: true, requireMention: false }, + ```json5 + { + agents: { + list: [ + { id: "main", workspace: "~/.openclaw/workspace-main" }, + { id: "coding", workspace: "~/.openclaw/workspace-coding" }, + ], + }, + bindings: [ + { agentId: "main", match: { channel: "discord", accountId: "default" } }, + { agentId: "coding", match: { channel: "discord", accountId: "coding" } }, + ], + channels: { + discord: { + groupPolicy: "allowlist", + accounts: { + default: { + token: "DISCORD_BOT_TOKEN_MAIN", + guilds: { + "123456789012345678": { + channels: { + "222222222222222222": { allow: true, requireMention: false }, + }, + }, }, }, - }, - }, - coding: { - token: "DISCORD_BOT_TOKEN_CODING", - guilds: { - "123456789012345678": { - channels: { - "333333333333333333": { allow: true, requireMention: false }, + coding: { + token: "DISCORD_BOT_TOKEN_CODING", + guilds: { + "123456789012345678": { + channels: { + "333333333333333333": { allow: true, requireMention: false }, + }, + }, }, }, }, }, }, - }, - }, -} -``` + } + ``` -Notes: + - Invite each bot to the guild and enable Message Content Intent. + - Tokens live in `channels.discord.accounts..token` (default account can use `DISCORD_BOT_TOKEN`). -- Invite each bot to the guild and enable Message Content Intent. -- Tokens live in `channels.discord.accounts..token` (default account can use `DISCORD_BOT_TOKEN`). - -### Telegram bots per agent - -```json5 -{ - agents: { - list: [ - { id: "main", workspace: "~/.openclaw/workspace-main" }, - { id: "alerts", workspace: "~/.openclaw/workspace-alerts" }, - ], - }, - bindings: [ - { agentId: "main", match: { channel: "telegram", accountId: "default" } }, - { agentId: "alerts", match: { channel: "telegram", accountId: "alerts" } }, - ], - channels: { - telegram: { - accounts: { - default: { - botToken: "123456:ABC...", - dmPolicy: "pairing", - }, - alerts: { - botToken: "987654:XYZ...", - dmPolicy: "allowlist", - allowFrom: ["tg:123456789"], - }, - }, - }, - }, -} -``` - -Notes: - -- Create one bot per agent with BotFather and copy each token. -- Tokens live in `channels.telegram.accounts..botToken` (default account can use `TELEGRAM_BOT_TOKEN`). - -### WhatsApp numbers per agent - -Link each account before starting the gateway: - -```bash -openclaw channels login --channel whatsapp --account personal -openclaw channels login --channel whatsapp --account biz -``` - -`~/.openclaw/openclaw.json` (JSON5): - -```js -{ - agents: { - list: [ - { - id: "home", - default: true, - name: "Home", - workspace: "~/.openclaw/workspace-home", - agentDir: "~/.openclaw/agents/home/agent", - }, - { - id: "work", - name: "Work", - workspace: "~/.openclaw/workspace-work", - agentDir: "~/.openclaw/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). + + + ```json5 { - agentId: "work", - match: { - channel: "whatsapp", - accountId: "personal", - peer: { kind: "group", id: "1203630...@g.us" }, + agents: { + list: [ + { id: "main", workspace: "~/.openclaw/workspace-main" }, + { id: "alerts", workspace: "~/.openclaw/workspace-alerts" }, + ], }, - }, - ], - - // 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: ~/.openclaw/credentials/whatsapp/personal - // authDir: "~/.openclaw/credentials/whatsapp/personal", - }, - biz: { - // Optional override. Default: ~/.openclaw/credentials/whatsapp/biz - // authDir: "~/.openclaw/credentials/whatsapp/biz", + bindings: [ + { agentId: "main", match: { channel: "telegram", accountId: "default" } }, + { agentId: "alerts", match: { channel: "telegram", accountId: "alerts" } }, + ], + channels: { + telegram: { + accounts: { + default: { + botToken: "123456:ABC...", + dmPolicy: "pairing", + }, + alerts: { + botToken: "987654:XYZ...", + dmPolicy: "allowlist", + allowFrom: ["tg:123456789"], + }, + }, }, }, - }, - }, -} -``` + } + ``` -## Example: WhatsApp daily chat + Telegram deep work + - Create one bot per agent with BotFather and copy each token. + - Tokens live in `channels.telegram.accounts..botToken` (default account can use `TELEGRAM_BOT_TOKEN`). -Split by channel: route WhatsApp to a fast everyday agent and Telegram to an Opus agent. + + + Link each account before starting the gateway: -```json5 -{ - agents: { - list: [ - { - id: "chat", - name: "Everyday", - workspace: "~/.openclaw/workspace-chat", - model: "anthropic/claude-sonnet-4-6", - }, - { - id: "opus", - name: "Deep Work", - workspace: "~/.openclaw/workspace-opus", - model: "anthropic/claude-opus-4-6", - }, - ], - }, - bindings: [ - { agentId: "chat", match: { channel: "whatsapp" } }, - { agentId: "opus", match: { channel: "telegram" } }, - ], -} -``` + ```bash + openclaw channels login --channel whatsapp --account personal + openclaw channels login --channel whatsapp --account biz + ``` -Notes: + `~/.openclaw/openclaw.json` (JSON5): -- 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: - -```json5 -{ - agents: { - list: [ - { - id: "chat", - name: "Everyday", - workspace: "~/.openclaw/workspace-chat", - model: "anthropic/claude-sonnet-4-6", - }, - { - id: "opus", - name: "Deep Work", - workspace: "~/.openclaw/workspace-opus", - model: "anthropic/claude-opus-4-6", - }, - ], - }, - bindings: [ + ```js { - agentId: "opus", - match: { channel: "whatsapp", peer: { kind: "direct", id: "+15551234567" } }, - }, - { agentId: "chat", match: { channel: "whatsapp" } }, - ], -} -``` + agents: { + list: [ + { + id: "home", + default: true, + name: "Home", + workspace: "~/.openclaw/workspace-home", + agentDir: "~/.openclaw/agents/home/agent", + }, + { + id: "work", + name: "Work", + workspace: "~/.openclaw/workspace-work", + agentDir: "~/.openclaw/agents/work/agent", + }, + ], + }, -Peer bindings always win, so keep them above the channel-wide rule. + // Deterministic routing: first match wins (most-specific first). + bindings: [ + { agentId: "home", match: { channel: "whatsapp", accountId: "personal" } }, + { agentId: "work", match: { channel: "whatsapp", accountId: "biz" } }, -## 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: - -```json5 -{ - agents: { - list: [ - { - id: "family", - name: "Family", - workspace: "~/.openclaw/workspace-family", - identity: { name: "Family Bot" }, - groupChat: { - mentionPatterns: ["@family", "@familybot", "@Family Bot"], + // 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" }, + }, }, - 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"], + ], + + // Off by default: agent-to-agent messaging must be explicitly enabled + allowlisted. + tools: { + agentToAgent: { + enabled: false, + allow: ["home", "work"], }, }, - ], - }, - bindings: [ + + channels: { + whatsapp: { + accounts: { + personal: { + // Optional override. Default: ~/.openclaw/credentials/whatsapp/personal + // authDir: "~/.openclaw/credentials/whatsapp/personal", + }, + biz: { + // Optional override. Default: ~/.openclaw/credentials/whatsapp/biz + // authDir: "~/.openclaw/credentials/whatsapp/biz", + }, + }, + }, + }, + } + ``` + + + + +## Common patterns + + + + Split by channel: route WhatsApp to a fast everyday agent and Telegram to an Opus agent. + + ```json5 { - agentId: "family", - match: { - channel: "whatsapp", - peer: { kind: "group", id: "120363999999999999@g.us" }, + agents: { + list: [ + { + id: "chat", + name: "Everyday", + workspace: "~/.openclaw/workspace-chat", + model: "anthropic/claude-sonnet-4-6", + }, + { + id: "opus", + name: "Deep Work", + workspace: "~/.openclaw/workspace-opus", + model: "anthropic/claude-opus-4-6", + }, + ], }, - }, - ], -} -``` + bindings: [ + { agentId: "chat", match: { channel: "whatsapp" } }, + { agentId: "opus", match: { channel: "telegram" } }, + ], + } + ``` -Notes: + 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. + - 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. -## Per-Agent Sandbox and Tool Configuration + + + Keep WhatsApp on the fast agent, but route one DM to Opus: + + ```json5 + { + agents: { + list: [ + { + id: "chat", + name: "Everyday", + workspace: "~/.openclaw/workspace-chat", + model: "anthropic/claude-sonnet-4-6", + }, + { + id: "opus", + name: "Deep Work", + workspace: "~/.openclaw/workspace-opus", + model: "anthropic/claude-opus-4-6", + }, + ], + }, + bindings: [ + { + agentId: "opus", + match: { channel: "whatsapp", peer: { kind: "direct", id: "+15551234567" } }, + }, + { agentId: "chat", match: { channel: "whatsapp" } }, + ], + } + ``` + + Peer bindings always win, so keep them above the channel-wide rule. + + + + Bind a dedicated family agent to a single WhatsApp group, with mention gating and a tighter tool policy: + + ```json5 + { + agents: { + list: [ + { + id: "family", + name: "Family", + workspace: "~/.openclaw/workspace-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 Each agent can have its own sandbox and tool restrictions: @@ -591,25 +587,26 @@ Each agent can have its own sandbox and tool restrictions: } ``` -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"`. + +`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 +- **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. + +`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](/tools/multi-agent-sandbox-tools) for detailed examples. +See [Multi-agent sandbox and tools](/tools/multi-agent-sandbox-tools) for detailed examples. ## Related -- [Channel Routing](/channels/channel-routing) — how messages route to agents -- [Sub-Agents](/tools/subagents) — spawning background agent runs -- [ACP Agents](/tools/acp-agents) — running external coding harnesses +- [ACP agents](/tools/acp-agents) — running external coding harnesses +- [Channel routing](/channels/channel-routing) — how messages route to agents - [Presence](/concepts/presence) — agent presence and availability - [Session](/concepts/session) — session isolation and routing +- [Sub-agents](/tools/subagents) — spawning background agent runs