Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 11 additions & 7 deletions docs/src/examples.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,20 @@ Runnable examples live in the repository under each crate's `examples/` director

## SerdesAI Integration

| Example | Description | Run |
| ------------------------- | --------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------- |
| [serdesai-basic] | Minimal agent with file tools, shell execution, web fetch, and streaming output. | `cargo run --example serdesai-basic -p reloaded-code-serdesai` |
| [serdesai-agents] | Load markdown agents through `AgentLoader`, build a named agent via `AgentBuildContext` using the models.dev catalog. | `cargo run --example serdesai-agents -p reloaded-code-serdesai` |
| [serdesai-task] | Orchestrator delegates a read-only task to a reader sub-agent, with streamed transcript and tool-call logging. | `cargo run --example serdesai-task -p reloaded-code-serdesai` |
| [serdesai-sandboxed] | Agent with `AllowedPathResolver` - file operations restricted to specific directories. | `cargo run --example serdesai-sandboxed -p reloaded-code-serdesai` |
| [serdesai-sandboxed-bash] | Sandboxed shell execution with a bubblewrap `public_bot` profile (Linux only). | `cargo run --example serdesai-sandboxed-bash --features linux-bubblewrap -p reloaded-code-serdesai` |
| Example | Description | Run |
| --------------------------------- | --------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------- |
| [serdesai-basic] | Minimal agent with file tools, shell execution, web fetch, and streaming output. | `cargo run --example serdesai-basic -p reloaded-code-serdesai` |
| [serdesai-agents] | Load markdown agents through `AgentLoader`, build a named agent via `AgentBuildContext` using the models.dev catalog. | `cargo run --example serdesai-agents -p reloaded-code-serdesai` |
| [serdesai-custom-tool] | Register a portable custom tool, build a markdown agent with models.dev, and run it through SerdesAI. | `cargo run --example serdesai-custom-tool -p reloaded-code-serdesai` |
| [serdesai-custom-tool-standalone] | Portable custom tool attached directly to a SerdesAI `AgentBuilder` (no agent runtime). | `cargo run --example serdesai-custom-tool-standalone -p reloaded-code-serdesai` |
| [serdesai-task] | Orchestrator delegates a read-only task to a reader sub-agent, with streamed transcript and tool-call logging. | `cargo run --example serdesai-task -p reloaded-code-serdesai` |
| [serdesai-sandboxed] | Agent with `AllowedPathResolver` - file operations restricted to specific directories. | `cargo run --example serdesai-sandboxed -p reloaded-code-serdesai` |
| [serdesai-sandboxed-bash] | Sandboxed shell execution with a bubblewrap `public_bot` profile (Linux only). | `cargo run --example serdesai-sandboxed-bash --features linux-bubblewrap -p reloaded-code-serdesai` |

[serdesai-basic]: https://github.com/Reloaded-Project/ReloadedCode/blob/main/src/reloaded-code-serdesai/examples/serdesai-basic.rs
[serdesai-agents]: https://github.com/Reloaded-Project/ReloadedCode/blob/main/src/reloaded-code-serdesai/examples/serdesai-agents.rs
[serdesai-custom-tool]: https://github.com/Reloaded-Project/ReloadedCode/blob/main/src/reloaded-code-serdesai/examples/serdesai-custom-tool.rs
[serdesai-custom-tool-standalone]: https://github.com/Reloaded-Project/ReloadedCode/blob/main/src/reloaded-code-serdesai/examples/serdesai-custom-tool-standalone.rs
[serdesai-task]: https://github.com/Reloaded-Project/ReloadedCode/blob/main/src/reloaded-code-serdesai/examples/serdesai-task.rs
[serdesai-sandboxed]: https://github.com/Reloaded-Project/ReloadedCode/blob/main/src/reloaded-code-serdesai/examples/serdesai-sandboxed.rs
[serdesai-sandboxed-bash]: https://github.com/Reloaded-Project/ReloadedCode/blob/main/src/reloaded-code-serdesai/examples/serdesai-sandboxed-bash.rs
Expand Down
15 changes: 9 additions & 6 deletions docs/src/getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -157,22 +157,25 @@ a Rust project and an LLM API key (e.g. `OPENAI_API_KEY`).

