Fabro carried its own Sandbox trait long after every implementation became a thin layer over the sandbox driver: one production type implemented it, a delegation macro forwarded it, and each consumer crate kept hand-written fakes of its thirty methods for tests. The trait existed to be mocked, and the mocks pinned behavior that no provider had — canned walk listings that ignored the traversal root, opaque provider paths, activation failures with no lifecycle behind them. There is now one sandbox type. RunSandbox keeps fabro's semantics — path resolution against the run's working directory, the Bash exec policy, git setup and push, credential refresh — as inherent methods over the driver's exec, filesystem, search, and git facets, and every consumer takes Arc<RunSandbox>. The directory, grep, and walk types are the driver's own, re-exported from fabro-sandbox. The exec policy reports the provider's measured duration rather than its own clock. Tests script a sandbox through fabro-sandbox's MockSandbox: a struct of fields (seeded files, the result every command returns, the platform, a runtime directory) that hands out a RunSandbox over the driver's scripted doubles and reads back what the code did — commands, timeouts, environment, term stops, writes, deletes, existence probes. The hand-written fakes in fabro-agent, fabro-acp, fabro-hooks, fabro-workflow, and fabro-server are gone; the one wrapper a git integration test still needs sits at the driver level, hiding a path from a real Host sandbox. The refresh-ahead loop takes the refresh as a closure so its schedule is tested without a sandbox at all. The driver pin moves to the testing-crate stack head, which gained the double behavior these ports needed: retention caps on scripted output, canned walks narrowed to the requested base, upload and download on the memory filesystem, and recorders for deletes, existence probes, and term stops. One test that modelled a provider handing back opaque object paths from a walk is removed: the driver contract has no such thing. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
9.1 KiB
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:
- User input is appended to a conversation
History - The session builds a
Requestwith system prompt, history, and tools - An LLM generates a response (text and/or tool calls) via
unified-llm - Tool calls are executed through a
ToolRegistryagainst aRunSandbox - 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 withAnthropicProfile,OpenAiProfile, andGeminiProfile.RunSandbox-- Filesystem, shell, grep, and glob operations over a sandbox-driver sandbox: the local filesystem throughlocal_sandbox, or a Docker or Daytona provider throughprovider_sandbox. Tests script one withfabro_sandbox::test_support::MockSandbox.ToolRegistry-- Maps tool names to definitions and async executor functions. Tools are registered per-profile.History-- Ordered list ofTurnvariants (User,Assistant,ToolResults,System,Steering) that converts to LLM messages.Emitter-- BroadcastsSessionEvents (tool calls, text, errors, warnings) over atokio::sync::broadcastchannel for UI or logging.SubAgentManager-- Spawns childSessions 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
pub trait AgentProfile: Send + Sync {
fn id(&self) -> String;
fn model(&self) -> String;
fn tool_registry(&self) -> &ToolRegistry;
fn build_system_prompt(
&self,
env: &RunSandbox,
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 toolsOpenAiProfile-- 128K context, reasoning effort support, andapply_patch(Codex apply_patch format)GeminiProfile-- 1M context, safety settings, plusread_many_filesandlist_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.
RunSandbox
impl RunSandbox {
pub async fn read_file_bytes(&self, path: &str) -> Result<Vec<u8>>;
pub async fn read_file_text(&self, path: &str) -> Result<String>;
pub async fn read_file(&self, path: &str, offset: Option<usize>, limit: Option<usize>) -> Result<String>; // line-numbered display
pub async fn write_file(&self, path: &str, content: &str) -> Result<()>;
pub async fn exec_command(&self, command: &str, timeout_ms: u64, ...) -> Result<ExecResult>;
pub async fn grep(&self, pattern: &str, path: &str, options: &GrepOptions) -> Result<Vec<GrepMatch>>;
pub async fn walk_files(&self, base: &str, relative_start: &str, options: &WalkOptions) -> Result<Vec<SandboxFile>>;
pub async fn glob(&self, pattern: &str, path: Option<&str>) -> Result<Vec<String>>;
// ... plus delete_file, file_exists, list_directory, initialize, cleanup, platform info
}
RunSandbox is one concrete type over a sandbox-driver sandbox. Paths resolve against the run's working directory; commands run as Bash under fabro's timeout and stop policy, with credential-shaped variables filtered when the sandbox is the worker host itself.
SessionConfig
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
use agent::{
AnthropicProfile, Session, SessionConfig, local_sandbox,
};
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(local_sandbox(PathBuf::from("/path/to/project")).await?);
// 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:
// 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:
let cancel_token = session.cancel_token();
// From another task:
cancel_token.cancel();
Custom Tools
Register additional tools via the profile's ToolRegistry:
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:
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
Warningevents (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 -- the local sandbox 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.mdbased on provider, with a 32KB budget