# 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
~/.opensoul/agents/<agentId>/sessions.
Auth profiles are per-agent. Each agent reads from its own:
~/.opensoul/agents/<agentId>/agent/auth-profiles.jsonMain 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 ~/.opensoul/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:
~/.opensoul/opensoul.json(orOPENSOUL_CONFIG_PATH) - State dir:
~/.opensoul(orOPENSOUL_STATE_DIR) - Workspace:
~/.opensoul/workspace(or~/.opensoul/workspace-<agentId>) - Agent dir:
~/.opensoul/agents/<agentId>/agent(oragents.list[].agentDir) - Sessions:
~/.opensoul/agents/<agentId>/sessions
# Single-agent mode (default)
If you do nothing, OpenSoul runs a single agent:
agentIddefaults tomain.- Sessions are keyed as
agent:main:<mainKey>. - Workspace defaults to
~/.opensoul/workspace(or~/.opensoul/workspace-<profile>whenOPENSOUL_PROFILEis set). - State defaults to
~/.opensoul/agents/main/agent.
# Agent helper
Use the agent wizard to add a new isolated agent:
opensoul agents add workThen add bindings (or let the wizard do it) to route inbound messages.
Verify with:
opensoul 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.mdandSOUL.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: "~/.opensoul/workspace-alex" },
{ id: "mia", workspace: "~/.opensoul/workspace-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:
peermatch (exact DM/group/channel id)guildId(Discord)teamId(Slack)accountIdmatch for a channel- channel-level match (
accountId: "*") - 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 anagentIdby(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
~/.opensoul/opensoul.json (JSON5):
{
agents: {
list: [
{
id: "home",
default: true,
name: "Home",
workspace: "~/.opensoul/workspace-home",
agentDir: "~/.opensoul/agents/home/agent",
},
{
id: "work",
name: "Work",
workspace: "~/.opensoul/workspace-work",
agentDir: "~/.opensoul/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: ~/.opensoul/credentials/whatsapp/personal
// authDir: "~/.opensoul/credentials/whatsapp/personal",
},
biz: {
// Optional override. Default: ~/.opensoul/credentials/whatsapp/biz
// authDir: "~/.opensoul/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: "~/.opensoul/workspace-chat",
model: "anthropic/claude-sonnet-4-5",
},
{
id: "opus",
name: "Deep Work",
workspace: "~/.opensoul/workspace-opus",
model: "anthropic/claude-opus-4-6",
},
],
},
bindings: [
{ agentId: "chat", match: { channel: "whatsapp" } },
{ agentId: "opus", match: { channel: "telegram" } },
],
}Notes:
- If you have multiple accounts for a channel, add
accountIdto 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.peerbinding 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: "~/.opensoul/workspace-chat",
model: "anthropic/claude-sonnet-4-5",
},
{
id: "opus",
name: "Deep Work",
workspace: "~/.opensoul/workspace-opus",
model: "anthropic/claude-opus-4-6",
},
],
},
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: "~/.opensoul/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
execis allowed and the binary exists in the sandbox. - For stricter gating, set
agents.list[].groupChat.mentionPatternsand 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: "~/.opensoul/workspace-personal",
sandbox: {
mode: "off", // No sandbox for personal agent
},
// No tool restrictions - all tools available
},
{
id: "family",
workspace: "~/.opensoul/workspace-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.