1 line
30 KiB
JSON
1 line
30 KiB
JSON
{"repo": "ben1009/pi-rs", "files": {"docs/rfc/001-core-architecture.md": {"c7573c18d750cef4399986cc250cb53ba40371e3": {"t": 1785746410.866967, "n": 14726, "h": "11f0cc722fadaed7", "c": "# RFC: `pi-rs` — a multi-LLM coding agent in Rust\n\nStatus: **living document** (updated 2026-05-23 to reflect current implementation)\nAuthor: liu\nDate: 2026-05-22\n\n## 1. Goals\n\n- Single-binary CLI that runs an interactive loop with an LLM and can edit code on disk.\n- Small, readable surface. Target ~1.5k LOC for v0.\n- Mirror the Claude Code tool-use loop closely enough that prompts and behaviors transfer.\n- Support five providers out of the box: **OpenAI, Anthropic, Google Gemini, DeepSeek, Kimi**.\n\n## 2. Non-goals (v0)\n\n- TUI, streaming UI, syntax highlighting.\n- Sub-agents, hooks, skills.\n- Slash commands beyond `/clear`, `/exit`, `/tokens`, `/save`, `/compact`.\n- Sandboxing/permissioning beyond a confirm-y/n on bash + writes outside CWD.\n- Binary file handling (base64 read/write).\n- Native provider adapters (Anthropic Messages API, Gemini native).\n\n> **Note:** Several items originally listed here (MCP, conversation persistence, context accounting, SSE streaming) have since been implemented and are documented in §14.\n\n## 3. Wire format: OpenAI-compatible Chat Completions\n\nAll five providers expose an OpenAI-compatible `/v1/chat/completions` endpoint with tool-use support. We speak that protocol in the default code path.\n\n| Provider | Base URL (no trailing slash) | Auth |\n|-----------|---------------------------------------------------------------|----------------------------|\n| OpenAI | `https://api.openai.com/v1` | `Authorization: Bearer …` |\n| Anthropic | `https://api.anthropic.com/v1` | `Authorization: Bearer …` |\n| Gemini | `https://generativelanguage.googleapis.com/v1beta/openai` | `Authorization: Bearer …` |\n| DeepSeek | `https://api.deepseek.com/v1` | `Authorization: Bearer …` |\n| Kimi | `https://api.moonshot.ai/v1` | `Authorization: Bearer …` |\n\nNotes:\n- Base URLs **must not** have a trailing slash; the client appends `/chat/completions`.\n- Anthropic's compat endpoint accepts `Authorization: Bearer` only; the `x-api-key` header is for the native Messages API and is **not** sent.\n- Kimi default is `api.moonshot.ai` (international). The CN endpoint `api.moonshot.cn` is configurable.\n\nDefault model IDs live in a single `const` table in `config.rs`; the README links to provider model-list docs. Model names rot fast.\n\nTradeoff: we lose vendor-specific features (Anthropic prompt-caching headers, Gemini grounding, OpenAI strict tools, extended thinking blocks). A `LlmClient` trait keeps the door open for native adapters later.\n\n## 4. Architecture\n\nCurrent source tree (as of 2026-05-23):\n\n```\nsrc/\n main.rs # CLI entry, arg parsing, agent loop\n lib.rs # Library exports\n config.rs # provider registry, default model table, precedence\n confirm.rs # y/n confirmation prompts (--yolo bypass)\n context.rs # per-model context-window tracking and compaction\n session.rs # conversation persistence, --resume, --sessions, /save\n llm/\n mod.rs # LlmClient trait, ChatRequest/ChatResponse, ToolCall\n openai_compat.rs # OpenAI-compatible client (default)\n tools/\n mod.rs # Tool trait, registry, JSON schema helpers\n bash.rs # bash execution with streaming output\n read_file.rs # read UTF-8 files with line numbers (tool name: `read`)\n write_file.rs # write files (tool name: `write`)\n edit.rs # exact string replacement\n mcp.rs # MCP client over stdio JSON-RPC 2.0\n```\n\nThe original RFC planned `agent.rs` and `errors.rs`; the agent loop lives in `main.rs` and errors are handled via `anyhow` + `color-eyre` throughout.\n\n`LlmClient` trait:\n\n```rust\n#[async_trait]\npub(crate) trait LlmClient: Send + Sync {\n async fn complete(&self, req: ChatRequest) -> Result<ChatResponse>;\n async fn complete_stream(&self, req: ChatRequest) -> Result<EventStream>;\n}\n```\n\n`ChatRequest` / `ChatResponse` use the OpenAI shape (`role`, `content`, `tool_calls`, `tool_call_id`) and are crate-private.\n\n## 5. Configuration\n\nPrecedence: CLI flag → env var → built-in defaults.\n\n```toml\n# Configuration via environment variables:\n# PI_PROVIDER, PI_MODEL, OPENAI_API_KEY, ANTHROPIC_API_KEY, etc.\n\n[providers.anthropic]\nbase_url = \"https://api.anthropic.com/v1\"\napi_key_env = \"ANTHROPIC_API_KEY\"\n\n[providers.gemini]\nbase_url = \"https://generativelanguage.googleapis.com/v1beta/openai\"\napi_key_env = \"GEMINI_API_KEY\"\n\n[providers.deepseek]\nbase_url = \"https://api.deepseek.com/v1\"\napi_key_env = \"DEEPSEEK_API_KEY\"\n\n[providers.kimi]\nbase_url = \"https://api.moonshot.ai/v1\"\napi_key_env = \"MOONSHOT_API_KEY\"\n```\n\nEnv: `PI_PROVIDER`, `PI_MODEL`, `PI_MAX_TOKENS`. CLI: `pi-rs -P openai -m gpt-5 --max-tokens 8192`.\n\nBuilt-in defaults match the table in §3 so a user can do `OPENAI_API_KEY=sk-… pi-rs -P openai` with no config file.\n\n## 6. Agent loop\n\n```\nloop {\n line = read_user_input()\n push { role: user, content: line }\n loop {\n resp = client.complete(messages, tools, max_tokens)\n push resp.message\n if resp.message.tool_calls.is_empty() { print(resp.message.content); break }\n for call in resp.message.tool_calls { // sequential, in emission order\n result = dispatch_tool(call) // String, may carry an error\n push { role: tool, tool_call_id: call.id, content: result }\n }\n if turns >= max_turns { warn; break }\n }\n}\n```\n\n- **Sequential tool execution.** Even when the model emits multiple `tool_calls` in one turn, they run in order. Predictable, matches Claude Code behavior, avoids races on the filesystem.\n- **`max_tokens`.** Required by Anthropic compat, optional elsewhere. Default `8192`, override via `--max-tokens` / `PI_MAX_TOKENS`.\n- **Reasoning models.** Reasoning content is ignored for display but **still consumes output tokens** and counts against `max_tokens`. Users on o-series / extended-thinking models may need a higher cap.\n- **Context warnings.** Per-model context tracking is implemented in `context.rs`; the agent warns when approaching limits and supports `/compact` to summarize history.\n\n### 6.1 System prompt (v0)\n\nSent on every request, not stored in user-visible history; preserved across `/clear`:\n\n```\nYou are pi-rs, a CLI coding agent. You help the user edit and run code in their working directory.\n\nWorking directory: {cwd}\nOperating system: {os}\nDate: {date}\n\nPrefer using the provided tools (bash, read, write, edit) over guessing. When a tool returns an error, read the error and try a different approach. Be concise.\n```\n\n`--print-system-prompt` dumps the rendered prompt and exits 0; useful for tests and debugging.\n\n### 6.2 Stop and cancellation\n\n- `finish_reason = stop` and no tool calls → return to outer loop.\n- Ctrl-C while waiting for input (`ReadlineError::Interrupted`): discard input, return to prompt. (Note: Ctrl-C during in-flight API calls or tool runs is not yet implemented — see §14.)\n- Ctrl-D / `/exit` → exit 0.\n- API non-2xx → print body to stderr, **remove the failed user turn from history** and **restore the user's last input into the readline buffer** so they can edit and retry without retyping. Loop stays alive.\n- Unknown tool name from the model → tool message `Error: unknown tool '<name>'`; the model recovers.\n\n## 7. Tools (v0)\n\n| name | input | behavior |\n|-------------|--------------------------------------------------|-------------------------------------------------------------------------|\n| `bash` | `{ command: string, timeout_ms?: number }` | `bash -c` (non-interactive, non-login), **stdout+stderr merged** in source order, 120s default, 600s max. Streaming output supported. |\n| `read` | `{ path: string, offset?: number, limit?: number }` | UTF-8 only; `cat -n` style line numbers; 2000-line default window. |\n| `write` | `{ path: string, content: string }` | Overwrite/create; **auto-creates parent directories** (`mkdir -p` semantics); **content written as-is**, no newline coercion. Confirm if path outside CWD. |\n| `edit` | `{ path, old_string, new_string, replace_all? }` | **Exact byte match** (no whitespace normalization). Error if `old_string` not unique and not `replace_all`. On miss, return up to 3 nearest line-number candidates. |\n\nPath resolution: relative paths resolve against the **process CWD at startup**; `bash` inherits the same CWD. The agent does not `cd` between calls.\n\nCWD boundary checks (for the \"outside CWD → confirm\" rule on `write`/`edit`) operate on the **canonicalized path** — i.e. symlinks are resolved before the prefix comparison so a symlink pointing outside CWD cannot bypass the prompt.\n\nEncoding & errors:\n- `read` on a non-UTF-8 file → tool message `Error: <path> is not valid UTF-8` (no base64 fallback in v0).\n- `write` on a path whose parent dir doesn't exist → parent dirs are created automatically.\n- `edit` miss → tool error including nearest matching lines so the model can self-correct without a re-`read`.\n\nTool trait:\n\n```rust\n#[async_trait]\npub trait Tool: Send + Sync {\n fn name(&self) -> &'static str;\n fn description(&self) -> &'static str;\n fn schema(&self) -> serde_json::Value;\n async fn run(&self, ctx: ToolCtx, input: serde_json::Value) -> Result<String>;\n}\n```\n\nTools return **plain UTF-8 strings**, which the agent puts into the OpenAI `tool` message `content` field as-is. No JSON wrapping.\n\nResult cap: **100,000 chars** per tool result by default, override with `--max-tool-output`. Excess replaced with `\\n... <truncated, N more chars>`. Large enough for typical source files and build logs without ballooning context cost.\n\nConfirmation: `bash`, plus `read`/`write`/`edit` to paths outside CWD, prompt y/n unless `--yolo`.\n\n## 8. CLI\n\n```\npi-rs # interactive REPL in CWD\npi-rs -p \"fix the failing test\" # one-shot prompt\npi-rs -P anthropic -m claude-opus-4-7 # provider/model override\npi-rs --max-tokens 16384\npi-rs --max-turns 30\npi-rs --max-tool-output 200000 # bump per-tool-result cap\npi-rs --yolo # skip confirmations\npi-rs --print-system-prompt # dump rendered prompt and exit 0\npi-rs --resume <SESSION_ID> # resume a specific session\npi-rs --sessions # list saved sessions\n```\n\nREPL slash commands: `/clear` (reset history; system prompt preserved), `/exit`, `/tokens` (print last response's `usage` block), `/save` (persist current session), `/compact` (summarize history to free context).\n\nExit codes for `-p` one-shot mode:\n- `0` — model finished (`finish_reason = stop`) and produced text.\n- `1` — API error, tool fatal error, or `--max-turns` exhausted.\n- `2` — missing API key for the selected provider.\n\n## 9. Errors\n\n- API non-2xx → `eprintln!` body, keep loop alive.\n- Tool dispatch (bad JSON, unknown tool, tool runtime failure) → tool message with `Error: …`.\n- Panics → `color-eyre`-style backtrace, exit 1.\n- Missing API key for selected provider → exit 2 with a clear \"set $X\" message.\n\n## 10. Dependencies\n\nPinned, minimal:\n\n- `tokio` (rt-multi-thread, macros, process, fs, time)\n- `reqwest` (json, rustls-tls, stream — for SSE)\n- `serde`, `serde_json`\n- `clap` (derive)\n- `anyhow`\n- `async-trait`\n- `rustyline` (readline + history)\n- `toml`\n- `dirs` (config path resolution)\n- `color-eyre` (panic / error reporting)\n\nTest-only:\n- `wiremock` (mocked `/v1/chat/completions` server for integration tests; no real-API calls in CI)\n\n## 11. Compatibility caveats\n\n- **Reasoning models** (o-series, gemini-2.5-thinking, claude extended thinking) emit reasoning content differently and consume extra output tokens. The agent ignores reasoning fields for display.\n- **Strict tool schemas.** OpenAI strict mode requires `additionalProperties: false`; some compat servers reject it. Default to non-strict.\n- **Tool-call IDs.** We round-trip whatever the server sends.\n- **System messages.** All five providers accept the OpenAI `system` role on their compat endpoints.\n\n## 12. Decisions (locked)\n\n1. Default provider: **Anthropic** (model from `config.rs` const table).\n2. Streaming: **SSE streaming implemented** (was off in v0, enabled post-v0).\n3. Bash sandboxing: **confirm-prompt only**. Not safe for untrusted prompts. Documented.\n4. Tool result cap: **100,000 chars** default; configurable via `--max-tool-output`.\n5. Quirks: **fail loudly** if a provider rejects something; do not silently rewrite requests.\n6. Tool execution: **sequential**, in the order the model emitted them.\n7. `max_tokens` default: **8192**.\n8. Tool result encoding: **plain UTF-8 strings**, no JSON wrapping.\n\n## 13. Milestones\n\n- **M1** — scaffold + `LlmClient` trait + `openai_compat` impl + non-tool chat working against all five providers. ✅\n- **M2** — tool trait + four tools + tool-use loop + `--yolo` + `--max-turns`. ✅\n- **M3** — REPL polish (`rustyline`, history file, `/clear`, `/exit`, `/tokens`), `--print-system-prompt`, `wiremock`-backed integration tests, README with one-line install + per-provider snippets. ✅\n- **M4** (post-v0) — SSE streaming, session persistence, context accounting + `/compact`, MCP client integration. ✅\n\n## 14. Out-of-scope, parked for v1+\n\n- Native Anthropic adapter (prompt caching via `cache_control: ephemeral`, extended thinking).\n- Native Gemini adapter (grounding, file API).\n- Sub-agents and parallel tool execution.\n- A real permission model (sandboxing, filesystem restrictions).\n- Binary file handling (base64 read/write).\n- Hooks / skills system.\n\n> **Implemented since v0 (moved out of non-goals):**\n> - ~~SSE streaming~~ — Implemented in M4.\n> - ~~Conversation persistence and `/resume`~~ — Implemented in `session.rs`.\n> - ~~MCP client~~ — Implemented in `mcp.rs`.\n> - ~~Per-model context-window accounting and compaction~~ — Implemented in `context.rs`.\n\n## 15. Follow-up issues to file before M1\n\n- [x] Pick LICENSE (MIT or Apache-2.0) and add it to the repo.\n- [x] Track the model-IDs `const` table in `config.rs` so it can be updated independently of the RFC.\n\n## 16. Open items (next)\n\n1. **Native Anthropic adapter** — The OpenAI-compat endpoint lacks `cache_control: ephemeral` prompt caching, which significantly reduces cost for multi-turn conversations with long system prompts. A native Messages API adapter would enable this.\n2. **Sub-agents** — RFC 002 drafted. Potential use cases: parallel research, multi-file refactoring with separate context windows.\n"}, "f92bb70af98c7a081e83078b500b59ad2b15d16f": {"t": 1785763659.4444132, "n": 14726, "h": "11f0cc722fadaed7", "c": "# RFC: `pi-rs` — a multi-LLM coding agent in Rust\n\nStatus: **living document** (updated 2026-05-23 to reflect current implementation)\nAuthor: liu\nDate: 2026-05-22\n\n## 1. Goals\n\n- Single-binary CLI that runs an interactive loop with an LLM and can edit code on disk.\n- Small, readable surface. Target ~1.5k LOC for v0.\n- Mirror the Claude Code tool-use loop closely enough that prompts and behaviors transfer.\n- Support five providers out of the box: **OpenAI, Anthropic, Google Gemini, DeepSeek, Kimi**.\n\n## 2. Non-goals (v0)\n\n- TUI, streaming UI, syntax highlighting.\n- Sub-agents, hooks, skills.\n- Slash commands beyond `/clear`, `/exit`, `/tokens`, `/save`, `/compact`.\n- Sandboxing/permissioning beyond a confirm-y/n on bash + writes outside CWD.\n- Binary file handling (base64 read/write).\n- Native provider adapters (Anthropic Messages API, Gemini native).\n\n> **Note:** Several items originally listed here (MCP, conversation persistence, context accounting, SSE streaming) have since been implemented and are documented in §14.\n\n## 3. Wire format: OpenAI-compatible Chat Completions\n\nAll five providers expose an OpenAI-compatible `/v1/chat/completions` endpoint with tool-use support. We speak that protocol in the default code path.\n\n| Provider | Base URL (no trailing slash) | Auth |\n|-----------|---------------------------------------------------------------|----------------------------|\n| OpenAI | `https://api.openai.com/v1` | `Authorization: Bearer …` |\n| Anthropic | `https://api.anthropic.com/v1` | `Authorization: Bearer …` |\n| Gemini | `https://generativelanguage.googleapis.com/v1beta/openai` | `Authorization: Bearer …` |\n| DeepSeek | `https://api.deepseek.com/v1` | `Authorization: Bearer …` |\n| Kimi | `https://api.moonshot.ai/v1` | `Authorization: Bearer …` |\n\nNotes:\n- Base URLs **must not** have a trailing slash; the client appends `/chat/completions`.\n- Anthropic's compat endpoint accepts `Authorization: Bearer` only; the `x-api-key` header is for the native Messages API and is **not** sent.\n- Kimi default is `api.moonshot.ai` (international). The CN endpoint `api.moonshot.cn` is configurable.\n\nDefault model IDs live in a single `const` table in `config.rs`; the README links to provider model-list docs. Model names rot fast.\n\nTradeoff: we lose vendor-specific features (Anthropic prompt-caching headers, Gemini grounding, OpenAI strict tools, extended thinking blocks). A `LlmClient` trait keeps the door open for native adapters later.\n\n## 4. Architecture\n\nCurrent source tree (as of 2026-05-23):\n\n```\nsrc/\n main.rs # CLI entry, arg parsing, agent loop\n lib.rs # Library exports\n config.rs # provider registry, default model table, precedence\n confirm.rs # y/n confirmation prompts (--yolo bypass)\n context.rs # per-model context-window tracking and compaction\n session.rs # conversation persistence, --resume, --sessions, /save\n llm/\n mod.rs # LlmClient trait, ChatRequest/ChatResponse, ToolCall\n openai_compat.rs # OpenAI-compatible client (default)\n tools/\n mod.rs # Tool trait, registry, JSON schema helpers\n bash.rs # bash execution with streaming output\n read_file.rs # read UTF-8 files with line numbers (tool name: `read`)\n write_file.rs # write files (tool name: `write`)\n edit.rs # exact string replacement\n mcp.rs # MCP client over stdio JSON-RPC 2.0\n```\n\nThe original RFC planned `agent.rs` and `errors.rs`; the agent loop lives in `main.rs` and errors are handled via `anyhow` + `color-eyre` throughout.\n\n`LlmClient` trait:\n\n```rust\n#[async_trait]\npub(crate) trait LlmClient: Send + Sync {\n async fn complete(&self, req: ChatRequest) -> Result<ChatResponse>;\n async fn complete_stream(&self, req: ChatRequest) -> Result<EventStream>;\n}\n```\n\n`ChatRequest` / `ChatResponse` use the OpenAI shape (`role`, `content`, `tool_calls`, `tool_call_id`) and are crate-private.\n\n## 5. Configuration\n\nPrecedence: CLI flag → env var → built-in defaults.\n\n```toml\n# Configuration via environment variables:\n# PI_PROVIDER, PI_MODEL, OPENAI_API_KEY, ANTHROPIC_API_KEY, etc.\n\n[providers.anthropic]\nbase_url = \"https://api.anthropic.com/v1\"\napi_key_env = \"ANTHROPIC_API_KEY\"\n\n[providers.gemini]\nbase_url = \"https://generativelanguage.googleapis.com/v1beta/openai\"\napi_key_env = \"GEMINI_API_KEY\"\n\n[providers.deepseek]\nbase_url = \"https://api.deepseek.com/v1\"\napi_key_env = \"DEEPSEEK_API_KEY\"\n\n[providers.kimi]\nbase_url = \"https://api.moonshot.ai/v1\"\napi_key_env = \"MOONSHOT_API_KEY\"\n```\n\nEnv: `PI_PROVIDER`, `PI_MODEL`, `PI_MAX_TOKENS`. CLI: `pi-rs -P openai -m gpt-5 --max-tokens 8192`.\n\nBuilt-in defaults match the table in §3 so a user can do `OPENAI_API_KEY=sk-… pi-rs -P openai` with no config file.\n\n## 6. Agent loop\n\n```\nloop {\n line = read_user_input()\n push { role: user, content: line }\n loop {\n resp = client.complete(messages, tools, max_tokens)\n push resp.message\n if resp.message.tool_calls.is_empty() { print(resp.message.content); break }\n for call in resp.message.tool_calls { // sequential, in emission order\n result = dispatch_tool(call) // String, may carry an error\n push { role: tool, tool_call_id: call.id, content: result }\n }\n if turns >= max_turns { warn; break }\n }\n}\n```\n\n- **Sequential tool execution.** Even when the model emits multiple `tool_calls` in one turn, they run in order. Predictable, matches Claude Code behavior, avoids races on the filesystem.\n- **`max_tokens`.** Required by Anthropic compat, optional elsewhere. Default `8192`, override via `--max-tokens` / `PI_MAX_TOKENS`.\n- **Reasoning models.** Reasoning content is ignored for display but **still consumes output tokens** and counts against `max_tokens`. Users on o-series / extended-thinking models may need a higher cap.\n- **Context warnings.** Per-model context tracking is implemented in `context.rs`; the agent warns when approaching limits and supports `/compact` to summarize history.\n\n### 6.1 System prompt (v0)\n\nSent on every request, not stored in user-visible history; preserved across `/clear`:\n\n```\nYou are pi-rs, a CLI coding agent. You help the user edit and run code in their working directory.\n\nWorking directory: {cwd}\nOperating system: {os}\nDate: {date}\n\nPrefer using the provided tools (bash, read, write, edit) over guessing. When a tool returns an error, read the error and try a different approach. Be concise.\n```\n\n`--print-system-prompt` dumps the rendered prompt and exits 0; useful for tests and debugging.\n\n### 6.2 Stop and cancellation\n\n- `finish_reason = stop` and no tool calls → return to outer loop.\n- Ctrl-C while waiting for input (`ReadlineError::Interrupted`): discard input, return to prompt. (Note: Ctrl-C during in-flight API calls or tool runs is not yet implemented — see §14.)\n- Ctrl-D / `/exit` → exit 0.\n- API non-2xx → print body to stderr, **remove the failed user turn from history** and **restore the user's last input into the readline buffer** so they can edit and retry without retyping. Loop stays alive.\n- Unknown tool name from the model → tool message `Error: unknown tool '<name>'`; the model recovers.\n\n## 7. Tools (v0)\n\n| name | input | behavior |\n|-------------|--------------------------------------------------|-------------------------------------------------------------------------|\n| `bash` | `{ command: string, timeout_ms?: number }` | `bash -c` (non-interactive, non-login), **stdout+stderr merged** in source order, 120s default, 600s max. Streaming output supported. |\n| `read` | `{ path: string, offset?: number, limit?: number }` | UTF-8 only; `cat -n` style line numbers; 2000-line default window. |\n| `write` | `{ path: string, content: string }` | Overwrite/create; **auto-creates parent directories** (`mkdir -p` semantics); **content written as-is**, no newline coercion. Confirm if path outside CWD. |\n| `edit` | `{ path, old_string, new_string, replace_all? }` | **Exact byte match** (no whitespace normalization). Error if `old_string` not unique and not `replace_all`. On miss, return up to 3 nearest line-number candidates. |\n\nPath resolution: relative paths resolve against the **process CWD at startup**; `bash` inherits the same CWD. The agent does not `cd` between calls.\n\nCWD boundary checks (for the \"outside CWD → confirm\" rule on `write`/`edit`) operate on the **canonicalized path** — i.e. symlinks are resolved before the prefix comparison so a symlink pointing outside CWD cannot bypass the prompt.\n\nEncoding & errors:\n- `read` on a non-UTF-8 file → tool message `Error: <path> is not valid UTF-8` (no base64 fallback in v0).\n- `write` on a path whose parent dir doesn't exist → parent dirs are created automatically.\n- `edit` miss → tool error including nearest matching lines so the model can self-correct without a re-`read`.\n\nTool trait:\n\n```rust\n#[async_trait]\npub trait Tool: Send + Sync {\n fn name(&self) -> &'static str;\n fn description(&self) -> &'static str;\n fn schema(&self) -> serde_json::Value;\n async fn run(&self, ctx: ToolCtx, input: serde_json::Value) -> Result<String>;\n}\n```\n\nTools return **plain UTF-8 strings**, which the agent puts into the OpenAI `tool` message `content` field as-is. No JSON wrapping.\n\nResult cap: **100,000 chars** per tool result by default, override with `--max-tool-output`. Excess replaced with `\\n... <truncated, N more chars>`. Large enough for typical source files and build logs without ballooning context cost.\n\nConfirmation: `bash`, plus `read`/`write`/`edit` to paths outside CWD, prompt y/n unless `--yolo`.\n\n## 8. CLI\n\n```\npi-rs # interactive REPL in CWD\npi-rs -p \"fix the failing test\" # one-shot prompt\npi-rs -P anthropic -m claude-opus-4-7 # provider/model override\npi-rs --max-tokens 16384\npi-rs --max-turns 30\npi-rs --max-tool-output 200000 # bump per-tool-result cap\npi-rs --yolo # skip confirmations\npi-rs --print-system-prompt # dump rendered prompt and exit 0\npi-rs --resume <SESSION_ID> # resume a specific session\npi-rs --sessions # list saved sessions\n```\n\nREPL slash commands: `/clear` (reset history; system prompt preserved), `/exit`, `/tokens` (print last response's `usage` block), `/save` (persist current session), `/compact` (summarize history to free context).\n\nExit codes for `-p` one-shot mode:\n- `0` — model finished (`finish_reason = stop`) and produced text.\n- `1` — API error, tool fatal error, or `--max-turns` exhausted.\n- `2` — missing API key for the selected provider.\n\n## 9. Errors\n\n- API non-2xx → `eprintln!` body, keep loop alive.\n- Tool dispatch (bad JSON, unknown tool, tool runtime failure) → tool message with `Error: …`.\n- Panics → `color-eyre`-style backtrace, exit 1.\n- Missing API key for selected provider → exit 2 with a clear \"set $X\" message.\n\n## 10. Dependencies\n\nPinned, minimal:\n\n- `tokio` (rt-multi-thread, macros, process, fs, time)\n- `reqwest` (json, rustls-tls, stream — for SSE)\n- `serde`, `serde_json`\n- `clap` (derive)\n- `anyhow`\n- `async-trait`\n- `rustyline` (readline + history)\n- `toml`\n- `dirs` (config path resolution)\n- `color-eyre` (panic / error reporting)\n\nTest-only:\n- `wiremock` (mocked `/v1/chat/completions` server for integration tests; no real-API calls in CI)\n\n## 11. Compatibility caveats\n\n- **Reasoning models** (o-series, gemini-2.5-thinking, claude extended thinking) emit reasoning content differently and consume extra output tokens. The agent ignores reasoning fields for display.\n- **Strict tool schemas.** OpenAI strict mode requires `additionalProperties: false`; some compat servers reject it. Default to non-strict.\n- **Tool-call IDs.** We round-trip whatever the server sends.\n- **System messages.** All five providers accept the OpenAI `system` role on their compat endpoints.\n\n## 12. Decisions (locked)\n\n1. Default provider: **Anthropic** (model from `config.rs` const table).\n2. Streaming: **SSE streaming implemented** (was off in v0, enabled post-v0).\n3. Bash sandboxing: **confirm-prompt only**. Not safe for untrusted prompts. Documented.\n4. Tool result cap: **100,000 chars** default; configurable via `--max-tool-output`.\n5. Quirks: **fail loudly** if a provider rejects something; do not silently rewrite requests.\n6. Tool execution: **sequential**, in the order the model emitted them.\n7. `max_tokens` default: **8192**.\n8. Tool result encoding: **plain UTF-8 strings**, no JSON wrapping.\n\n## 13. Milestones\n\n- **M1** — scaffold + `LlmClient` trait + `openai_compat` impl + non-tool chat working against all five providers. ✅\n- **M2** — tool trait + four tools + tool-use loop + `--yolo` + `--max-turns`. ✅\n- **M3** — REPL polish (`rustyline`, history file, `/clear`, `/exit`, `/tokens`), `--print-system-prompt`, `wiremock`-backed integration tests, README with one-line install + per-provider snippets. ✅\n- **M4** (post-v0) — SSE streaming, session persistence, context accounting + `/compact`, MCP client integration. ✅\n\n## 14. Out-of-scope, parked for v1+\n\n- Native Anthropic adapter (prompt caching via `cache_control: ephemeral`, extended thinking).\n- Native Gemini adapter (grounding, file API).\n- Sub-agents and parallel tool execution.\n- A real permission model (sandboxing, filesystem restrictions).\n- Binary file handling (base64 read/write).\n- Hooks / skills system.\n\n> **Implemented since v0 (moved out of non-goals):**\n> - ~~SSE streaming~~ — Implemented in M4.\n> - ~~Conversation persistence and `/resume`~~ — Implemented in `session.rs`.\n> - ~~MCP client~~ — Implemented in `mcp.rs`.\n> - ~~Per-model context-window accounting and compaction~~ — Implemented in `context.rs`.\n\n## 15. Follow-up issues to file before M1\n\n- [x] Pick LICENSE (MIT or Apache-2.0) and add it to the repo.\n- [x] Track the model-IDs `const` table in `config.rs` so it can be updated independently of the RFC.\n\n## 16. Open items (next)\n\n1. **Native Anthropic adapter** — The OpenAI-compat endpoint lacks `cache_control: ephemeral` prompt caching, which significantly reduces cost for multi-turn conversations with long system prompts. A native Messages API adapter would enable this.\n2. **Sub-agents** — RFC 002 drafted. Potential use cases: parallel research, multi-file refactoring with separate context windows.\n"}}}} |