Skip to content

feat: add slash commands for agent switching#2790

Open
dgageot wants to merge 3 commits into
docker:mainfrom
dgageot:feat/agent-switching-commands
Open

feat: add slash commands for agent switching#2790
dgageot wants to merge 3 commits into
docker:mainfrom
dgageot:feat/agent-switching-commands

Conversation

@dgageot
Copy link
Copy Markdown
Member

@dgageot dgageot commented May 13, 2026

Summary

Adds support for slash commands that switch the active agent. For example, declaring /plan in the agent config makes the planner sub-agent take over the conversation when the user types /plan.

Usage

A new agent: field can be set on a command in agent.yaml:

agents:
  root:
    sub_agents: [planner, reviewer]
    commands:
      plan:
        description: Hand off to the planner
        agent: planner
      review:
        description: Hand off to the reviewer
        agent: reviewer

When the user types /plan, the active agent is switched to planner. Anything typed after the command (e.g. /plan add a logout button) is forwarded to the target agent as the first user message.

agent: can be combined with instruction: to switch and send a fixed prompt; on its own it acts as a pure handoff.

A complete example lives in examples/agent_switching_commands.yaml.

Changes

  • pkg/config/types/commands.go — new Agent field on Command; YAML parser accepts the agent key.
  • pkg/runtime/commands.go — new LookupCommand helper; ResolveCommand forwards trailing args verbatim for agent-only commands.
  • pkg/app/app.go — exposes App.LookupCommand.
  • pkg/tui/handlers.gohandleAgentCommand switches the active agent before sending the resolved message.
  • pkg/cli/runner.goPrepareUserMessage does the same for the non-TUI flow.
  • agent-schema.json — documents the new agent property.
  • examples/agent_switching_commands.yaml — full example with /plan, /review, /back.
  • pkg/runtime/commands_test.go — unit tests for the new behavior.

Validation

  • task lint clean.
  • task test passes for all changed packages.
  • task build succeeds.

Add support for commands that switch the active agent via a new 'agent:' field
in the commands section of agent.yaml. Users can now use /plan to switch to a
planner sub-agent, /review to switch to a reviewer, etc. Trailing arguments
after the command are forwarded to the target agent as the first prompt.
@dgageot dgageot requested a review from a team as a code owner May 13, 2026 14:01
trungutt
trungutt previously approved these changes May 13, 2026
Copy link
Copy Markdown

@docker-agent docker-agent left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Assessment: 🔴 CRITICAL

Comment thread pkg/cli/runner.go
// This must happen before the message is added to the session so the
// next runtime turn runs on the right agent.
if cmd, _, ok := runtime.LookupCommand(ctx, rt, userInput); ok && cmd.Agent != "" {
if err := rt.SetCurrentAgent(cmd.Agent); err != nil {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[HIGH] Agent switched before ResolveCommand — raw /command string sent to target agent

In PrepareUserMessage, rt.SetCurrentAgent(cmd.Agent) is called on line 333 before runtime.ResolveCommand on line 339. Since ResolveCommand delegates to LookupCommand, which calls rt.CurrentAgentInfo(ctx) to look up the command by name, after SetCurrentAgent the lookup runs against the new agent's command table — not the original agent's.

If the target agent (e.g. planner) does not have the /plan command in its own commands: map (the common case — it's usually only defined on the root agent), LookupCommand returns ok=false and ResolveCommand falls back to returning the raw input verbatim, e.g. /plan add a login button, instead of the intended resolved form add a login button. That raw slash-command string is then sent to the target agent as the first user message.

Trigger: Any agent-only slash command (no instruction:) where the target agent doesn't re-declare that command.

Fix: Capture the resolved content before switching agents (mirroring the TUI flow in handleAgentCommand, which correctly calls LookupCommand/ResolveCommand first and switches after):

resolvedContent := runtime.ResolveCommand(ctx, rt, userInput)

if cmd, _, ok := runtime.LookupCommand(ctx, rt, userInput); ok && cmd.Agent != "" {
    if err := rt.SetCurrentAgent(cmd.Agent); err != nil {
        slog.WarnContext(ctx, "Failed to switch agent for /command", "agent", cmd.Agent, "error", err)
    }
}

Comment thread pkg/tui/handlers.go Outdated
}
}

if resolved != "" {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[MEDIUM] Resolved message sent to old agent when handleSwitchAgent fails

When m.application.SwitchAgent(agentName) returns an error, handleSwitchAgent returns early with a notification.ErrorCmd without updating m.sessionState.CurrentAgentName. Back in handleAgentCommand, the if resolved != "" branch on line 645 is then unconditionally reached, and SendMsg{Content: resolved} is appended to cmds and dispatched via tea.Batch.

The result: the user sees the "Failed to switch to agent" notification AND has their message silently dispatched to the original (still-active) agent — not the intended target. This can cause user confusion because the message appears to be accepted even though the intended agent hand-off failed.

Suggested fix: Skip the SendMsg when the agent switch was requested but failed, by tracking whether the switch succeeded:

switchSucceeded := true
if ok && cmd.Agent != "" && cmd.Agent != m.sessionState.CurrentAgentName() {
    prevAgent := m.sessionState.CurrentAgentName()
    switched, switchCmd := m.handleSwitchAgent(cmd.Agent)
    m = switched.(*appModel)
    if switchCmd != nil {
        cmds = append(cmds, switchCmd)
    }
    switchSucceeded = m.sessionState.CurrentAgentName() != prevAgent
}

if resolved != "" && switchSucceeded {
    cmds = append(cmds, core.CmdHandler(messages.SendMsg{Content: resolved}))
}

Wires the new agent-switching slash command feature into the built-in coder agent:
- Adds a planner sub-agent with read-only tools (filesystem, fetch, todo, user_prompt)
- Adds /plan command on root agent to switch to plan mode
- Adds symmetric /back command on planner agent to hand work back
- Updates root instruction to mention the /plan command
Addresses review feedback from docker-agent bot:

1. In pkg/cli/runner.go (PrepareUserMessage):
   - Moved ResolveCommand call BEFORE SetCurrentAgent
   - This ensures the command is looked up in the original agent's command table
   - Prevents raw slash-command strings from being sent to target agents when
     the target agent doesn't have the command defined

2. In pkg/tui/handlers.go (handleAgentCommand):
   - Added switchSucceeded flag to track whether agent switch was successful
   - Only send resolved message if the agent switch succeeded
   - Prevents messages from being sent to the wrong agent when switching fails
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants