Skip to content

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:

CrateRole
stepper-protocolChannel types and DTOs: Action (TUI→core), AppEvent (core→TUI), ApprovalRequest (oneshot). Depends only on serde, uuid, and tokio sync primitives.
stepper-tuiRatatui-based TUI with inline viewport. Depends only on stepper-protocol (plus ratatui stack).
stepper-cliClap CLI binary. Wires RealCore (default=TUI agent; -p flag=headless auto-approval mode), handles auth login, config, init commands, and MCP server lifecycle.
stepper-providerTrait and normalized types for LLM providers: ChatRequest, Message, ContentBlock, ChatEvent, Usage, StopReason, ToolSpec. HTTP-free; no reqwest or tokio-rt.
stepper-providersConcrete provider adapters (Anthropic, OpenAI-compatible, Responses), auth (API key, OAuth Codex), streaming via reqwest+SSE, token storage in keyring.
stepper-configConfiguration loading: .stepper/ discovery, setting.json deep-merge, frontmatter parsing (YAML), substitution engine, model/provider resolution, JSON schema validation.
stepper-permissionPure permission evaluation engine: deny > ask > allow > mode precedence, bash redirection/command parsing, symlink escape prevention, path canonicalization.
stepper-toolsTool 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-mcpMCP 1.7 client (stdio/HTTP transport), tool namespacing, timeouts, integration with local tool registry. Validated with context7.
stepper-coreMain 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-tui depends only on stepper-protocol (plus ratatui). No core, config, providers, or HTTP access.
  • reqwest and HTTP are confined to stepper-providers, stepper-tools (web_fetch), and stepper-mcp (HTTP transport). stepper-protocol and stepper-provider trait are HTTP-free and tokio-rt-free.
  • stepper-protocol is 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**: implements chat_stream(req, cancel) → BoxStream<ChatEvent>. Normalization via WireDeltaStreamAccumulatorChatEvent.
  • **Tool**: implements spec() / 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 by ConfigProviderResolver.
  • **Approver** (tools) → **ChannelApprover** (core): Ask decision emits AppEvent::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). --resume seeds resume_context; Rewind restores, prunes, truncates turns.

Advanced Features

  • **Parallel layers**: A step with parallel: true (and optional parallel-max worker cap) causes the orchestrator to fan-out the prior layer's assign_tasks list 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 + ModelChanged event), /context (context window summary).
  • **Skills**: Layers declare skills in frontmatter; SkillTool allows models to call skill { 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: bool option enables Anthropic prefix caching; system message wrapped in cache_control: ephemeral.
  • **Model-driven compaction**: If compaction.provider is 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)