Architecture
10-crate Cargo workspace design: crate roles, isolation invariants, and data flow from CLI bootstrap through multi-layer agent orchestration to TUI.
Stepper is a layered CLI/TUI AI coding agent built in Rust, structured as a 10-crate Cargo workspace. This page outlines the crate organization, data flow from CLI bootstrap to agent execution, and the core isolation invariants that ensure clean architectural boundaries.
Crate Overview
The workspace contains 10 implemented crates (of 13 originally planned), each with a specific responsibility in the system:
| Crate | Role |
|---|---|
stepper-protocol | Channel types and DTOs: Action (TUI→core), AppEvent (core→TUI), ApprovalRequest (oneshot). Depends only on serde, uuid, and tokio sync primitives. |
stepper-tui | Ratatui-based TUI with inline viewport. Depends only on stepper-protocol (plus ratatui stack). |
stepper-cli | Clap CLI binary. Wires RealCore (default=TUI agent; -p flag=headless auto-approval mode), handles auth login, config, init commands, and MCP server lifecycle. |
stepper-provider | Trait and normalized types for LLM providers: ChatRequest, Message, ContentBlock, ChatEvent, Usage, StopReason, ToolSpec. HTTP-free; no reqwest or tokio-rt. |
stepper-providers | Concrete provider adapters (Anthropic, OpenAI-compatible, Responses), auth (API key, OAuth Codex), streaming via reqwest+SSE, token storage in keyring. |
stepper-config | Configuration loading: .stepper/ discovery, setting.json deep-merge, frontmatter parsing (YAML), substitution engine, model/provider resolution, JSON schema validation. |
stepper-permission | Pure permission evaluation engine: deny > ask > allow > mode precedence, bash redirection/command parsing, symlink escape prevention, path canonicalization. |
stepper-tools | Tool trait and registry; 9 built-in tools (read/write/edit/bash/search/grep/todo/web_fetch); MCP tool bridging; secret path detection; permission gating via ToolCx. |
stepper-mcp | MCP 1.7 client (stdio/HTTP transport), tool namespacing, timeouts, integration with local tool registry. Validated with context7. |
stepper-core | Main orchestrator: provider resolution, AgentLoop (ReAct pattern), multi-step pipelines, parallel layer execution, session/checkpoint management, handoff summarization, built-in slash commands. |
Isolation Invariants
Three strict architectural boundaries are enforced via CI tests (crates/stepper-cli/tests/isolation.rs) using cargo metadata analysis:
stepper-tuidepends only onstepper-protocol(plus ratatui). No core, config, providers, or HTTP access.reqwestand HTTP are confined tostepper-providers,stepper-tools(web_fetch), andstepper-mcp(HTTP transport).stepper-protocolandstepper-providertrait are HTTP-free and tokio-rt-free.stepper-protocolis clap-free (no CLI parsing in the channel contract).
High-Level Data Flow
text
stepper-cli main.rs (#[tokio::main])
├─ build_orchestrator(model, mode, cwd)
│ Config::load → build_steps(layer frontmatter+skills) → ensure_provider(convention fallback)
│ McpManager::connect(mcpServers) → register tools to base_tools
│ ConfigProviderResolver + RuleSet + HookHost
├─ channels: mpsc<Action>(TUI→core) + mpsc<AppEvent>(core→TUI) + CancellationToken
├─ stepper-core::spawn_core(orchestrator, session, action_rx, cancel) → event_rx [RealCore]
│ while action:
│ SubmitInput → checkpoint_turn → Orchestrator.run_turn → session append/save
│ SlashCommand → commands::expand(substitution) → run_turn
│ Rewind → restore+turns truncate
│ RunShell → bash tool single-turn execution
│ Orchestrator.run_turn: SessionStart → step layers (sequence or parallel):
│ resolver.resolve(model) → Box<dyn LlmProvider>
│ base_tools.filtered(allow/deny).filter_mcp
│ ToolCx{cwd, project_root, mode, rules, approver=ChannelApprover, cancel}
│ AgentLoop.drive(system, handoff): stream→token/usage emit→compact→tool exec
│ (gate→approver) → result injection → repeat
│ emit: LayerStarted/Finished/ModelChanged/UsageUpdated/ToolCall* → handoff
└─ stepper-tui::run_tui(event_rx, action_tx, init, cancel)
blocking input thread(event::poll/read) → mpsc → select!{input, 33ms tick, AppEvent rx, cancel}
input → (mode-dependent) Action → AppState.apply_action → Effect(Send/CommitToScrollback)
ApprovalRequested(oneshot) → overlay y/a/n → reply.send (resumes agent loop)spawn_fake_core (mock) remains in stepper-tui for walking-skeleton testing; the CLI uses only RealCore (stepper-core::spawn_core). Both satisfy the same channel contract and are interchangeable.Key Types & Contracts
- **Channel contract** (fixed):
mpsc<Action>+mpsc<AppEvent>+CancellationToken. Satisfied by both RealCore and mock. - **
LlmProvider**: implementschat_stream(req, cancel) → BoxStream<ChatEvent>. Normalization viaWireDelta→StreamAccumulator→ChatEvent. - **
Tool**: implementsspec()/call(args, cx) → ToolResult.ToolCx.gate(PermissionRequest)enforces permission evaluation and approval gating. Built-in and MCP tools use the same trait. - **
ProviderResolver**: model ref →Box<dyn LlmProvider>+ModelInfo. Implemented byConfigProviderResolver. - **
Approver** (tools) → **ChannelApprover** (core): Ask decision emitsAppEvent::ApprovalRequested{oneshot}→ TUI overlay → user reply → resumes agent loop. - **Orchestrator / AgentLoop**:
run_turn→ step layers (each with independent context, provider, tools) →AgentLoop.drive(ReAct pattern). Handoff = free-text summarization chain. - **Sessions & Checkpoints**:
SessionStore(.stepper/sessions/<id>.json),Snapshotter(.stepper/checkpoints/<turn>/file copy).--resumeseedsresume_context;Rewindrestores, prunes, truncates turns.
Advanced Features
- **Parallel layers**: A step with
parallel: true(and optionalparallel-maxworker cap) causes the orchestrator to fan-out the prior layer'sassign_taskslist to workers. Each worker is a sub-agent. Fallback to sequential if no task list. - **Built-in slash commands**:
/help,/clear(session reset),/model [provider/model-id](validation + first step switch +ModelChangedevent),/context(context window summary). - **Skills**: Layers declare
skillsin frontmatter;SkillToolallows models to callskill { name }to invoke layer-specific skills. Progressive disclosure: skill name+description are advertised in system prompt, body served on-demand. - **Dispatch tool** (C4): Models can call
dispatch(...)to spawn parallel sub-agents (workers) from the current layer onward. Orchestrator must enable it; sub-agents cannot recurse. - **Prompt caching**:
ChatRequest.cache: booloption enables Anthropic prefix caching; system message wrapped incache_control: ephemeral. - **Model-driven compaction**: If
compaction.provideris configured, the orchestrator uses that provider's model to summarize message history on soft threshold (0.70 context), keeping last 6 messages intact. - **Permission gating**:
ToolCx::gate(request)evaluates rules against mode (Auto/Plan/AcceptEdits); bash atoms with redirection/$()escalate Ask to require explicit approval. - **MCP timeouts**: Connect timeout (default 10s,
STEPPER_MCP_CONNECT_TIMEOUT_MS) prevents hung servers at startup. Tool call timeout (default 120s,STEPPER_MCP_TOOL_TIMEOUT_MS) prevents mid-turn hangs.
Build & Test
sh
cargo check --workspace # Type check
cargo clippy --workspace --all-targets # Lint (0 warnings enforced)
cargo test --workspace # full network-less suite (live mcp_live/e2e skip if env unset)
cargo run # Interactive TUI (real tty + API keys required)
cargo run -- -p "..." --model anthropic/claude-x # Headless one-shot (auto-approve)
cargo run -- --resume <session-id> # Resume session
cargo run -- auth login --codex # Codex OAuth login
cargo run -- config --schema | --validate # Settings schema/validation
cargo run -- init # Scaffold .stepper/ (stepper.md + setting.json)