Per-message and sticky agent (persona) switching across all OpenClaw channels, driven by the user's master agents.list.
The plugin catalogs every agent defined in your OpenClaw config (the master + all subs) at load time and lets you route any conversation to a specific persona — either for a single message (one-shot) or persistently (sticky, survives restarts).
- Repo-bound: the agent catalog is populated dynamically from
agents.listinopenclaw.json— no hardcoded agent names. - One-shot routing via keyword/prefix rules.
- Sticky routing via
/agent <name>or sticky keyword rules, persisted through a gateway restart. - Reset back to the master agent at any time.
- OpenClaw gateway
>= 2026.7.1(compat.minGatewayVersion/peerDependencies). - A user
agents.listconfigured inopenclaw.jsoncontainingmain(the master) plus any number of subs.
Copy the agent-switcher/dist build output into your OpenClaw extensions directory and enable it:
~/.openclaw/extensions/agent-switcher/dist/ # the .js build output
Add the plugin to your plugin entries (or allow-list):
If you distribute/install from source, run the build first:
npm install
npm run build # tsc -p tsconfig.jsonNote for gateway installs: the extensions dir holds a copy of
dist/. After rebuilding, re-copy the newdist/*.js(includingagents.js) to the extensions target and restart the gateway.
Any /agent <unknown> returns the list of known agent ids.
Configured via configSchema in the plugin (defaultAgent + an ordered rules array). Each rule can match on:
channels— channel idsconversations— conversation idssenders— sender idsprefix— message begins with one of these (one-shot)keywords— message contains any of these (one-shot)sticky— message contains any of these and routes sticky (persistent)
Rules are evaluated in order; the first hard match (prefix/keyword → one-shot, or sticky keyword → sticky) wins.
"config": {
"defaultAgent": "main",
"rules": [
{ "id": "coding", "agent": "coder", "keywords": ["refactor", "write a function"] },
{ "id": "sci", "agent": "scientist", "prefix": ["research"], "sticky": ["deep dive"] }
]
}src/agents.ts builds the catalog from (in priority order):
api.config.agents.list— the full merged config handed to the plugin at registration.api.config.agents(if the list lives directly underagents).- A file fallback reading
openclaw.json(viaapi.runtime.fs) next to the gateway root.
The master is defaultAgent (or the agent flagged master: true, or main). Every other entry is a sub. resolveId matches ids/names case-insensitively and maps master/default/off → the master id.
src/sticky.ts — persistStickyRoute binds a conversation to an agent via the core channel session surface
(core.channel.session.updateLastRoute / session.updateLastRoute), writing a ChannelRouteRef for
agent:channel:direct:conversationId. Because the route policy becomes lastRoutePolicy = "session", the binding
survives a gateway restart. The in-process state.json map is a fast-path mirror, with the core route as the
authoritative backstop.
- Hooks use
api.on("message_received", (event, ctx) => ...)(not the legacyregisterHook). - Commands use
api.registerCommand({ name, description, acceptsArgs, handler(ctx) }). ctx.channelId/ctx.conversationId/ctx.sessionKeylocate the conversation;event.content/event.fromcarry the message.
agent-switch/
├── README.md
├── openclaw.plugin.json # plugin manifest (id, version, configSchema)
├── package.json # npm metadata + build script
├── tsconfig.json
├── src/
│ ├── index.ts # registration: hooks + commands
│ ├── agents.ts # dynamic catalog from agents.list
│ ├── rules.ts # ordered match rules (one-shot / sticky)
│ ├── sticky.ts # persistStickyRoute (core session route)
│ ├── handoff.ts # handoff message formatting
│ └── sdk-types.d.ts # light type hints for the plugin surface
├── dist/ # compiled output (loaded by the gateway)
└── docs/
MIT

{ "plugins": { "allow": ["clawrouter", "whatsapp", "agent-switcher"], "entries": { "agent-switcher": { "enabled": true } } } }