mirror of
https://github.com/fabro-sh/fabro.git
synced 2026-09-05 08:10:39 +00:00
239 lines
8.8 KiB
Markdown
239 lines
8.8 KiB
Markdown
# agent
|
|
|
|
A programmable agentic loop for building coding agents. This crate provides the core session management, tool execution, and LLM interaction loop used to power interactive coding assistants.
|
|
|
|
## Architecture
|
|
|
|
The crate is organized around a central `Session` that drives an agentic loop:
|
|
|
|
1. **User input** is appended to a conversation `History`
|
|
2. The session builds a `Request` with system prompt, history, and tools
|
|
3. An LLM generates a response (text and/or tool calls) via `unified-llm`
|
|
4. Tool calls are executed through a `ToolRegistry` against a `Sandbox`
|
|
5. Results are recorded and the loop continues until the LLM responds with text only (natural completion), a turn limit is reached, or the session is interrupted
|
|
|
|
```
|
|
User Input
|
|
|
|
|
v
|
|
[Session::process_input]
|
|
|
|
|
v
|
|
+-------------------+
|
|
| Build Request | <-- system prompt + history + tools
|
|
+-------------------+
|
|
|
|
|
v
|
|
+-------------------+
|
|
| LLM Call | <-- via unified-llm Client
|
|
+-------------------+
|
|
|
|
|
v
|
|
+-------------------+ +-------------------+
|
|
| Tool Calls? -----+-yes-| Execute Tools |
|
|
+-------------------+ | (parallel or seq) |
|
|
| no +-------------------+
|
|
v |
|
|
[Done] +---> loop back to Build Request
|
|
```
|
|
|
|
### Key Components
|
|
|
|
- **`Session`** -- Manages the full agentic loop: LLM calls, tool execution, steering, follow-ups, interrupt handling, and event emission.
|
|
- **`AgentProfile`** (trait) -- Defines how to build system prompts, which tools to register, and what capabilities a provider supports. Ships with `AnthropicProfile`, `OpenAiProfile`, and `GeminiProfile`.
|
|
- **`Sandbox`** (trait) -- Abstracts filesystem, shell, grep, and glob operations. `LocalSandbox` provides a real implementation; the trait enables sandboxing and testing.
|
|
- **`ToolRegistry`** -- Maps tool names to definitions and async executor functions. Tools are registered per-profile.
|
|
- **`History`** -- Ordered list of `Turn` variants (`User`, `Assistant`, `ToolResults`, `System`, `Steering`) that converts to LLM messages.
|
|
- **`Emitter`** -- Broadcasts `SessionEvent`s (tool calls, text, errors, warnings) over a `tokio::sync::broadcast` channel for UI or logging.
|
|
- **`SubAgentManager`** -- Spawns child `Session`s on background tasks for delegated work, with depth limits.
|
|
- **`SessionConfig`** -- Tunable parameters: max turns, tool round limits, command timeouts, loop detection, output truncation limits, and user instructions.
|
|
|
|
## Key Types and Traits
|
|
|
|
### `Session`
|
|
|
|
The main entry point. Created with an LLM client, a provider profile, a sandbox, and a config.
|
|
|
|
### `AgentProfile`
|
|
|
|
```rust
|
|
pub trait AgentProfile: Send + Sync {
|
|
fn id(&self) -> String;
|
|
fn model(&self) -> String;
|
|
fn tool_registry(&self) -> &ToolRegistry;
|
|
fn build_system_prompt(
|
|
&self,
|
|
env: &dyn Sandbox,
|
|
env_context: &EnvContext,
|
|
project_docs: &[String],
|
|
user_instructions: Option<&str>,
|
|
) -> String;
|
|
// ... default methods for tools(), knowledge_cutoff(), context_window_size()
|
|
}
|
|
```
|
|
|
|
Built-in profiles:
|
|
- **`AnthropicProfile`** -- 200K context, extended thinking beta headers, and Anthropic task tools
|
|
- **`OpenAiProfile`** -- 128K context, reasoning effort support, and `apply_patch` (Codex apply_patch format)
|
|
- **`GeminiProfile`** -- 1M context, safety settings, plus `read_many_files` and `list_dir`
|
|
|
|
All profiles include the common file, shell, search, and `web_fetch` tools.
|
|
`web_search` is included only when a Brave Search API key is supplied while
|
|
building the profile.
|
|
|
|
### `Sandbox`
|
|
|
|
```rust
|
|
pub trait Sandbox: Send + Sync {
|
|
async fn read_file_bytes(&self, path: &str) -> Result<Vec<u8>, String>;
|
|
async fn read_file_text(&self, path: &str) -> Result<String, String>;
|
|
async fn read_file(&self, path: &str, offset: Option<usize>, limit: Option<usize>) -> Result<String, String>; // line-numbered display
|
|
async fn write_file(&self, path: &str, content: &str) -> Result<(), String>;
|
|
async fn exec_command(&self, command: &str, timeout_ms: u64, ...) -> Result<ExecResult, String>;
|
|
async fn grep(&self, pattern: &str, path: &str, options: &GrepOptions) -> Result<Vec<String>, String>;
|
|
async fn walk_files(&self, base: &str, relative_start: &str, options: &WalkOptions) -> Result<Vec<SandboxFile>, String>;
|
|
async fn glob(&self, pattern: &str, path: Option<&str>) -> Result<Vec<String>, String>;
|
|
// ... plus delete_file, file_exists, list_directory, initialize, cleanup, platform info
|
|
}
|
|
```
|
|
|
|
`LocalSandbox` is the real implementation with env-var filtering (strips secrets), process group management, and ripgrep/grep fallback.
|
|
|
|
### `SessionConfig`
|
|
|
|
```rust
|
|
pub struct SessionConfig {
|
|
pub default_command_timeout_ms: u64, // default: 10s
|
|
pub max_command_timeout_ms: u64, // default: 600s
|
|
pub enable_loop_detection: bool, // default: true
|
|
pub loop_detection_window: usize, // default: 10
|
|
pub max_subagent_depth: usize, // default: 1
|
|
pub user_instructions: Option<String>,
|
|
pub reasoning_effort: Option<String>,
|
|
// ... plus tool_output_limits, tool_line_limits, git_root
|
|
}
|
|
```
|
|
|
|
## Usage
|
|
|
|
```rust
|
|
use agent::{
|
|
AnthropicProfile, LocalSandbox, Session, SessionConfig,
|
|
};
|
|
use std::path::PathBuf;
|
|
use std::sync::Arc;
|
|
use unified_llm::client::Client;
|
|
|
|
// 1. Create an LLM client (via unified-llm)
|
|
let client: Client = /* configure unified-llm client */;
|
|
|
|
// 2. Choose a provider profile
|
|
let profile = Arc::new(AnthropicProfile::new("claude-sonnet-4-20250514"));
|
|
|
|
// 3. Create a sandbox
|
|
let env = Arc::new(LocalSandbox::new(
|
|
PathBuf::from("/path/to/project"),
|
|
));
|
|
|
|
// 4. Configure the session
|
|
let config = SessionConfig {
|
|
enable_loop_detection: true,
|
|
user_instructions: Some("Always write tests first".into()),
|
|
..SessionConfig::default()
|
|
};
|
|
|
|
// 5. Create and initialize the session
|
|
let mut session = Session::new(client, profile, env, config, None);
|
|
session.initialize().await?;
|
|
|
|
// 6. Subscribe to events (for UI rendering)
|
|
let mut rx = session.subscribe();
|
|
tokio::spawn(async move {
|
|
while let Ok(event) = rx.recv().await {
|
|
// Handle SessionEvent: tool calls, text, errors, etc.
|
|
}
|
|
});
|
|
|
|
// 7. Process user input
|
|
session.process_input("Fix the failing test in src/lib.rs").await?;
|
|
```
|
|
|
|
### Steering and Follow-ups
|
|
|
|
Inject guidance mid-conversation or queue follow-up messages:
|
|
|
|
```rust
|
|
// Inject a steering message before the next LLM call
|
|
session.steer("Focus on the root cause, not symptoms".into());
|
|
|
|
// Queue a follow-up that runs after the current input completes
|
|
session.follow_up("Now run the test suite to verify".into());
|
|
```
|
|
|
|
### Interrupt
|
|
|
|
Cancel a running session from another thread:
|
|
|
|
```rust
|
|
let cancel_token = session.cancel_token();
|
|
// From another task:
|
|
cancel_token.cancel();
|
|
```
|
|
|
|
### Custom Tools
|
|
|
|
Register additional tools via the profile's `ToolRegistry`:
|
|
|
|
```rust
|
|
use agent::tool_registry::{RegisteredTool, ToolExecutor};
|
|
use unified_llm::types::ToolDefinition;
|
|
use std::sync::Arc;
|
|
|
|
let custom_tool = RegisteredTool {
|
|
definition: ToolDefinition {
|
|
name: "my_tool".into(),
|
|
description: "Does something useful".into(),
|
|
parameters: serde_json::json!({
|
|
"type": "object",
|
|
"properties": {
|
|
"input": {"type": "string"}
|
|
},
|
|
"required": ["input"]
|
|
}),
|
|
},
|
|
executor: Arc::new(|args, env| {
|
|
Box::pin(async move {
|
|
let input = args["input"].as_str().unwrap_or("");
|
|
Ok(format!("Processed: {input}"))
|
|
})
|
|
}),
|
|
};
|
|
|
|
// Register on a mutable profile before creating the session
|
|
profile.tool_registry_mut().register(custom_tool);
|
|
```
|
|
|
|
### Subagents
|
|
|
|
Spawn child sessions for delegated tasks:
|
|
|
|
```rust
|
|
use agent::subagent::SubAgentManager;
|
|
|
|
let mut profile = AnthropicProfile::new("claude-sonnet-4-20250514");
|
|
let manager = Arc::new(tokio::sync::Mutex::new(SubAgentManager::new(3)));
|
|
let factory = Arc::new(|| { /* create a new Session */ });
|
|
|
|
// Registers spawn_agent, send_input, wait, close_agent tools
|
|
profile.register_subagent_tools(manager, factory, 0);
|
|
```
|
|
|
|
## Safety Features
|
|
|
|
- **Loop detection** -- Detects repeating tool call patterns (period 1, 2, or 3) and injects a steering warning
|
|
- **Context window monitoring** -- Emits `Warning` events (kind `"context_window"`) when estimated usage exceeds 80%
|
|
- **Tool argument validation** -- Validates arguments against JSON Schema before execution
|
|
- **Tool output truncation** -- Per-tool character and line limits with head/tail or tail-only truncation modes
|
|
- **Environment variable filtering** -- `LocalSandbox` strips secrets (`*_API_KEY`, `*_SECRET`, `*_TOKEN`, `*_PASSWORD`, `*_CREDENTIAL`) from subprocess environments
|
|
- **Command timeouts** -- Configurable per-command with process group cleanup (SIGTERM then SIGKILL)
|
|
- **Project doc discovery** -- Automatically discovers `AGENTS.md`, `CLAUDE.md`, `GEMINI.md`, or `.codex/instructions.md` based on provider, with a 32KB budget
|