Full reference for all dragen features. For a quick overview, see the README.
- How It Works
- Sandbox Configuration
- Tool Registration
- Structured Output
- Parallel Execution
- Shared Context
- Event Callbacks
- Configuration
- Python API
Dragen implements the CodeAct pattern. Instead of JSON function calls, the LLM writes Python code to solve tasks:
- You register tools as Python functions (
#[tool]in Rust,@agent.toolin Python) - The agent builds a system prompt with auto-generated tool signatures and docstrings
- The LLM receives your task and writes Python code to solve it
- Code executes in the Littrs sandbox — tools are the only way to reach the outside world
- Execution output (return values + print output) is sent back to the LLM
- The LLM calls
finish(answer)when done, returning a typed result
User Task → LLM → Python Code → Sandbox → Result → LLM → ... → finish()
The agent extracts code from both ```python fenced blocks and <code>...</code> XML tags. For structured output, the LLM can also use <finish>{json}</finish> blocks to return JSON directly without code execution.
By default, Agent::new() creates a sandbox with builtins enabled (json, math, typing). For full control, configure a sandbox and pass it in with Agent::with_sandbox().
use littrs::{Sandbox, Limits};
use dragen::{Agent, AgentConfig};
let mut sandbox = Sandbox::with_builtins();
// Resource limits (uncatchable by try/except in sandbox code)
sandbox.limit(Limits {
max_instructions: Some(100_000),
max_recursion_depth: Some(50),
});
// Mount host files into the sandbox
sandbox.mount("input.json", "./data/input.json", false); // read-only
sandbox.mount("output.txt", "./results/output.txt", true); // read-write
// Register a custom module accessible via `import config`
sandbox.module("config", |m| {
m.constant("api_version", littrs::PyValue::Str("v2".into()));
m.function("get_flag", |_args| littrs::PyValue::Bool(true));
});
let agent = Agent::with_sandbox(sandbox, AgentConfig::new("moonshotai/kimi-k2.5"));import dragen
sandbox = dragen.Sandbox(builtins=True)
sandbox.limit(max_instructions=100_000, max_recursion_depth=50)
sandbox.mount("input.json", "./data/input.json")
sandbox.mount("output.txt", "./results/output.txt", writable=True)
agent = dragen.Agent("moonshotai/kimi-k2.5", sandbox=sandbox)| Feature | Description |
|---|---|
| Resource limits | Cap bytecode instructions and recursion depth per execution. Enforced at the VM level — try/except cannot suppress them |
| File mounting | Mount host files into the sandbox. Read-only by default, optionally read-write. Unmounted paths raise FileNotFoundError |
| Custom modules | Register modules with constants and functions, accessible via import |
| Built-in modules | json, math, typing available with Sandbox::with_builtins() / Sandbox(builtins=True) |
| Variable injection | Set sandbox variables from host code with sandbox.set(name, value) |
| Zero ambient capabilities | No filesystem, no network, no env vars, no dangerous imports. Tools are the only gateway |
The easiest way to register tools. Write a normal function with doc comments, and the macro generates everything:
use littrs::tool;
/// Get current weather for a city.
///
/// Args:
/// city: The city name
/// units: Temperature units (C or F)
#[tool]
fn get_weather(city: String, units: Option<String>) -> String {
format!("{}: 22°{}", city, units.unwrap_or("C".into()))
}
let mut agent = Agent::new(config);
agent.register(get_weather::Tool);The macro handles PyValue ↔ Rust type conversion automatically. Option<T> parameters become optional arguments.
@agent.tool
def get_weather(city: str, units: str = "celsius") -> dict:
"""Get current weather for a city."""
return {"city": city, "temp": 22, "units": units}Type hints (str, int, float, bool, list, dict, any) are extracted and shown in the tool documentation the LLM sees.
For cases where the macro/decorator isn't suitable:
use littrs::{ToolInfo, PyValue};
let info = ToolInfo::new("search", "Search the web")
.arg("query", "str", "The search query")
.arg_opt("limit", "int", "Max results")
.returns("list");
agent.register_tool(info, |args| {
let query = args[0].as_str().unwrap_or("");
PyValue::List(vec![PyValue::Str(format!("Result for: {}", query))])
});Override the default finish() to add custom logic:
let finish_info = ToolInfo::new("finish", "Return the final report")
.arg("report", "dict", "The structured report")
.returns("dict");
agent.register_finish(finish_info, |args| {
// Custom processing before returning
args.first().cloned().unwrap_or(PyValue::None)
});sandbox.describe() produces Python-style signatures from registered tools, embedded in the system prompt:
def get_weather(city: str, units?: str) -> str:
"""Get current weather for a city."""
def search(query: str, limit?: int) -> list:
"""Search the web."""
Pass a JSON Schema and the agent self-corrects until the output validates:
let schema = serde_json::json!({
"type": "object",
"required": ["summary", "sentiment", "confidence"],
"properties": {
"summary": {"type": "string"},
"sentiment": {"type": "string", "enum": ["positive", "negative", "neutral"]},
"confidence": {"type": "number", "minimum": 0.0, "maximum": 1.0}
}
});
let mut agent = Agent::with_model("moonshotai/kimi-k2.5").schema(schema);
let result: Analysis = agent.run("Analyze: 'This product is amazing!'").await?;If the LLM's output fails validation, the error message is sent back with the schema details, and the LLM gets another iteration to fix it.
from pydantic import BaseModel
class Analysis(BaseModel):
summary: str
sentiment: str
confidence: float
result = agent.run(
"Analyze: 'This product is amazing!'",
schema=Analysis.model_json_schema()
)
analysis = Analysis(**result)The LLM can return results in two ways:
finish()function call — the agent registers afinishtool that captures the value and signals completion<finish>XML block — the LLM writes<finish>{"key": "value"}</finish>to return JSON directly without code execution
Both paths go through schema validation if a schema is set.
Run multiple tasks concurrently with agent.map():
let agent = Agent::new(config);
agent.register(search::Tool);
let results: Vec<Result<String>> = agent.map(vec![
"Research topic A".into(),
"Research topic B".into(),
"Research topic C".into(),
]).await;
for (i, result) in results.iter().enumerate() {
match result {
Ok(value) => println!("Task {} succeeded: {}", i, value),
Err(e) => println!("Task {} failed: {}", i, e),
}
}Each task runs on a fresh clone of the agent with:
- Same configuration, tools, and schema
- Independent sandbox state and message history
- Shared
Contextaccess (if configured)
Individual task failures don't affect other tasks. Results are returned in order.
results = agent.map([
"Research topic A",
"Research topic B",
"Research topic C",
])Context is a thread-safe key-value store for passing data between agents without manual Arc<Mutex<>> management.
use dragen::{Agent, AgentConfig, Context};
let ctx = Context::new();
// Planner writes output to context
let mut planner = Agent::new(AgentConfig::new("moonshotai/kimi-k2.5"))
.to_context(&ctx, "plan");
planner.run::<PlanOutput>(&task).await?;
// Executor reads from context (auto-injected into prompt)
let mut executor = Agent::new(AgentConfig::new("moonshotai/kimi-k2.5"))
.from_context(&ctx, "plan");
executor.run::<String>("Execute the plan").await?;.to_context(&ctx, "key")— afterrun()completes, the result is serialized to JSON and stored under the given key.from_context(&ctx, "key")— before calling the LLM, the stored value is injected into the task prompt as a<context>block- Cloning a
Contextis cheap (Arc-based) — all agents share the same underlying store - Multiple agents can read from and write to the same context concurrently
let ctx = Context::new();
ctx.set("key", &value); // Store any serializable value
let val: Option<T> = ctx.get("key"); // Retrieve and deserialize
ctx.contains("key"); // Check existence
ctx.remove("key"); // Remove a key
ctx.keys(); // List all keys
ctx.clear(); // Clear everythingHook into every step of the agent loop for logging, debugging, or custom control.
| Event | Fields | Description |
|---|---|---|
IterationStart |
iteration, max_iterations |
Beginning of a new iteration |
LLMRequest |
message_count |
About to call the LLM |
LLMResponse |
content, tokens_used |
LLM responded |
Thinking |
content |
Extracted from custom thinking tags |
CodeGenerated |
code |
Code block extracted from response |
CodeExecuted |
code, output, success |
Code ran in sandbox |
ToolCall |
name, args |
A tool was invoked |
ToolResult |
name, result |
A tool returned |
Finish |
value |
Agent called finish() |
Error |
message |
An error occurred |
let agent = Agent::with_model("moonshotai/kimi-k2.5")
.on_code_generated(|e| {
if let AgentEvent::CodeGenerated { code } = e {
println!("Code: {}", code);
}
})
.on_code_executed(|e| {
if let AgentEvent::CodeExecuted { output, success, .. } = e {
println!("[{}] {}", if *success { "OK" } else { "ERR" }, output);
}
})
.on_finish(|e| {
if let AgentEvent::Finish { value } = e {
println!("Done: {:?}", value);
}
});Built-in logging to stderr:
let agent = Agent::with_model("moonshotai/kimi-k2.5").verbose(true);@agent.on("code_executed")
def on_code(event):
print(f"[{'OK' if event['success'] else 'ERR'}] {event['output']}")
@agent.on("finish")
def on_finish(event):
print(f"Done: {event['value']}")Extract structured thinking from custom XML tags:
let config = AgentConfig::new("moonshotai/kimi-k2.5")
.thinking_tag("intent");
// The agent will extract content from <intent>...</intent> blocks
// and emit Thinking eventsAgentConfig uses a builder pattern:
let config = AgentConfig::new("moonshotai/kimi-k2.5")
.max_iterations(10) // Max code execution iterations (default: 10)
.temperature(0.7) // LLM sampling temperature
.max_tokens(4096) // Max tokens per LLM response
.no_max_tokens() // Remove token limit (use model default)
.system("You are a helpful research assistant.") // Custom system description
.thinking_tag("intent"); // Extract <intent>...</intent> blocksagent = dragen.Agent(
"moonshotai/kimi-k2.5",
max_iterations=10,
temperature=0.7,
max_tokens=4096,
system="You are a helpful research assistant.",
verbose=True,
sandbox=sandbox, # Optional pre-configured sandbox
)The Python package provides the same capabilities as Rust with a Pythonic API.
sandbox = dragen.Sandbox(builtins=True) # or builtins=False for minimal sandbox
sandbox.limit(max_instructions=100_000, max_recursion_depth=50)
sandbox.mount("data.json", "./data.json")
sandbox.mount("out.txt", "./out.txt", writable=True)
sandbox.set("x", 42)
sandbox.run("x + 1") # => 43
sandbox.files() # {"out.txt": "..."}@agent.tool
def search(query: str, limit: int = 5) -> list:
"""Search the web for information.
Args:
query: The search query
limit: Maximum number of results
"""
return [{"title": f"Result for {query}"}]Type hints are extracted from the function signature. Docstrings become tool descriptions. Default values make parameters optional.
@agent.tool(finish=True)
def submit_report(title: str, content: str, sources: list) -> dict:
"""Submit the final research report."""
return {"title": title, "content": content, "sources": sources}@agent.on("code_executed")
def handle(event):
print(event) # {"code": "...", "output": "...", "success": True}results = agent.map(["Task A", "Task B", "Task C"])from pydantic import BaseModel
class Report(BaseModel):
title: str
sections: list[str]
sources: list[str]
result = agent.run("Write a report on AI agents", schema=Report.model_json_schema())
report = Report(**result)