# Slug Code — Agent & Developer Reference FOSS Rust rewrite of Claude Code. Pluggable LLM backend targeting OpenAI-compatible APIs (vLLM primary). Binary: `slug`. Package: `slug-code`. Rust 2024 edition. Repo layout: `master` = original TypeScript Claude Code source (reference only). `rust-rewrite` = this Rust implementation (all work happens here). --- ## Module Map ``` src/ ├── main.rs CLI entry (clap). Wires all modules. Handles session flags. ├── config/mod.rs Config struct + PermissionMode enum. TOML + CLI + env. ├── provider/ │ ├── mod.rs Provider trait + OpenAIProvider (SSE streaming impl) │ └── types.rs ChatMessage, Role, ToolCall, StreamEvent, StreamChunk ├── agent/mod.rs Core loop: user input → LLM stream → tool exec → repeat ├── tools/ │ ├── mod.rs Tool trait + ToolRegistry (HashMap>) │ ├── bash.rs BashTool — runs via `bash -c`, captures stdout+stderr │ ├── read.rs ReadTool — reads file with line numbers, offset+limit │ ├── write.rs WriteTool — writes file, creates parent dirs │ ├── edit.rs EditTool — exact string replacement, enforces uniqueness │ ├── glob.rs GlobTool — globwalk, max_depth 20, sorted output │ └── grep.rs GrepTool — prefers rg, falls back to grep, 250 line cap ├── permissions/mod.rs PermissionHandler. Glob allow/deny. ask/yolo/sandbox/allowEdits. ├── slugmd/mod.rs Loads SLUG.md hierarchy every turn, 40K char budget. ├── session/mod.rs JSONL persistence at ~/.slug/sessions/. SessionManager. ├── hooks/mod.rs HookManager. 5 events, command+prompt types. .slug/hooks.json. ├── compact/mod.rs Compactor. ToolResultTrim→Truncate. /compact command. └── tui/mod.rs Interactive REPL. Wires agent+session+hooks. Slash commands. ``` --- ## Key Types ### provider/types.rs ```rust ChatMessage { role: Role, content: Option, tool_calls: Option>, tool_call_id: Option } // Constructors: ChatMessage::system(&str), ::user(&str), ::assistant(Option, Option>), ::tool_result(&str, &str) Role: System | User | Assistant | Tool // serde: lowercase ToolCall { id: String, call_type: String, function: FunctionCall } FunctionCall { name: String, arguments: String } // arguments is JSON string StreamEvent: Text(String) | ToolCallDelta(ToolCallDelta) | Finish | Done ToolCallDelta { index: usize, id: Option, name: Option, arguments_delta: Option } ``` ### provider/mod.rs ```rust trait Provider: Send + Sync { fn stream_chat(&self, messages: &[ChatMessage], tools: &[ToolDefinition]) -> Pin> + Send + '_>>; } // OpenAIProvider implements Provider. POST /chat/completions with stream:true. // SSE parsing: buffer bytes → split on \n → strip "data: " prefix → parse StreamChunk. // Tool calls assembled via ToolCallAccumulator keyed by delta.index. ``` ### tools/mod.rs ```rust trait Tool: Send + Sync { fn definition(&self) -> ToolDefinition; fn execute(&self, args: &Value) -> Result; } ToolDefinition { name: String, description: String, parameters: Value } // parameters is JSON Schema // ToolRegistry: HashMap>. execute() returns Result — errors become tool result strings in agent. ``` ### config/mod.rs ```rust Config { endpoint, api_key, model, system_prompt, max_tokens: u32, temperature: Option, max_tool_rounds: usize, permission_mode: PermissionMode } // Defaults: endpoint="http://localhost:8000/v1", model="default", max_tokens=4096, max_tool_rounds=50 // Load order: TOML file → with_cli_overrides() PermissionMode: Ask | Yolo | AllowEdits | Sandbox(String) // serde: { "type": "ask" } / { "type": "yolo" } / { "type": "allowedits" } / { "type": "sandbox", ... } ``` ### permissions/mod.rs ```rust PermissionRequest: Bash { command: &str } | FileWrite { path: &str } | FileEdit { path: &str } // check() order: deny rules → allow rules → mode fallback // Glob patterns: "Bash(npm *)", "Edit(src/**)", "Write(tests/**)" // Settings loaded from ~/.slug/settings.json then .slug/settings.json (merged, project extends global) // glob_match(pattern, text): ** matches across /, * does not ``` ### session/mod.rs ```rust SessionManager { sessions_dir: PathBuf } Session { session_id: String, messages: Vec } SessionMeta { session_id, created_at, last_used_at, working_directory, model, summary } // Storage: ~/.slug/sessions/{id}.jsonl (one ChatMessage per line) + {id}.meta.json // Methods: create_session(), load_session(&str), save_message(&str, &ChatMessage), list_sessions(), get_latest_session(), fork_session(&str) ``` ### hooks/mod.rs ```rust HookEvent: PreToolUse { tool_name, args } | PostToolUse { tool_name, args, result } | UserPromptSubmit { prompt } | SessionStart | SessionEnd HookAction: Command { command: String } | Prompt { content: String } HookResult { additional_context: Option, blocked: bool, block_reason: Option } // HookManager::new() loads ~/.slug/settings.json + .slug/hooks.json // fire(&HookEvent) → HookResult. Command hooks: sh -c, 30s timeout, non-zero = blocked. // Config: { "hooks": [{ "event": "PreToolUse", "tool_filter": "bash", "action": { "type": "command", "command": "..." } }] } ``` ### compact/mod.rs ```rust CompactionStrategy: Full | Truncate | ToolResultTrim Compactor { max_context_tokens: usize } // default 200_000 // estimate_tokens(text) = chars / 4 // needs_compaction() = estimated > 80% of max // ToolResultTrim: replace Tool role messages older than last 5 with "[result truncated — N chars]" // Truncate: keep system prompt + last 10 user-turn groups // extract_session_memory(): structured summary of task, files, errors, decisions ``` ### slugmd/mod.rs ```rust load_slug_context() -> String // Load order (all optional, concatenated with headers): // 1. ~/.slug/SLUG.md "# Global Rules" // 2. ./SLUG.md "# Project Rules" // 3. .slug/rules/*.md "# Rule: {filename}" (sorted alphabetically) // 4. ./SLUG.local.md "# Local Notes (private)" // 40,000 char hard limit — truncates at line boundary with warning appended // Returns "" if no files exist (no-op path, no overhead) // NEVER stored in Agent.messages — reloaded from disk on every LLM call ``` ### agent/mod.rs ```rust Agent { provider, tools, permissions, messages: Vec, max_rounds: usize } // new() → seeds messages with [ChatMessage::system(config.system_prompt)] // new_with_history() → prepends system prompt then extends with prior_messages // messages() → &[ChatMessage] (used for session saving) // stream_turn(input, on_text) → streams LLM response, executes tools, loops // run_once(prompt) → non-interactive, returns final text // compact() → ToolResultTrim first, then Truncate if still over threshold // messages_with_slug_context() → [slug_context_system_msg, ...self.messages] (rebuilt each call) // execute_with_permission(name, args) → checks PermissionHandler before calling ToolRegistry // Tool execution: read/glob/grep = always allowed. bash/write/edit = permission checked. ``` --- ## Data Flow ``` User input → tui::run() → hook_mgr.fire(UserPromptSubmit) // can block or inject context → agent.stream_turn(input, on_text) → messages.push(ChatMessage::user(input)) → messages_with_slug_context() // fresh SLUG.md prepended every call → provider.stream_chat(messages, tool_defs) → POST /v1/chat/completions (stream: true) → SSE parse → yield StreamEvent::{Text, ToolCallDelta, Finish, Done} → on_text(chunk) called for each Text event // prints to stdout in TUI → ToolCallAccumulators assembled from deltas → if tool_calls not empty: → messages.push(assistant msg with tool_calls) → for each tool_call: → execute_with_permission(name, args) → permissions.check(request) // deny→allow→mode → tools.execute(name, args) // ToolRegistry dispatch → messages.push(tool_result) → loop (up to max_tool_rounds) → if no tool_calls: messages.push(assistant msg), return → session_mgr.save_message() for each new message ``` --- ## Coding Standards ### Error Handling - Use `?` for propagation everywhere. No `unwrap()` outside tests. - Return `anyhow::Result` from all fallible functions. - Tool execution errors are caught and returned as the tool result string — never propagate to agent loop. - Use `anyhow::bail!("message")` for early returns with context. - Use `anyhow::anyhow!("message")` to construct errors. ### Async - All async code uses `tokio`. Runtime is `#[tokio::main]` in main.rs. - Streams use `async_stream::stream!` macro with explicit `yield Ok(...)` / `yield Err(...)`. - Never `yield Err` inside `async_stream` using `?` — yield the error explicitly and return. - Provider `stream_chat` returns `Pin> + Send + '_>>`. ### Serialization - All wire types (ChatMessage, ToolCall, SessionMeta) derive `Serialize + Deserialize`. - `skip_serializing_if = "Option::is_none"` on optional fields to keep JSON clean. - Tool arguments arrive as `serde_json::Value` — extract with `.as_str()`, `.as_u64()` etc, never index directly. - Config uses `#[serde(tag = "type", rename_all = "lowercase")]` for PermissionMode enum. ### Adding a New Tool 1. Create `src/tools/{name}.rs` implementing `Tool` trait. 2. Add `mod {name};` to `src/tools/mod.rs`. 3. Register: `registry.register(Box::new({name}::{Name}Tool));` in `ToolRegistry::new()`. 4. Permission: if mutating (writes files, runs code), add a `PermissionRequest` variant and check in `agent::execute_with_permission`. 5. Write unit tests in the same file under `#[cfg(test)]`. ### Adding a New Provider 1. Create `src/provider/{name}.rs` implementing `Provider` trait. 2. `pub use` from `src/provider/mod.rs`. 3. Wire into `main.rs` behind a config flag or CLI arg. 4. Must return `Pin> + Send + '_>>`. 5. Normalize provider-specific formats to `StreamEvent` — the agent loop is provider-agnostic. --- ## Testing Standards Testing is **non-negotiable**. Every module must have tests. Agents: do not consider a task complete without writing tests. ### Required Tests Per Module - **Tools**: Test each tool with valid args, missing args, and error cases (file not found, bad pattern, etc.) - **Provider**: Test `build_request_body` with and without tools, with and without temperature. - **Permissions**: Test each PermissionMode, glob pattern matching, deny overrides allow, sandbox path checking. - **Session**: Test create/save/load round-trip, fork copies messages, list_sessions returns sorted. - **Hooks**: Test command hook execution, blocked result on non-zero exit, prompt hook injection. - **Compact**: Test ToolResultTrim preserves last 5, Truncate preserves system prompt + last 10 groups, estimate_tokens. - **Slugmd**: Test 40K truncation, missing files are skipped, section headers are present. ### Test Patterns ```rust #[cfg(test)] mod tests { use super::*; use tempfile::TempDir; // use tempfile crate for file-based tests #[test] fn test_edit_tool_not_found() { let tool = EditTool; let args = serde_json::json!({ "file_path": "/nonexistent", "old_string": "x", "new_string": "y" }); assert!(tool.execute(&args).is_err()); } } ``` - Use `tempfile::TempDir` for any test touching the filesystem. - Test tool errors produce `Err`, not panics. - Test permission glob matching exhaustively — it's a custom implementation. - Integration tests go in `tests/` at crate root. --- ## Documentation Standards Every public item must have a doc comment. Agents: do not submit code without doc comments on public API. ```rust /// One-line summary. /// /// Longer explanation if the behavior is non-obvious. /// Include what errors are returned and under what conditions. pub fn load_slug_context() -> String { ... } ``` - `///` for public items (rendered by rustdoc). - `//` for implementation comments explaining *why*, not *what*. - Mark unfinished areas with `// TODO(feature): description` not just `// TODO`. - Module-level `//! ...` doc comments required for every `mod.rs`. --- ## CLI Reference ``` slug [OPTIONS] -e, --endpoint LLM API base URL [env: SLUG_ENDPOINT] -k, --api-key API key [env: SLUG_API_KEY] -m, --model Model name [env: SLUG_MODEL] -p, --prompt Non-interactive single prompt -c, --config Config file path --system-prompt Override system prompt --yolo Skip all permission prompts --sandbox Auto-approve ops within directory --allow-edits Auto-approve file edits in cwd --continue Resume most recent session --resume Resume specific session --fork-session Fork session into new branch ``` Config file: `~/.slug/config.toml` Permission rules: `~/.slug/settings.json` or `.slug/settings.json` Hooks: `.slug/hooks.json` SLUG.md files: `~/.slug/SLUG.md`, `./SLUG.md`, `.slug/rules/*.md`, `./SLUG.local.md` Sessions: `~/.slug/sessions/{id}.jsonl` --- ## What's Not Done Yet (Remaining Work) **High priority:** - Subagent parallelism — fork (shared cache), teammate (file mailbox), worktree (isolated branch) - Ratatui TUI — proper terminal UI with markdown rendering, scrollback, syntax highlighting - Retry with exponential backoff — 10 retries, jitter, model fallback on repeated 5xx - Stream interruption — Escape key aborts active stream cleanly, context preserved - Wire PreToolUse/PostToolUse hooks into agent::execute_with_permission - Concurrent tool execution — read/glob/grep run in parallel, bash/write/edit serial **Medium priority:** - MCP client — connect to MCP servers, deferred tool loading - Computer use (screenshot, click, type) - Auto-compact trigger when context approaches limit **Low priority:** - Session search / conversation history browser - Anthropic provider — `/v1/messages` format, different streaming protocol - Google Gemini provider --- ## Anti-Patterns — Never Do These - No `unwrap()` or `expect()` in non-test code. - Never store SLUG.md content in `Agent.messages` — it must be reloaded every turn. - Never add analytics, telemetry, or tracking of any kind. - Never add Anthropic-internal features (feature flags, bridge mode, CCR, Kairos, etc.). - Do not add backwards-compat shims — just change the code. - Do not add error handling for impossible cases — trust internal invariants. - Do not add abstractions for one-time use — three similar lines beat a premature helper. - Tool errors should return `Err(...)` — the agent catches them and turns them into tool result strings. Don't swallow errors silently. - Permission checks must happen in `agent::execute_with_permission` — tools themselves are unaware of permissions.