## Custom tools

Implement [`ToolContext`] and [`ToolFactory`], then register with the builder:
Implement a portable [`CustomTool`] plus [`ToolContext`] and [`ToolFactory`],
then register with the builder:

```rust
struct MyFactory;
struct MyTool;
impl ToolContext for MyFactory {
fn name(&self) -> &'static str { "my_tool" }
fn context(&self) -> ToolPrompt {
ToolPrompt::Static("Guidance for using my_tool.")
}
}
impl ToolFactory for MyFactory {
fn create(&self, _ctx: &ToolBuildContext) -> Box<dyn Any + Send + Sync> {
todo!("return your tool")
fn create(&self, _ctx: &ToolBuildContext) -> ToolResult<Arc<dyn CustomTool>> {
Ok(Arc::new(MyTool))
}
}

// MyTool implements CustomTool once; SerdesAI and other adapters wrap it.
let runtime = AgentRuntimeBuilder::new()
.custom_tool(MyFactory)
.tools(vec![
Expand All @@ -184,9 +187,6 @@ let runtime = AgentRuntimeBuilder::new()
See [Tools > Custom tools](tools.md#custom-tools) for annotated details
and error handling.

[`ToolContext`]: https://docs.rs/reloaded-code-core/latest/reloaded_code_core/trait.ToolContext.html
[`ToolFactory`]: https://docs.rs/reloaded-code-core/latest/reloaded_code_core/trait.ToolFactory.html

## Credential management

`CredentialResolver` resolves API keys by name (e.g. `"OPENAI_API_KEY"`) -
Expand Down Expand Up @@ -271,3 +271,6 @@ reloaded-code-core = { version = "0.2", default-features = false, features = ["b
[SerdesAI]: https://crates.io/crates/serdes-ai
[OpenCode]: https://opencode.ai/
[bubblewrap]: https://github.com/containers/bubblewrap
[`ToolContext`]: https://docs.rs/reloaded-code-core/latest/reloaded_code_core/trait.ToolContext.html
[`CustomTool`]: https://docs.rs/reloaded-code-core/latest/reloaded_code_core/trait.CustomTool.html
[`ToolFactory`]: https://docs.rs/reloaded-code-core/latest/reloaded_code_core/trait.ToolFactory.html
25 changes: 24 additions & 1 deletion docs/src/guides/custom-framework.md
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,28 @@ The builder includes guidance only for tracked tools. Cross-tool references
(e.g. "prefer grep over read for searching") are included only when both tools
are present.

## Portable custom tools

Custom tools implement `CustomTool` in `reloaded-code-core`, not a framework
trait. Your adapter only needs a thin wrapper that:

1. Converts `CustomToolDefinition` into your framework's tool definition type
2. Forwards JSON arguments to `CustomTool::call(ToolRunContext, args)`
3. Converts the returned `ToolOutput` into your framework's tool return type

That means the same custom tool implementation can be registered once through
`ToolFactory` and reused by SerdesAI or any other Rust LLM framework adapter.

!!! tip "Adapter example"

See SerdesAI's
[`CustomToolAdapter`](https://github.com/Reloaded-Project/ReloadedCode/blob/main/src/reloaded-code-serdesai/src/tools/custom.rs)
for a concrete adapter implementation, plus
[`serdesai-custom-tool`](../examples.md#serdesai-integration) for a runnable
portable custom tool example using the agent runtime, or
[`serdesai-custom-tool-standalone`](../examples.md#serdesai-integration) for a
direct `AgentBuilder` example without the runtime.

## Step 3: Choose a path resolver

| Resolver | Use when |
Expand Down Expand Up @@ -156,7 +178,8 @@ let glob = AllowedGlobResolver::new(["/workspace/project"])?
| `read_todos`, `write_todos` | Shared todo state |
| `SystemPromptBuilder` | Context-aware system prompt generation |
| `ToolContext` trait | Tool metadata interface for prompt building |
| `ToolFactory` / `CustomToolRegistry` | Framework-agnostic custom tool creation and lookup |
| `CustomTool` | Portable custom tool definition and execution |
| `ToolFactory` / `CustomToolRegistry` | Portable custom tool creation and lookup |
| `ToolCatalogEntry` / `ToolCatalogKind` | Standard/custom tool catalog for adapters |
| `PathResolver` trait | Path security boundary |
| `AllowedPathResolver` | Directory-based sandbox |
Expand Down
48 changes: 40 additions & 8 deletions docs/src/tools.md
Original file line number Diff line number Diff line change
Expand Up @@ -127,12 +127,16 @@ Custom tools let embedders add non-built-in tools to an agent runtime.
```rust
use reloaded_code_agents::AgentRuntimeBuilder;
use reloaded_code_core::{
ToolBuildContext, ToolCatalogEntry, ToolCatalogKind, ToolContext, ToolFactory,
CustomTool, CustomToolDefinition, CustomToolFuture, ToolBuildContext,
ToolCatalogEntry, ToolCatalogKind, ToolContext, ToolFactory, ToolOutput,
ToolResult, ToolRunContext,
};
use reloaded_code_core::context::ToolPrompt;
use std::any::Any;
use serde_json::json;
use std::sync::Arc;

