Cersei

Tool Primitives

Low-level building blocks for agent tools — file I/O, command execution, HTTP, search, git, and text diffing.

Tool Primitives

The 34 built-in tools are thin wrappers over a set of async primitives. These primitives are the actual building blocks — file reads, process spawning, HTTP fetching, regex search, git operations, text diffing. They return structured types instead of strings, have no JSON schema overhead, and work outside the Tool trait.

Use them directly when building custom tools, data pipelines, DevOps automation, or anything that needs fine-grained control over I/O.

use cersei_tools::tool_primitives::{fs, diff, process, http, search, git};

Tool primitives are pure async functions and structs. No Tool trait, no JSON serialization, no model-facing API. They're designed for developers, not models.


At a Glance


Quick Examples

Read a file with line numbers

use cersei_tools::tool_primitives::fs;

let content = fs::read_file(Path::new("src/main.rs"), 0, 50).await?;
println!("Lines: {}, returned: {}", content.total_lines, content.lines_returned);
println!("{}", content.content); // 1-based line numbers, cat -n style

Produce a unified diff

use cersei_tools::tool_primitives::diff;

let old = "fn main() {\n    println!(\"hello\");\n}\n";
let new = "fn main() {\n    println!(\"world\");\n}\n";

let patch = diff::unified_diff(old, new, 3);
// @@ -1,3 +1,3 @@
//  fn main() {
// -    println!("hello");
// +    println!("world");
//  }

Execute a command

use cersei_tools::tool_primitives::process::{exec, ExecOptions, Shell};

let output = exec("cargo test", ExecOptions {
    cwd: Some("/my/project".into()),
    timeout: Some(Duration::from_secs(60)),
    shell: Shell::Bash,
    ..Default::default()
}).await?;

if output.exit_code == 0 {
    println!("Tests passed: {}", output.stdout);
} else {
    eprintln!("Tests failed (exit {}): {}", output.exit_code, output.stderr);
}

Search files with grep

use cersei_tools::tool_primitives::search;

let matches = search::grep("TODO", Path::new("src/"), search::GrepOptions {
    glob_filter: Some("*.rs".into()),
    max_results: Some(50),
    ..Default::default()
}).await?;

for m in &matches {
    println!("{}:{}: {}", m.file.display(), m.line_number, m.line_content);
}

Check git status

use cersei_tools::tool_primitives::git;

if git::is_repo(Path::new(".")).await {
    let status = git::status(Path::new(".")).await?;
    println!("Branch: {:?}", status.branch);
    for file in &status.files {
        println!("  {} {}", file.status, file.path);
    }
}

Fetch a URL

use cersei_tools::tool_primitives::http;

let text = http::fetch_html("https://example.com", 50_000, Default::default()).await?;
println!("{}", text); // HTML converted to readable plain text

Primitives vs Tools

Tool (model-facing)Primitive (developer-facing)
Inputserde_json::ValueTyped Rust arguments
OutputToolResult (String)Structured types (FileContent, SearchMatch, etc.)
SchemaJSON Schema for the modelNo schema
PermissionsChecked by the agent runtimeNone — caller's responsibility
AsyncYesYes
Use caseModel calls it via tool_useDeveloper calls it from Rust

The built-in tools call primitives internally:

Model → ToolUse("Read", {file_path: "src/main.rs"})
    → FileReadTool.execute(input, ctx)
        → tool_primitives::fs::read_file(path, offset, limit)  ← this is the primitive
            → Returns FileContent { content, total_lines, ... }
        → ToolResult::success(content.content)                  ← formatted for the model

Next Steps

  • API Reference — every function, struct, enum, and field
  • Cookbook — build a DiffTool, a deploy verifier, a research agent

On this page