Channels
Agent Switcher
OpenClaw plugin: per-message and sticky agent (persona) switching across all channels from the master agents.list
Install
npm install
npm
Configuration Example
{
"plugins": {
"allow": ["clawrouter", "whatsapp", "agent-switcher"],
"entries": {
"agent-switcher": {
"enabled": true
}
}
}
}
README
# Agent Switcher
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.list` in `openclaw.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.
---
## Requirements
- OpenClaw gateway `>= 2026.7.1` (`compat.minGatewayVersion` / `peerDependencies`).
- A user `agents.list` configured in `openclaw.json` containing `main` (the master) plus any number of subs.
---
## Installation
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):
```jsonc
{
"plugins": {
"allow": ["clawrouter", "whatsapp", "agent-switcher"],
"entries": {
"agent-switcher": {
"enabled": true
}
}
}
}
```
If you distribute/install from source, run the build first:
```bash
npm install
npm run build # tsc -p tsconfig.json
```
> **Note for gateway installs:** the extensions dir holds a *copy* of `dist/`. After rebuilding, re-copy the new `dist/*.js` (including `agents.js`) to the extensions target and restart the gateway.
---
## Usage
### Commands
| Command | Behavior |
|---------|----------|
| `/agent` | Show the currently routed agent for this conversation (or master if none). |
| `/agent <name>` | **Stickily** route this conversation to that agent. Accepts the agent id or name, case-insensitive. `master` / `default` map to the master. |
| `/agent off` · `/agent reset` · `/agent clear` | Reset routing back to the master agent for this conversation. |
| `/agents` | List the full catalog (master + all subs) with their roles. |
Any `/agent <unknown>` returns the list of known agent ids.
### Automatic rules
Configured via `configSchema` in the plugin (`defaultAgent` + an ordered `rules` array). Each rule can match on:
- `channels` — channel ids
- `conversations` — conversation ids
- `senders` — sender ids
- `prefix` — 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.
```jsonc
"config": {
"defaultAgent": "main",
"rules": [
{ "id": "coding", "agent": "coder", "keywords": ["refactor", "write a function"] },
{ "id": "sci", "agent": "scientist", "prefix": ["research"], "sticky": ["deep dive"] }
]
}
```
---
## How the catalog is built
`src/agents.ts` builds the catalog from (in priority order):
1. `api.config.agents.list` — the full merged config handed to the plugin at registration.
2. `api.config.agents` (if the list lives directly under `agents`).
3. A file fallback reading `openclaw.json` (via `api.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.
---
## How sticky persistence works
`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.
---
## Command / hook contract (OpenClaw plugin API)
- Hooks use `api.on("message_received", (event, ctx) => ...)` (not the legacy `registerHook`).
- Commands use `api.registerCommand({ name, description, acceptsArgs, handler(ctx) })`.
- `ctx.channelId`/`ctx.conversationId`/`ctx.sessionKey` locate the conversation; `event.content`/`event.from` carry the message.
---
## File layout
```
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/
```
---
## License
MIT
channels
Comments
Sign in to leave a comment