Multi-agent routing with shared memory

Specialised agents do better work than generalists. Same SOUL, different focus. The general agent is your everyday assistant. The research agent gets a SOUL that asks for thorough, structured findings. They share memory so they can hand off facts.

10-multi-agent-integration/mini-openclaw.py
python
AGENTS = {
    "general": {
        "name": "OpenClaw",
        "soul": """# OpenClaw (General Agent)
You are OpenClaw, a personal AI assistant.
... (helpful, concise, memory strategy) ...""",
        "tools": ["run_command", "read_file", "write_file", "web_search", "save_memory", "memory_search"],
    },
    "research": {
        "name": "OpenClaw Research",
        "soul": """# OpenClaw (Research Agent)
You are OpenClaw's research specialist. You focus on in-depth analysis.
... (thorough, structured findings, cites reasoning) ...""",
        "tools": ["run_command", "read_file", "web_search", "save_memory", "memory_search"],
    },
}

ROUTE_PREFIXES = {"/research": "research"}
DEFAULT_AGENT = "general"

Each agent is a dict with its own SOUL and its own list of allowed tools. Same code, two configs.

10-multi-agent-integration/mini-openclaw.py
python
def resolve_agent(user_text: str) -> tuple[str, str]:
    """Route a message to an agent based on prefix."""
    for prefix, agent_name in ROUTE_PREFIXES.items():
        if user_text.lower().startswith(prefix):
            cleaned = user_text[len(prefix):].strip()
            return agent_name, cleaned or user_text
    return DEFAULT_AGENT, user_text

Routing is one function. Strip the prefix if present, return (agent_name, cleaned_text). Default to general.

How a multi-agent turn flows

Same agent loop, different SOUL + tool list per agent. Sessions are split. Memory is shared.

Sessions are split because the general agent and the research agent should not share conversation history. The research agent's verbose findings would clutter the general agent's everyday context. Memory IS shared because facts the user shared ('I am on macOS') should be visible to both agents.

The agent loop itself is unchanged. We changed the SIGNATURE: run_agent_turn now takes an optional agent_name, and looks up SOUL + tools from the AGENTS dict instead of using a module-level constant. One supporting change: the flat TOOLS list became ALL_TOOLS, a dict keyed by tool name, so each agent can pick its own subset. Routing is a tiny function in front of the loop. Multi-agent is mostly a renaming exercise once you have the loop.

10-multi-agent-integration/mini-openclaw.py
python
ALL_TOOLS = {
    "run_command": {"type": "function", "function": {
        "name": "run_command", "description": "Run a shell command. Subject to safety checks.",
        "parameters": {"type": "object", "properties": {"command": {"type": "string", "description": "The shell command to execute"}}, "required": ["command"]}}},
    "read_file": {"type": "function", "function": {
        "name": "read_file", "description": "Read the contents of a file.",
        "parameters": {"type": "object", "properties": {"path": {"type": "string", "description": "Path to the file to read"}}, "required": ["path"]}}},
    # ... write_file, web_search, save_memory, memory_search follow the same shape
}

Every tool definition now lives in ALL_TOOLS, keyed by name. An agent config lists names, and the loop looks up the full definitions from here.

10-multi-agent-integration/mini-openclaw.py
python
def run_agent_turn(session_id: str, user_text: str, agent_name: str | None = None) -> str:
    if agent_name is None:
        agent_name, user_text = resolve_agent(user_text)

    agent_config = AGENTS[agent_name]
    agent_tools = [ALL_TOOLS[t] for t in agent_config["tools"]]

    full_session_id = f"{session_id}:{agent_name}"

    with session_locks[full_session_id]:
        history = load_session(full_session_id)
        history.append({"role": "user", "content": user_text})

        while True:
            messages = [{"role": "system", "content": agent_config["soul"]}] + history
            response = client.chat.completions.create(
                model=MODEL, max_tokens=4096, tools=agent_tools, messages=messages,
            )
            # ... rest of the loop is unchanged

The loop now reads agent_name + agent_config. Sessions are keyed by {session_id}:{agent_name}. Tool list comes from agent_config.

AI prompt: Try it: route between agents

Loading practice…