Load agent markdown files into a typed catalog with runtime defaults and permission evaluation.
The agent file format mirrors OpenCode's schema - similar enough that many files are drop-in compatible, but not identical.
Use [AgentLoader] to read agent files from a directory, then store them in
an [AgentCatalog] for lookup by name:
use reloaded_code_agents::{AgentCatalog, AgentLoader};
let loader = AgentLoader::new();
let mut catalog = AgentCatalog::new();
loader.add_directory(&mut catalog, "/home/user/.opencode")?;
for agent in catalog.iter() {
println!("{}: {}", agent.name, agent.description);
}
# Ok::<(), reloaded_code_agents::AgentLoadError>(())Agent files are markdown with YAML frontmatter.
The format is similar to OpenCode's agent schema; so fields like mode,
model and permissions should be familiar.
---
name: code-searcher
mode: subagent
description: Searches codebases to find relevant files and extracts content
model: synthetic/hf:moonshotai/Kimi-K2.5
permission:
read: allow
grep: allow
task: deny
tool_settings:
read:
line_numbers: false
grep:
line_numbers: false
---
You are a code search assistant. Use grep to find relevant files and code patterns,
then read the matching files to extract and summarize the content.Required:
description- What this agent does
Optional:
name- Agent identifier (defaults to filename)mode- Agent behaviour modemodel- LLM provider/model specificationpermission- Tool access permissionstool_settings- Per-tool configurationtemperature,top_p- Sampling parameters
all(default) - Both primary and subagent capabilitiesprimary- Top-level agent, can delegate to subagentssubagent- Can only be invoked by other agents viatasktool
Specify which LLM to use.
Format: provider/model or synthetic/hf:model-id.
Examples:
openai/gpt-5.3-codexsynthetic/hf:moonshotai/Kimi-K2.5fireworks/accounts/fireworks/routers/kimi-k2p5-turbo
Tip: Use the reloaded-code-models-dev crate for models.dev support.
You can find examples using it in main repo.
Map of tool names to allow or deny. Unlisted tools are denied.
permission:
read: allow
write: deny
bash: allow
task: allow # Required to delegate to subagentsSeveral tools support pattern-based rules instead of a simple allow/deny.
Evaluation uses last-match-wins: the final matching rule takes effect.
| Tool(s) | Pattern matches against | Supports patterns |
|---|---|---|
| read, write, edit, glob, grep | File path (relative or absolute) | yes |
| bash | Command string | yes |
| task | Target agent name | yes |
| webfetch, todoread, todowrite | - | no (allow/deny only) |
File tools - patterns match against the path as given. Absolute paths
start with / or a drive letter like C:/. Relative paths have no such
prefix. ** matches any file at any depth, relative to the workspace root (catch-all).
* matches files in the workspace root only. /** matches any file on the
system, including other drives on Windows.
permission:
read:
"src/**": allow
"secrets/**": deny
"**": allowBash - patterns match against the command string:
permission:
bash:
"rm *": deny
"curl *": deny
"*": allowTask delegation - patterns match against the target agent name:
permission:
task:
"reader-*": allow
"*": denyNote: task is special - when omitted entirely (not just set to a pattern),
it allows delegation to all callable subagents for OpenCode compatibility.
To disable delegation, explicitly set task: deny.
Configure per-tool behaviour via tool_settings:
tool_settings:
read:
line_numbers: true # default: true
limit: 2000 # default: 2000
max_line_length: 2000 # default: 2000
grep:
line_numbers: true # default: true
limit: 100 # default: 100
max_line_length: 2000 # default: 2000
glob:
limit: 1000 # default: 1000
bash:
timeout_ms: 120000 # default: 120000 (2 minutes)
max_timeout_ms: 600000 # default: 600000 (10 minutes)
webfetch:
timeout_ms: 30000 # default: 30000 (30 seconds)
max_timeout_ms: 600000 # default: 600000 (10 minutes)
max_response_size: 5242880 # default: 5242880 (5 MiB in bytes)Setting reference:
| Tool | Setting | Type | Default | Min | Description |
|---|---|---|---|---|---|
| read | line_numbers |
bool | true |
- | Show line numbers in output |
| read | limit |
usize | 2000 |
1 | Max lines per file read |
| read | max_line_length |
usize | 2000 |
4 | Max characters per line (truncates longer lines) |
| grep | line_numbers |
bool | true |
- | Show line numbers in output |
| grep | limit |
usize | 100 |
1 | Max matches returned |
| grep | max_line_length |
usize | 2000 |
4 | Max characters per match line |
| glob | limit |
usize | 1000 |
1 | Max files returned |
| bash | timeout_ms |
usize | 120000 |
1000 | Default command timeout in milliseconds |
| bash | max_timeout_ms |
usize | 600000 |
* | Maximum timeout LLM can request (must be >= timeout_ms) |
| webfetch | timeout_ms |
usize | 30000 |
1000 | Fetch timeout in milliseconds |
| webfetch | max_timeout_ms |
usize | 600000 |
* | Maximum timeout LLM can request (must be >= timeout_ms) |
| webfetch | max_response_size |
usize | 5242880 |
1 | Max response body size in bytes |
Output format:
With line numbers (default true):
L1: fn main() {
L2: println!("Hello");
L3: }
Without line numbers (false):
fn main() {
println!("Hello");
}
When to use:
-
line_numbers: true(default) - When the agent needs to reference specific lines, use theedittool, or do code review. Most agents should use this. -
line_numbers: false- For read-only agents that summarize, analyse, or answer questions without citing line numbers. Saves tokens and produces cleaner output. -
read.limit- Maximum number of lines returned when the LLM doesn't specify alimitin its tool call. Lines beyond this are not read. -
read.max_line_length- Maximum characters per line in read output. Longer lines are truncated with...appended. -
grep.limit- Maximum number of matches returned when the LLM doesn't specify alimit. Extra matches are dropped. -
grep.max_line_length- Maximum characters per line in grep output. Longer lines are truncated with...appended. -
glob.limit- Maximum number of file paths returned. Results beyond this are dropped andtruncated: trueis set. -
bash.timeout_ms- Maximum time a shell command may run before being killed, in milliseconds. Used when the LLM doesn't specifytimeout_ms. -
bash.max_timeout_ms- Maximum timeout the LLM is allowed to request via thetimeout_msparameter. Must be greater than or equal totimeout_ms. -
webfetch.timeout_ms- Maximum time to wait for a response from a URL, in milliseconds. Used when the LLM doesn't specifytimeout_ms. -
webfetch.max_timeout_ms- Maximum timeout the LLM is allowed to request via thetimeout_msparameter. Must be greater than or equal totimeout_ms. -
webfetch.max_response_size- Maximum response body size in bytes. Responses larger than this are rejected. Default is 5242880 bytes (5 MiB).
Framework adapters (like reloaded-code-serdesai) use [AgentRuntime] to
build runnable agents. An AgentRuntime bundles your loaded agents with default
settings and available tools:
use reloaded_code_agents::{
AgentCatalog, AgentDefaults, AgentLoader, AgentRuntimeBuilder,
};
let loader = AgentLoader::new();
let mut catalog = AgentCatalog::new();
loader.add_directory(&mut catalog, "/home/user/.opencode")?;
let runtime = AgentRuntimeBuilder::new()
.catalog(catalog)
.defaults(AgentDefaults::with_model("openai/gpt-5.4"))
// .max_task_depth(5) // optional; defaults to 3 Task hops
// .tools(my_custom_tools) // optional; defaults to read/write/edit/glob/grep/bash/webfetch/todoread/todowrite/task
.build();
// Pass `runtime` to your framework adapter to build agents by name
# Ok::<(), reloaded_code_agents::AgentLoadError>(())Embedders can register custom tools that integrate with the agent runtime,
permission system, and system prompt builder. Implement ToolFactory and
register it on the builder:
use reloaded_code_agents::{
AgentRuntimeBuilder, ToolFactory, ToolBuildContext, ToolCatalogEntry,
ToolCatalogKind,
};
use reloaded_code_core::context::ToolPrompt;
use std::any::Any;
struct WebSearchFactory;
impl ToolFactory for WebSearchFactory {
fn name(&self) -> &'static str { "web_search" }
fn create(&self, _ctx: &ToolBuildContext) -> Box<dyn Any + Send + Sync> {
// SerdesAI users: double-box the tool for the type-erasure boundary.
// let tool: Box<dyn serdes_ai::Tool<()>> = Box::new(WebSearchTool::new());
// Box::new(tool)
todo!("implement and return tool")
}
fn prompt(&self) -> Option<ToolPrompt> {
Some(ToolPrompt::Static("Use web_search to find information online."))
}
}
let tools = vec![
// ...existing tools...
ToolCatalogEntry::new("web_search", ToolCatalogKind::Custom),
];
let runtime = AgentRuntimeBuilder::new()
.custom_tool(WebSearchFactory)
.tools(tools)
.build()?;ToolFactory- Trait for creating custom tools at agent build time. Returns a type-erasedBox<dyn Any + Send + Sync>for cross-crate compatibility.ToolBuildContext- Context passed tocreate(), providing workspace root and permission ruleset.CustomToolRegistry- Maps tool names toToolFactoryinstances. The builder'scustom_tool()method callsinsert()internally; the build step callsget()to resolve factories.ErasedToolRegistry-Arc<CustomToolRegistry>wrapper for cheap cloning inAgentRuntime(which derivesClone).custom_tool()- Builder method following the consuming builder pattern (mut self). Chain calls:.custom_tool(FooFactory).custom_tool(BarFactory).
Because reloaded-code-agents does not depend on serdes_ai, the
ToolFactory::create() return type is type-erased. SerdesAI users must
double-box their tool:
fn create(&self, ctx: &ToolBuildContext) -> Box<dyn Any + Send + Sync> {
let tool: Box<dyn serdes_ai::Tool<()>> = Box::new(MyTool::new(ctx));
Box::new(tool) // outer Box satisfies the type-erasure boundary
}The SerdesAI build layer downcasts the inner Box<dyn Tool<()>> and
attaches it to the agent builder. A failed downcast produces
AgentBuildError::CustomToolDowncastFailed.
Override prompt() on your ToolFactory to include guidance in the agent's
system prompt. Return None (the default) to skip prompt guidance for tools
that don't need it.
| Error variant | When it occurs |
|---|---|
UnknownCustomTool |
A ToolCatalogKind::Custom entry has no matching registered factory |
CustomToolDowncastFailed |
A factory's create() returned a value that cannot be downcast to Box<dyn Tool<()>> |
The agent file format mirrors OpenCode's. Many files are drop-in compatible, but there are differences:
This library denies tools unless explicitly allowed. OpenCode uses
default-allow. This is because reloaded-code targets
automation/servers, where determinism is more valuable.
For default-allow behaviour, open a PR.
File tool patterns (read, write, edit, glob, grep) match against
the path as given, supporting both absolute (/home/user/..., C:/...)
and relative paths. OpenCode instead provides external_directory for
granting access outside the workspace. This library omits that in favour
of more granular pattern control.
This library does not provide interactive UX extensions (for example, TUI approval flows). To avoid false expectations, settings that require interaction are rejected, while settings with no runtime effect are accepted and ignored:
permission.task: ask- Rejected with a schema validation error (allow/denyonly), becauseaskis an interactive approval mode in OpenCode.hidden- Accepted for compatibility, but ignored at runtime.
For the internal architecture, see ARCHITECTURE.md.