From c272386baa5cd77f2c503b278c53477399824e53 Mon Sep 17 00:00:00 2001 From: mingmxren Date: Sun, 1 Mar 2026 14:46:09 +0800 Subject: [PATCH] docs(plan): add command centralization design --- ...026-03-01-command-centralization-design.md | 156 ++++++++++++++++++ 1 file changed, 156 insertions(+) create mode 100644 docs/plans/2026-03-01-command-centralization-design.md diff --git a/docs/plans/2026-03-01-command-centralization-design.md b/docs/plans/2026-03-01-command-centralization-design.md new file mode 100644 index 000000000..a04b9d735 --- /dev/null +++ b/docs/plans/2026-03-01-command-centralization-design.md @@ -0,0 +1,156 @@ +# Cross-Channel Command Centralization Design + +## Background + +Current command behavior is split across two execution paths: + +- `pkg/commands` owns command definitions and dispatcher matching. +- `pkg/agent/loop.go` still executes command business logic through a hardcoded `handleCommand` switch. + +This creates drift between "registered commands" and "actually executable commands", especially when channel filtering is defined in `pkg/commands` but bypassed by `AgentLoop` fallback handling. + +## Goals + +- Make generic commands (`/start`, `/help`, `/new`, `/session`, `/show`, `/list`) consistently available across channels. +- Centralize command definition and execution in one domain (`pkg/commands`). +- Keep architecture extensible and easy to reason about: channel adapters do transport, agent does orchestration, commands do command policy + execution. + +## Non-Goals + +- No permission/role system redesign in this phase. +- No platform-specific slash command UX redesign (autocomplete, rich command metadata beyond current fields). +- No routing/session semantic change outside command execution flow. + +## Confirmed Runtime Policies + +- Unknown slash command (e.g. `/foo`) must pass through to LLM as normal user input. +- Registered command that is unsupported on current channel must return explicit user-facing error and stop further processing. + +## Root Cause Summary + +The mismatch comes from mixed responsibilities: + +1. `commands.Dispatcher` can identify commands by registry, but commands without handlers may be passed down. +2. Channel adapters use dispatcher results to decide local interception vs fallback. +3. `AgentLoop.handleCommand` contains an independent switch that executes command logic without using channel constraints from registry. + +Result: command registry is not authoritative for behavior. + +## Design Decision + +Adopt **Agent-central execution with Commands-domain authority**: + +- `pkg/commands` is the only source of command metadata, support scope, parser behavior, and handler execution. +- `AgentLoop` becomes an orchestrator that invokes `commands.Executor` before LLM execution. +- Channel packages stop owning business command execution; they only normalize inbound/outbound platform messages. +- Telegram command menu registration continues, driven by `pkg/commands` metadata. + +## Architecture + +### 1) Commands Domain (`pkg/commands`) + +Introduce/normalize these concepts: + +- `Definition`: command metadata + aliases + supported channels + handler. +- `Executor`: parses inbound text and returns a tri-state decision. +- `Runtime`: minimal capability interface injected by `AgentLoop` (session ops, route scope, config reads, reply sink). + +Tri-state result contract: + +- `handled`: command executed and response produced. +- `rejected`: command recognized but unsupported on this channel (explicit error). +- `passthrough`: unknown command or non-command; continue to normal LLM flow. + +### 2) Agent Domain (`pkg/agent`) + +`processMessage` flow becomes: + +1. Resolve route + agent. +2. Build command runtime context (scopeKey/session services/config/channel/reply). +3. Call `commands.Executor.Execute(...)`. +4. Branch by result: + - `handled` -> return response. + - `rejected` -> return explicit error. + - `passthrough` -> continue existing session+LLM path. + +`AgentLoop` no longer owns business command switch logic. + +### 3) Channel Domain (`pkg/channels/*`) + +- Inbound text is published to bus as-is. +- No local business execution for generic commands. +- Telegram keeps asynchronous `RegisterCommands(...)`, sourcing command list from registry filtered for channel and visibility. + +## Data Flow + +1. Channel receives message. +2. Channel publishes `bus.InboundMessage`. +3. `AgentLoop.processMessage` resolves route/scope. +4. `commands.Executor` decides `handled/rejected/passthrough`. +5. Only `passthrough` continues to LLM. + +This ensures one command behavior path for Telegram, WhatsApp, CLI, and Cron. + +## API Shape (Sketch) + +```go +type Outcome int + +const ( + OutcomePassthrough Outcome = iota + OutcomeHandled + OutcomeRejected +) + +type ExecuteResult struct { + Outcome Outcome + Command string + Reply string + Err error +} + +type Runtime interface { + Channel() string + ScopeKey() string + SessionOps() SessionOps + Config() *config.Config +} +``` + +## Migration Strategy + +1. Add executor tri-state contract in `pkg/commands` with tests. +2. Move `/new` and `/session` logic from `AgentLoop` into command handlers using runtime session ops. +3. Move `/show` and `/list` logic into command handlers. +4. Replace `AgentLoop` command switch with `commands.Executor` call. +5. Remove channel-side business execution fallback paths for generic commands. +6. Keep Telegram command registration driven by same registry. + +## Testing Strategy + +### Unit (commands) + +- Parse normal slash and mention syntax. +- Registered+supported -> handled. +- Registered+unsupported -> rejected. +- Unknown slash -> passthrough. + +### Agent integration + +- Command handled path skips LLM. +- Rejected path returns explicit error and skips LLM. +- Passthrough path still hits LLM. +- `/new` and `/session` work consistently for non-Telegram channels. + +### Channel regression + +- Telegram startup command registration unaffected. +- WhatsApp/WhatsApp native no longer diverge due to local command execution. + +## Acceptance Criteria + +- Command behavior is determined only by `pkg/commands` definitions + executor. +- No generic command business logic remains in `AgentLoop.handleCommand`. +- Unknown slash commands continue to LLM. +- Unsupported registered commands return explicit errors. +- README command behavior statements align with runtime behavior.