docs(plan): add command centralization design
This commit is contained in:
parent
d8683deb57
commit
c272386baa
1 changed files with 156 additions and 0 deletions
156
docs/plans/2026-03-01-command-centralization-design.md
Normal file
156
docs/plans/2026-03-01-command-centralization-design.md
Normal file
|
|
@ -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.
|
||||
Loading…
Add table
Reference in a new issue