struct WebSearchFactory;
struct WebSearchTool;

// Name + prompt guidance.
impl ToolContext for WebSearchFactory {
Expand All @@ -142,11 +146,37 @@ impl ToolContext for WebSearchFactory {
}
}

// Build framework-specific tool instance.
// Build portable tool instance.
impl ToolFactory for WebSearchFactory {
fn create(&self, _ctx: &ToolBuildContext) -> Box<dyn Any + Send + Sync> {
// SerdesAI: return Box::new(Box<dyn serdes_ai::Tool<()>>).
todo!("return your tool")
fn create(&self, _ctx: &ToolBuildContext) -> ToolResult<Arc<dyn CustomTool>> {
Ok(Arc::new(WebSearchTool))
}
}

impl ToolContext for WebSearchTool {
fn name(&self) -> &'static str { "web_search" }
fn context(&self) -> ToolPrompt {
ToolPrompt::Static("Use web_search to find information online.")
}
}

impl CustomTool for WebSearchTool {
fn definition(&self) -> CustomToolDefinition {
CustomToolDefinition::new("web_search", "Find information online")
.with_parameters(json!({
"type": "object",
"properties": {
"query": { "type": "string", "description": "Search query" }
},
"required": ["query"]
}))
}

fn call<'a>(&'a self, _ctx: ToolRunContext<'a>, args: serde_json::Value) -> CustomToolFuture<'a> {
Box::pin(async move {
let query = args["query"].as_str().unwrap_or_default();
Ok(ToolOutput::new(format!("searched for {query}")))
})
}
}

Expand All @@ -163,10 +193,12 @@ let runtime = AgentRuntimeBuilder::new()
Rules:

- Factory name must match catalog entry name.
- Custom tool definition name must match catalog entry name.
- `ToolContext::context()` adds system-prompt guidance.
- Custom tool names work in agent `permission` maps.
- Missing factory: `AgentBuildError::UnknownCustomTool`.
- Wrong return type: `AgentBuildError::CustomToolDowncastFailed`.
- Factory creation failure: `AgentBuildError::CustomToolCreateFailed`.
- Definition/catalog mismatch: `AgentBuildError::CustomToolNameMismatch`.

See [reloaded-code-core API docs](https://docs.rs/reloaded-code-core/latest)
for full API details.
Expand Down Expand Up @@ -474,4 +506,4 @@ For a deeper dive into path security, see [Sandboxing](sandboxing.md).
[bubblewrap]: https://github.com/containers/bubblewrap
[create_todo_tools]: https://docs.rs/reloaded-code-serdesai/latest/reloaded_code_serdesai/tools/todo/fn.create_todo_tools.html
[reloaded-code-core]: https://docs.rs/reloaded-code-core
[reloaded-code-serdesai]: https://docs.rs/reloaded-code-serdesai
[reloaded-code-serdesai]: https://docs.rs/reloaded-code-serdesai
15 changes: 8 additions & 7 deletions src/reloaded-code-agents/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -282,10 +282,11 @@ the factory on the builder:
```rust,no_run
use reloaded_code_agents::AgentRuntimeBuilder;
use reloaded_code_core::{
ToolBuildContext, ToolCatalogEntry, ToolCatalogKind, ToolContext, ToolFactory,
CustomTool, ToolBuildContext, ToolCatalogEntry, ToolCatalogKind, ToolContext,
ToolFactory, ToolResult,
};
use reloaded_code_core::context::ToolPrompt;
use std::any::Any;
use std::sync::Arc;

struct WebSearchFactory;

Expand All @@ -297,8 +298,8 @@ impl ToolContext for WebSearchFactory {
}

impl ToolFactory for WebSearchFactory {
fn create(&self, _ctx: &ToolBuildContext) -> Box<dyn Any + Send + Sync> {
todo!("return tool instance")
fn create(&self, _ctx: &ToolBuildContext) -> ToolResult<Arc<dyn CustomTool>> {
todo!("return portable custom tool instance")
}
}

Expand All @@ -311,10 +312,11 @@ let runtime = AgentRuntimeBuilder::new()
.tools(tools)
.build()?;

# Ok::<(), reloaded_code_agents::AgentLoadError>(())
# Ok::<(), reloaded_code_core::permissions::ExpandError>(())
Comment thread
Sewer56 marked this conversation as resolved.
```

`create()` returns `Box<dyn Any + Send + Sync>`.
`create()` returns a portable `Arc<dyn CustomTool>`. Framework adapters wrap
that object in their native tool trait.

If a `ToolCatalogKind::Custom` entry has no matching factory, build returns
`AgentBuildError::UnknownCustomTool`.
Expand Down Expand Up @@ -365,4 +367,3 @@ For the internal architecture, see [ARCHITECTURE.md](https://github.com/Reloaded
[API Reference]: https://docs.rs/reloaded-code-agents
[`ToolFactory`]: https://docs.rs/reloaded_code_core/latest/reloaded_code_core/trait.ToolFactory.html
[`ToolContext`]: https://docs.rs/reloaded_code_core/latest/reloaded_code_core/trait.ToolContext.html

2 changes: 1 addition & 1 deletion src/reloaded-code-agents/src/path/resolver.rs
Original file line number Diff line number Diff line change
Expand Up @@ -286,7 +286,7 @@ mod tests {

// Bare "**" -> workspace root (same as Action(Allow)).
let expected = soft_canonicalize(temp.path())?;
assert_eq!(inner.allowed_paths(), &[expected.clone()]);
assert_eq!(inner.allowed_paths(), std::slice::from_ref(&expected));

// Any path within workspace should be allowed.
assert!(
Expand Down
42 changes: 37 additions & 5 deletions src/reloaded-code-agents/src/runtime/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -109,10 +109,10 @@ mod tests {
use reloaded_code_core::permissions::{ExpandError, PermissionAction};
use reloaded_code_core::tool_metadata::{glob as glob_meta, read as read_meta};
use reloaded_code_core::{
default_tools, TaskSettings, ToolBuildContext, ToolCatalogEntry, ToolCatalogKind,
ToolFactory,
default_tools, CustomTool, CustomToolDefinition, CustomToolFuture, TaskSettings,
ToolBuildContext, ToolCatalogEntry, ToolCatalogKind, ToolFactory, ToolOutput, ToolResult,
ToolRunContext,
};
use std::any::Any;
use std::sync::Arc;

type TestResult = Result<(), ExpandError>;
Expand Down Expand Up @@ -241,8 +241,40 @@ mod tests {
}

impl ToolFactory for TestFactory {
fn create(&self, _ctx: &ToolBuildContext) -> Box<dyn Any + Send + Sync> {
Box::new(())
fn create(&self, _ctx: &ToolBuildContext) -> ToolResult<Arc<dyn CustomTool>> {
Ok(Arc::new(TestTool {
name: self.name,
prompt: self.prompt,
}))
}
}

struct TestTool {
name: &'static str,
prompt: &'static str,
}

impl ToolContext for TestTool {
fn name(&self) -> &'static str {
self.name
}

fn context(&self) -> ToolPrompt {
ToolPrompt::Static(self.prompt)
}
}

impl CustomTool for TestTool {
fn definition(&self) -> CustomToolDefinition {
CustomToolDefinition::new(self.name, "test tool")
}

fn call<'a>(
&'a self,
_ctx: ToolRunContext<'a>,
_args: serde_json::Value,
) -> CustomToolFuture<'a> {
Box::pin(async { Ok(ToolOutput::new("ok")) })
}
}

Expand Down
14 changes: 9 additions & 5 deletions src/reloaded-code-core/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -275,8 +275,9 @@ Currently uses ~2000 tokens for full toolset, ~560 tokens for search-only.

Core provides framework-agnostic plumbing for user-defined tools:

- [`ToolFactory`] - trait for building a tool from build-time context. Returns a
type-erased `Box<dyn Any>` so adapter crates can downcast to their own tool trait.
- [`ToolFactory`] - builds a tool from context, returning a portable [`CustomTool`].
- [`CustomToolDefinition`] - framework-neutral name, description, and JSON Schema.
- [`ToolRunContext`] - optional framework metadata for tool calls.
- [`ToolBuildContext`] - shared build-time info passed to every factory (workspace
root, permissions). Create once, reuse.
- [`CustomToolRegistry`] - stores factories by tool name. Insert, lookup, iterate.
Expand All @@ -286,9 +287,9 @@ Core provides framework-agnostic plumbing for user-defined tools:
- [`ToolCatalogKind`] - enum listing every tool type (Read, Write, Bash, etc.).
Adapters use this to know which tools to build.

Adapter crates downcast the `Box<dyn Any>` returned by [`ToolFactory::create`]
to their framework's tool trait. This keeps core free of dependencies on any
specific LLM framework like SerdesAI.
Adapters wrap the [`CustomTool`] from [`ToolFactory::create`] in their
framework's native tool trait, keeping core framework-free and enabling reuse
across adapters.

## Permissions

Expand Down Expand Up @@ -379,9 +380,12 @@ let key = resolver.resolve("OPENAI_API_KEY");
[`build(self)`]: crate::SystemPromptBuilder::build
[`context`]: crate::context
[`ToolContext`]: crate::context::ToolContext
[`CustomTool`]: crate::CustomTool
[`CustomToolDefinition`]: crate::CustomToolDefinition
[`ToolFactory`]: crate::ToolFactory
[`ToolFactory::create`]: crate::ToolFactory::create
[`ToolBuildContext`]: crate::ToolBuildContext
[`ToolRunContext`]: crate::ToolRunContext
[`CustomToolRegistry`]: crate::CustomToolRegistry
[`SharedToolRegistry`]: crate::SharedToolRegistry
[`ToolCatalogEntry`]: crate::ToolCatalogEntry
Expand Down
Loading
Loading