diff --git a/README.MD b/README.MD index 198ea86..5da37b7 100644 --- a/README.MD +++ b/README.MD @@ -25,13 +25,13 @@ A **CI pipeline**? A **custom product**? **ReloadedCode** ships the same agent tools as a Rust library. Shell sandboxing. Default-deny permissions. ~10 MiB footprint. -| | OpenCode | ReloadedCode | -| ------------ | ------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Language | TypeScript | Rust | +| | OpenCode | ReloadedCode | +| ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Language | TypeScript | Rust | | Memory | ~305 MiB | ~13 MiB | -| Interface | TUI / Desktop / IDE | Library (headless) | -| Agent format | Markdown + YAML | Similar format | -| Embeddable | HTTP API | Rust crate | +| Interface | TUI / Desktop / IDE | Library (headless) | +| Agent format | Markdown + YAML | Similar format | +| Embeddable | HTTP API | Rust crate | ## Features @@ -114,13 +114,14 @@ async fn main() -> Result<(), Box> { ## Crate Map -| Crate | Version | Description | -| --------------------------------------------------------------------- | ------- | ------------------------------------------------------------------------------------------------ | -| [**reloaded-code-core**](./src/reloaded-code-core/) | 0.2 | Framework-agnostic tool implementations, path resolvers, permissions, system prompt builder | -| [**reloaded-code-agents**](./src/reloaded-code-agents/) | 0.1 | agent markdown loader similar to [OpenCode](https://opencode.ai), typed catalog, runtime builder | -| [**reloaded-code-serdesai**](./src/reloaded-code-serdesai/) | 0.2 | SerdesAI framework integration, tool adapters, 15 provider bridges, task delegation | -| [**reloaded-code-bubblewrap**](./src/reloaded-code-bubblewrap/) | 0.1 | Linux bubblewrap sandbox profiles (Public Bot + Trusted Maintenance) | -| [**reloaded-code-models-dev**](./src/reloaded-code-models-dev/) | 0.1 | models.dev catalog sync with ETag caching and offline fallback | +| Crate | Version | Description | +| ------------------------------------------------------------------------- | ------- | ------------------------------------------------------------------------------------------------ | +| [**reloaded-code-core**](./src/reloaded-code-core/) | 0.2 | Framework-agnostic tool implementations, path resolvers, permissions, custom tool registry | +| [**reloaded-code-agents**](./src/reloaded-code-agents/) | 0.1 | agent markdown loader similar to [OpenCode](https://opencode.ai), typed catalog, runtime builder | +| [**reloaded-code-serdesai**](./src/reloaded-code-serdesai/) | 0.2 | SerdesAI framework integration, tool adapters, 15 provider bridges, task delegation | +| [**reloaded-code-bubblewrap**](./src/reloaded-code-bubblewrap/) | 0.1 | Linux bubblewrap sandbox profiles (Public Bot + Trusted Maintenance) | +| [**reloaded-code-models-dev**](./src/reloaded-code-models-dev/) | 0.1 | models.dev catalog sync with ETag caching and offline fallback | +| [**reloaded-code-provider-config**](./src/reloaded-code-provider-config/) | 0.1 | Provider configuration loading and provider catalog overrides | ## Examples diff --git a/docs/src/agents.md b/docs/src/agents.md index 223ae4f..f1dcc02 100644 --- a/docs/src/agents.md +++ b/docs/src/agents.md @@ -92,6 +92,17 @@ Evaluation uses **last-match-wins**: the final matching rule takes effect. For the full rule table and examples, see [Tools > Permission rules](tools.md#permission-rules). +#### Custom tool permissions + +Custom tools are referenced in the `permission` map by name, same as built-in tools. +See [Custom tools](tools.md#custom-tools) for registration. + +```yaml +permission: + web_search: allow # custom tool + database: deny # custom tool +``` + ### Model specification Format: `provider/model-id` or `synthetic/hf:huggingface-model-id`. diff --git a/docs/src/architecture.md b/docs/src/architecture.md index 73c8397..9d8f449 100644 --- a/docs/src/architecture.md +++ b/docs/src/architecture.md @@ -12,12 +12,14 @@ graph TD serdesai["reloaded-code-serdesai
SerdesAI framework integration"] bubblewrap["reloaded-code-bubblewrap
Linux sandbox profiles"] modelsdev["reloaded-code-models-dev
models.dev catalog sync"] + providerconfig["reloaded-code-provider-config
Provider config loading"] agents --> core serdesai --> core serdesai --> agents serdesai -.->|optional| bubblewrap modelsdev --> core + providerconfig --> core classDef default fill:#1C1C1C,stroke:#fa7774,strokeWidth:2px,color:#fff ``` @@ -32,6 +34,8 @@ The foundation. Contains every tool implementation as a plain function - **Path resolvers** - control which files tools can access - **System prompt builder** - generates context-aware tool guidance - **Permission engine** - last-match-wins rules with wildcard patterns +- **Custom tool registry + catalog** - framework-agnostic `ToolFactory`, + `CustomToolRegistry`, and `ToolCatalogEntry` types - **Credential resolver** - API key lookup with override support ([details](getting-started.md#credential-management)) - **Model catalog** - compact hash-table-based provider/model lookup @@ -46,6 +50,7 @@ Loads agent definitions from markdown files with YAML frontmatter. Provides: - **AgentLoader** - scans directories for `.md` agent files - **AgentCatalog** - name-to-config lookup table - **AgentRuntime** - bundles catalog + defaults + permissions + task settings +- **AgentRuntimeBuilder** - accepts core tool catalogs and custom tool factories The agent file format mirrors [OpenCode]'s schema - similar enough that many files are drop-in compatible, but [not identical](migration.md). The most @@ -82,6 +87,11 @@ Syncs the online [models.dev](https://models.dev) catalog into a compact - Offline fallback when network is unavailable - Cache load in ~0.3 ms +### reloaded-code-provider-config + +Loads provider override configuration and turns it into provider catalog entries +that can be merged with or replace the defaults from models.dev. + ## Where your code plugs in There are two integration paths: diff --git a/docs/src/getting-started.md b/docs/src/getting-started.md index 0974d43..25bf8e4 100644 --- a/docs/src/getting-started.md +++ b/docs/src/getting-started.md @@ -155,6 +155,38 @@ a Rust project and an LLM API key (e.g. `OPENAI_API_KEY`). [serdesai-agents](https://github.com/Reloaded-Project/ReloadedCode/blob/main/src/reloaded-code-serdesai/examples/serdesai-agents.rs) (with agent files). See [Examples](examples.md) for the full list. +## Custom tools + +Implement [`ToolContext`] and [`ToolFactory`], then register with the builder: + +```rust +struct MyFactory; +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 { + todo!("return your tool") + } +} + +let runtime = AgentRuntimeBuilder::new() + .custom_tool(MyFactory) + .tools(vec![ + ToolCatalogEntry::new("my_tool", ToolCatalogKind::Custom), + ]) + .build()?; +``` + +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"`) - diff --git a/docs/src/guides/custom-framework.md b/docs/src/guides/custom-framework.md index d864c4e..a7dc8d8 100644 --- a/docs/src/guides/custom-framework.md +++ b/docs/src/guides/custom-framework.md @@ -97,6 +97,10 @@ pb.track(read_tool); // pb.track(other_tool); // pb.track(another_tool); +// For custom tools (e.g. tool factories, framework adapters) where you +// have name + prompt but no instance, use track_entry(): +pb.track_entry("my_custom_tool", ToolPrompt::Static("Use my_custom_tool to do X.")); + let system_prompt = pb.build(); ``` @@ -143,22 +147,24 @@ let glob = AllowedGlobResolver::new(["/workspace/project"])? ## What you get from core -| Component | What it provides | -| ---------------------------------------------- | ------------------------------------------- | -| `read_file`, `write_file`, `edit_file` | File operations | -| `glob_files`, `grep_search` | Search operations | -| `execute_command`, `execute_command_with_mode` | Shell execution | -| `fetch_url` | URL fetching | -| `read_todos`, `write_todos` | Shared todo state | -| `SystemPromptBuilder` | Context-aware system prompt generation | -| `ToolContext` trait | Tool metadata interface for prompt building | -| `PathResolver` trait | Path security boundary | -| `AllowedPathResolver` | Directory-based sandbox | +| Component | What it provides | +| ---------------------------------------------- | -------------------------------------------------------- | +| `read_file`, `write_file`, `edit_file` | File operations | +| `glob_files`, `grep_search` | Search operations | +| `execute_command`, `execute_command_with_mode` | Shell execution | +| `fetch_url` | URL fetching | +| `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 | +| `ToolCatalogEntry` / `ToolCatalogKind` | Standard/custom tool catalog for adapters | +| `PathResolver` trait | Path security boundary | +| `AllowedPathResolver` | Directory-based sandbox | | `AllowedGlobResolver` | Glob-based sandbox (last matching rule takes precedence) | -| `Ruleset` / `Rule` | Permission evaluation engine | -| `CredentialResolver` | API key lookup with overrides | -| `ModelCatalog` | Compact provider/model hash table | -| `ToolError` | Unified error type for all tools | +| `Ruleset` / `Rule` | Permission evaluation engine | +| `CredentialResolver` | API key lookup with overrides | +| `ModelCatalog` | Compact provider/model hash table | +| `ToolError` | Unified error type for all tools | For the full API reference, see [docs.rs/reloaded-code-core](https://docs.rs/reloaded-code-core). diff --git a/docs/src/tools.md b/docs/src/tools.md index bb6fba3..71da74f 100644 --- a/docs/src/tools.md +++ b/docs/src/tools.md @@ -118,6 +118,58 @@ permission: | [**todoread**](#todoread-todowrite) | `read_todos` | Read shared todo list state | | [**todowrite**](#todoread-todowrite) | `write_todos` | Update shared todo list state | | [**task**](#task) | `TaskInput`/`TaskOutput` | Delegate work to a named sub-agent | +| [**custom**](#custom-tools) | `ToolFactory` | User-defined tool registered by the embedder | + +### Custom tools + +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, +}; +use reloaded_code_core::context::ToolPrompt; +use std::any::Any; + +struct WebSearchFactory; + +// Name + prompt guidance. +impl ToolContext for WebSearchFactory { + fn name(&self) -> &'static str { "web_search" } + fn context(&self) -> ToolPrompt { + ToolPrompt::Static("Use web_search to find information online.") + } +} + +// Build framework-specific tool instance. +impl ToolFactory for WebSearchFactory { + fn create(&self, _ctx: &ToolBuildContext) -> Box { + // SerdesAI: return Box::new(Box>). + todo!("return your tool") + } +} + +let runtime = AgentRuntimeBuilder::new() + // Register factory. + .custom_tool(WebSearchFactory) + // Enable tool in catalog. + .tools(vec![ + ToolCatalogEntry::new("web_search", ToolCatalogKind::Custom), + ]) + .build()?; +``` + +Rules: + +- Factory 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`. + +See [reloaded-code-core API docs](https://docs.rs/reloaded-code-core/latest) +for full API details. ### read @@ -422,6 +474,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 -[Agents]: agents.md -[agent files]: agents.md +[reloaded-code-serdesai]: https://docs.rs/reloaded-code-serdesai \ No newline at end of file diff --git a/src/reloaded-code-agents/ARCHITECTURE.md b/src/reloaded-code-agents/ARCHITECTURE.md index beb3d4f..1b9bbfe 100644 --- a/src/reloaded-code-agents/ARCHITECTURE.md +++ b/src/reloaded-code-agents/ARCHITECTURE.md @@ -175,7 +175,8 @@ Fallback settings used when an individual agent doesn't specify them: ### Tool Catalog -`default_tools()` returns 10 entries: +`reloaded-code-core::default_tools()` returns 10 entries that +`AgentRuntimeBuilder` uses by default: | Kind | Tool name | | --------- | ----------- | @@ -340,8 +341,7 @@ reloaded-code-agents │ ├── state.rs AgentRuntime, AgentDefaults │ ├── builder.rs AgentRuntimeBuilder │ ├── model.rs resolve_model_with_catalog(), ResolvedModel, ModelResolutionError -│ ├── task.rs callable_targets(), summarize_callable_targets(), allowed_tools() -│ └── tool_catalog.rs ToolCatalogEntry, ToolCatalogKind, default_tools() +│ └── task.rs callable_targets(), summarize_callable_targets(), allowed_tools() └── benches/ └── parser.rs Criterion benchmarks for frontmatter parsing ``` diff --git a/src/reloaded-code-agents/README.md b/src/reloaded-code-agents/README.md index 468b47a..b856ea7 100644 --- a/src/reloaded-code-agents/README.md +++ b/src/reloaded-code-agents/README.md @@ -272,6 +272,53 @@ let runtime = AgentRuntimeBuilder::new() # Ok::<(), reloaded_code_agents::AgentLoadError>(()) ``` +## Custom tools + +Embedders can register custom tools that integrate with the agent runtime, +permission system, and system prompt builder. Custom tool primitives live in +`reloaded-code-core`; implement [`ToolContext`] and [`ToolFactory`], then register +the factory on the builder: + +```rust,no_run +use reloaded_code_agents::AgentRuntimeBuilder; +use reloaded_code_core::{ + ToolBuildContext, ToolCatalogEntry, ToolCatalogKind, ToolContext, ToolFactory, +}; +use reloaded_code_core::context::ToolPrompt; +use std::any::Any; + +struct WebSearchFactory; + +impl ToolContext for WebSearchFactory { + fn name(&self) -> &'static str { "web_search" } + fn context(&self) -> ToolPrompt { + ToolPrompt::Static("Use web_search to find information online.") + } +} + +impl ToolFactory for WebSearchFactory { + fn create(&self, _ctx: &ToolBuildContext) -> Box { + todo!("return tool instance") + } +} + +let tools = vec![ + ToolCatalogEntry::new("web_search", ToolCatalogKind::Custom), +]; + +let runtime = AgentRuntimeBuilder::new() + .custom_tool(WebSearchFactory) + .tools(tools) + .build()?; + +# Ok::<(), reloaded_code_agents::AgentLoadError>(()) +``` + +`create()` returns `Box`. + +If a `ToolCatalogKind::Custom` entry has no matching factory, build returns +`AgentBuildError::UnknownCustomTool`. + ## Differences from OpenCode The agent file format mirrors [OpenCode]'s. Many files are drop-in @@ -316,3 +363,6 @@ For the internal architecture, see [ARCHITECTURE.md](https://github.com/Reloaded [OpenCode]: https://opencode.ai/ [Documentation]: https://reloaded-project.github.io/ReloadedCode/agents [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 + diff --git a/src/reloaded-code-agents/src/lib.rs b/src/reloaded-code-agents/src/lib.rs index a1bf88a..9227bdb 100644 --- a/src/reloaded-code-agents/src/lib.rs +++ b/src/reloaded-code-agents/src/lib.rs @@ -16,9 +16,9 @@ pub use loader::AgentLoader; pub use parser::AgentParseError; pub use path::{build_resolver_for_tool, FileToolResolver}; pub use runtime::{ - callable_targets, default_tools, resolve_model_with_catalog, summarize_callable_targets, - AgentDefaults, AgentRuntime, AgentRuntimeBuilder, ModelResolutionError, ResolvedModel, - TaskSettings, TaskTargetSummary, ToolCatalogEntry, ToolCatalogKind, + callable_targets, resolve_model_with_catalog, summarize_callable_targets, AgentDefaults, + AgentRuntime, AgentRuntimeBuilder, ModelResolutionError, ResolvedModel, TaskSettings, + TaskTargetSummary, }; pub use types::{ parse_model_parts, AgentConfig, AgentLoadError, AgentLoadResult, AgentMode, AgentToolSettings, diff --git a/src/reloaded-code-agents/src/path/resolver.rs b/src/reloaded-code-agents/src/path/resolver.rs index 90ee90e..52a87a4 100644 --- a/src/reloaded-code-agents/src/path/resolver.rs +++ b/src/reloaded-code-agents/src/path/resolver.rs @@ -182,7 +182,7 @@ fn build_glob_policy( #[cfg(test)] mod tests { use super::*; - use reloaded_code_core::ToolBuildContext; + use reloaded_code_core::tool_context::ToolBuildContext; use soft_canonicalize::soft_canonicalize; type TestResult = Result<(), ToolError>; diff --git a/src/reloaded-code-agents/src/runtime/builder.rs b/src/reloaded-code-agents/src/runtime/builder.rs index 0ec7128..6f2089c 100644 --- a/src/reloaded-code-agents/src/runtime/builder.rs +++ b/src/reloaded-code-agents/src/runtime/builder.rs @@ -1,18 +1,21 @@ //! Builds an [`AgentRuntime`] from your agents, defaults, and tools. use super::state::{AgentDefaults, AgentRuntime}; -use super::tool_catalog::{default_tools, ToolCatalogEntry}; use crate::AgentCatalog; use reloaded_code_core::permissions::ExpandError; -use reloaded_code_core::TaskSettings; +use reloaded_code_core::{ + default_tools, CustomToolRegistry, SharedToolRegistry, TaskSettings, ToolCatalogEntry, + ToolFactory, +}; /// Builds an [`AgentRuntime`] step by step. -#[derive(Debug, Clone)] +#[derive(Debug)] pub struct AgentRuntimeBuilder { catalog: AgentCatalog, defaults: AgentDefaults, task_settings: TaskSettings, tools: Vec, + custom_tool_registry: CustomToolRegistry, } impl Default for AgentRuntimeBuilder { @@ -31,6 +34,7 @@ impl AgentRuntimeBuilder { defaults: AgentDefaults::default(), task_settings: TaskSettings::default(), tools: default_tools(), + custom_tool_registry: CustomToolRegistry::new(), } } @@ -69,26 +73,46 @@ impl AgentRuntimeBuilder { self } + /// Registers a custom tool factory. + /// + /// The factory's name (via [`ToolContext::name`](reloaded_code_core::ToolContext::name)) + /// must match the `name` field of the corresponding [`ToolCatalogEntry`] with kind + /// [`ToolCatalogKind::Custom`](reloaded_code_core::ToolCatalogKind::Custom). + pub fn custom_tool(mut self, factory: impl ToolFactory + 'static) -> Self { + self.custom_tool_registry.insert(factory); + self + } + /// Finishes building and returns the [`AgentRuntime`]. /// /// # Errors /// - Returns [`ExpandError`] when any agent's permission configuration contains invalid patterns. #[inline] pub fn build(self) -> Result { - AgentRuntime::from_parts(self.catalog, self.defaults, self.task_settings, self.tools) + AgentRuntime::from_parts( + self.catalog, + self.defaults, + self.task_settings, + self.tools, + SharedToolRegistry::from_registry(self.custom_tool_registry), + ) } } #[cfg(test)] mod tests { use super::AgentRuntimeBuilder; - use crate::runtime::tool_catalog::{default_tools, ToolCatalogEntry, ToolCatalogKind}; use crate::runtime::AgentDefaults; use crate::{AgentCatalog, AgentConfig, AgentMode, AgentToolSettings, PermissionRule}; use indexmap::IndexMap; + use reloaded_code_core::context::{ToolContext, ToolPrompt}; 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::TaskSettings; + use reloaded_code_core::{ + default_tools, TaskSettings, ToolBuildContext, ToolCatalogEntry, ToolCatalogKind, + ToolFactory, + }; + use std::any::Any; use std::sync::Arc; type TestResult = Result<(), ExpandError>; @@ -192,4 +216,46 @@ mod tests { assert!(first.is_allowed(read_meta::NAME, "*")); Ok(()) } + + #[test] + fn builder_registers_custom_tool() -> TestResult { + struct TestFactory { + name: &'static str, + prompt: &'static str, + } + + impl TestFactory { + fn new(name: &'static str, prompt: &'static str) -> Self { + Self { name, prompt } + } + } + + impl ToolContext for TestFactory { + fn name(&self) -> &'static str { + self.name + } + + fn context(&self) -> ToolPrompt { + ToolPrompt::Static(self.prompt) + } + } + + impl ToolFactory for TestFactory { + fn create(&self, _ctx: &ToolBuildContext) -> Box { + Box::new(()) + } + } + + let runtime = AgentRuntimeBuilder::new() + .custom_tool(TestFactory::new("stub", "Stub tool guidance.")) + .build()?; + + let factory = runtime.custom_tool_registry().get("stub"); + assert!( + factory.is_some(), + "custom tool factory should be registered" + ); + assert_eq!(factory.unwrap().name(), "stub"); + Ok(()) + } } diff --git a/src/reloaded-code-agents/src/runtime/mod.rs b/src/reloaded-code-agents/src/runtime/mod.rs index ab27d43..744098c 100644 --- a/src/reloaded-code-agents/src/runtime/mod.rs +++ b/src/reloaded-code-agents/src/runtime/mod.rs @@ -11,11 +11,6 @@ //! - [`AgentDefaults`] - Default model, temperature, and top-p when agents don't specify them //! - [`TaskSettings`] - Shared Task delegation limits for all integrations using the runtime //! -//! Tools: -//! - [`ToolCatalogEntry`] - One tool the runtime can provide to agents -//! - [`ToolCatalogKind`] - Which tools are available -//! - [`default_tools()`] - The standard tool set (read, write, edit, glob, grep, bash, webfetch, todo, task) -//! //! Task delegation: //! - [`summarize_callable_targets()`] - Builds target summaries with names and descriptions //! - [`callable_targets()`] - Returns the agents the active agent may delegate to @@ -45,11 +40,9 @@ mod builder; mod model; mod state; mod task; -mod tool_catalog; pub use builder::AgentRuntimeBuilder; pub use model::{resolve_model_with_catalog, ModelResolutionError, ResolvedModel}; pub use reloaded_code_core::TaskSettings; pub use state::{AgentDefaults, AgentRuntime}; pub use task::{callable_targets, summarize_callable_targets, TaskTargetSummary}; -pub use tool_catalog::{default_tools, ToolCatalogEntry, ToolCatalogKind}; diff --git a/src/reloaded-code-agents/src/runtime/state.rs b/src/reloaded-code-agents/src/runtime/state.rs index 479120d..903a0bf 100644 --- a/src/reloaded-code-agents/src/runtime/state.rs +++ b/src/reloaded-code-agents/src/runtime/state.rs @@ -6,11 +6,10 @@ //! - [`AgentDefaults`] — Fallback settings when an agent doesn't specify them. use super::task::{build_runtime_task_caches, TaskTargetSummary}; -use super::tool_catalog::ToolCatalogEntry; use crate::{AgentCatalog, RulesetExt}; use ahash::AHashMap; use reloaded_code_core::permissions::{ExpandError, Ruleset}; -use reloaded_code_core::TaskSettings; +use reloaded_code_core::{SharedToolRegistry, TaskSettings, ToolCatalogEntry}; use std::sync::Arc; /// Default settings used when an agent doesn't specify them. @@ -43,6 +42,7 @@ pub struct AgentRuntime { defaults: AgentDefaults, task_settings: TaskSettings, tools: Vec, + custom_tool_registry: SharedToolRegistry, permission_rulesets: AHashMap>, allowed_tools_by_caller: AHashMap>, callable_target_summaries_by_caller: AHashMap>, @@ -55,6 +55,7 @@ impl AgentRuntime { defaults: AgentDefaults, task_settings: TaskSettings, tools: Vec, + custom_tool_registry: SharedToolRegistry, ) -> Result { let permission_rulesets = catalog .iter() @@ -73,6 +74,7 @@ impl AgentRuntime { defaults, task_settings, tools, + custom_tool_registry, permission_rulesets, allowed_tools_by_caller, callable_target_summaries_by_caller, @@ -103,6 +105,12 @@ impl AgentRuntime { &self.tools } + /// Returns the custom tool registry. + #[inline] + pub fn custom_tool_registry(&self) -> &SharedToolRegistry { + &self.custom_tool_registry + } + /// Returns the cached permission ruleset for the named caller. /// /// The returned [`Arc`] is cheap to clone and reuses the ruleset built when diff --git a/src/reloaded-code-agents/src/runtime/task.rs b/src/reloaded-code-agents/src/runtime/task.rs index 47e985a..5fb4e1e 100644 --- a/src/reloaded-code-agents/src/runtime/task.rs +++ b/src/reloaded-code-agents/src/runtime/task.rs @@ -5,11 +5,11 @@ //! - [`callable_targets`] - Returns the agents an active agent may delegate to via Task. //! - [`TaskTargetSummary`] - Stable Task UI metadata for a callable target. -use super::tool_catalog::{ToolCatalogEntry, ToolCatalogKind}; use crate::{AgentCatalog, AgentConfig, AgentMode, RulesetExt}; use ahash::AHashMap; use reloaded_code_core::permissions::{ExpandError, Ruleset}; use reloaded_code_core::tool_metadata::task as task_meta; +use reloaded_code_core::{ToolCatalogEntry, ToolCatalogKind}; use std::sync::Arc; /// Compact metadata used to describe one callable Task target. diff --git a/src/reloaded-code-core/README.md b/src/reloaded-code-core/README.md index f970512..d2a6911 100644 --- a/src/reloaded-code-core/README.md +++ b/src/reloaded-code-core/README.md @@ -3,7 +3,8 @@ [![Crates.io](https://img.shields.io/crates/v/reloaded-code-core.svg)](https://crates.io/crates/reloaded-code-core) [![Docs.rs](https://docs.rs/reloaded-code-core/badge.svg)](https://docs.rs/reloaded-code-core) Framework-agnostic core tools for building coding agents - file operations, -search, shell execution, sandboxing, permissions, and system prompt generation. +search, shell execution, sandboxing, permissions, custom tool registries, and +system prompt generation. Headless, TUI, or anything in between. Production-grade implementations with minimal overhead. @@ -22,6 +23,7 @@ Headless, TUI, or anything in between. Production-grade implementations with min - [Context and wrapper mapping](#context-and-wrapper-mapping) - [System prompt builder](#system-prompt-builder) - [Typical wrapper integration (serdesAI)](#typical-wrapper-integration-serdesai) + - [Custom tool registry](#custom-tool-registry) - [Permissions](#permissions) - [Credentials](#credentials) @@ -227,12 +229,18 @@ guidance in sync. [`SystemPromptBuilder`] builds one prompt string for agent runtimes. - [`track(&mut self, tool: T)`] records tool guidance and returns the tool unchanged. +- [`track_entry(&mut self, name, prompt)`] records a raw context entry without wrapping a + tool. Use this when you have name and prompt but no tool instance to wrap via + [`SystemPromptBuilder::track`]. - [`working_directory(self, path)`] and [`allowed_paths(self, resolver)`] add environment metadata. - [`add_context(self, name, context)`] appends supplemental sections (for example `GIT_WORKFLOW`). - [`system_prompt(self, prompt)`] prepends custom instructions; [`build(self)`] renders the final prompt. You usually build framework wrappers from these primitives (`ToolContext` + `SystemPromptBuilder`). +If tools are selected from a catalog or created later, use [`ToolFactory`], +[`ToolCatalogEntry`], and [`CustomToolRegistry`] from core. + ### Typical wrapper integration (serdesAI) For example with `reloaded-code-serdesai`, wrappers are built from these primitives. @@ -263,6 +271,25 @@ The system prompt is auto-optimized: cross-tool references e.g. `prefer X tool over Y for Z` are ommitted unless all tools are present. Currently uses ~2000 tokens for full toolset, ~560 tokens for search-only. +## Custom tool registry + +Core provides framework-agnostic plumbing for user-defined tools: + +- [`ToolFactory`] - trait for building a tool from build-time context. Returns a + type-erased `Box` so adapter crates can downcast to their own tool trait. +- [`ToolBuildContext`] - shared build-time info passed to every factory (workspace + root, permissions). Create once, reuse. +- [`CustomToolRegistry`] - stores factories by tool name. Insert, lookup, iterate. +- [`SharedToolRegistry`] - same as above, wrapped in `Arc`. Cheap to clone and + pass around runtime builders. +- [`ToolCatalogEntry`] - pairs a model-facing tool name with its [`ToolCatalogKind`]. +- [`ToolCatalogKind`] - enum listing every tool type (Read, Write, Bash, etc.). + Adapters use this to know which tools to build. + +Adapter crates downcast the `Box` returned by [`ToolFactory::create`] +to their framework's tool trait. This keeps core free of dependencies on any +specific LLM framework like SerdesAI. + ## Permissions [`permissions`] provides ordered allow/deny rules for tool access and delegation. @@ -344,6 +371,7 @@ let key = resolver.resolve("OPENAI_API_KEY"); [`TaskSettings`]: crate::TaskSettings [`SystemPromptBuilder`]: crate::SystemPromptBuilder [`track(&mut self, tool: T)`]: crate::SystemPromptBuilder::track +[`track_entry(&mut self, name, prompt)`]: crate::SystemPromptBuilder::track_entry [`working_directory(self, path)`]: crate::SystemPromptBuilder::working_directory [`allowed_paths(self, resolver)`]: crate::SystemPromptBuilder::allowed_paths [`add_context(self, name, context)`]: crate::SystemPromptBuilder::add_context @@ -351,6 +379,13 @@ let key = resolver.resolve("OPENAI_API_KEY"); [`build(self)`]: crate::SystemPromptBuilder::build [`context`]: crate::context [`ToolContext`]: crate::context::ToolContext +[`ToolFactory`]: crate::ToolFactory +[`ToolFactory::create`]: crate::ToolFactory::create +[`ToolBuildContext`]: crate::ToolBuildContext +[`CustomToolRegistry`]: crate::CustomToolRegistry +[`SharedToolRegistry`]: crate::SharedToolRegistry +[`ToolCatalogEntry`]: crate::ToolCatalogEntry +[`ToolCatalogKind`]: crate::ToolCatalogKind [`PathResolver`]: crate::PathResolver [`AbsolutePathResolver`]: crate::AbsolutePathResolver [`AllowedGlobResolver`]: crate::path::AllowedGlobResolver diff --git a/src/reloaded-code-core/src/context/mod.rs b/src/reloaded-code-core/src/context/mod.rs index c33138a..b0ab256 100644 --- a/src/reloaded-code-core/src/context/mod.rs +++ b/src/reloaded-code-core/src/context/mod.rs @@ -18,9 +18,7 @@ //! struct NotesTool; //! //! impl ToolContext for ReadTool { -//! fn name(&self) -> &'static str { -//! "read" -//! } +//! fn name(&self) -> &'static str { "read" } //! //! fn context(&self) -> ToolPrompt { //! ToolPrompt::Read { @@ -31,9 +29,7 @@ //! } //! //! impl ToolContext for NotesTool { -//! fn name(&self) -> &'static str { -//! "notes" -//! } +//! fn name(&self) -> &'static str { "notes" } //! //! fn context(&self) -> ToolPrompt { //! ToolPrompt::Static("Use this tool for short project notes.") @@ -72,9 +68,7 @@ pub const GITHUB_CLI: &str = include_str!("github_cli.txt"); /// struct MyTool; /// /// impl ToolContext for MyTool { -/// fn name(&self) -> &'static str { -/// "mytool" -/// } +/// fn name(&self) -> &'static str { "mytool" } /// /// fn context(&self) -> ToolPrompt { /// ToolPrompt::Static("Instructions for using MyTool...") diff --git a/src/reloaded-code-core/src/custom_tool/factory.rs b/src/reloaded-code-core/src/custom_tool/factory.rs new file mode 100644 index 0000000..64ff1bc --- /dev/null +++ b/src/reloaded-code-core/src/custom_tool/factory.rs @@ -0,0 +1,49 @@ +//! Trait and context for creating tools at build time. + +use crate::context::ToolContext; +use crate::tool_context::ToolBuildContext; +use std::any::Any; + +/// Build-time factory for a user-defined tool. +/// +/// Implement this to register a tool. The same type also acts as [`ToolContext`], +/// supplying the tool's identity and prompt. +/// +/// The [`ToolFactory::create()`] method returns a type-erased boxed value. +/// Adapter crates downcast it to the framework-specific tool trait they expect. +/// +/// # Example +/// +/// ```rust +/// use reloaded_code_core::{ToolBuildContext, ToolFactory}; +/// use reloaded_code_core::context::{ToolContext, ToolPrompt}; +/// use std::any::Any; +/// +/// struct WebSearchFactory; +/// +/// struct WebSearchTool; +/// impl WebSearchTool { +/// fn new(_ctx: &ToolBuildContext) -> Self { Self } +/// } +/// +/// impl ToolContext for WebSearchFactory { +/// fn name(&self) -> &'static str { "web_search" } +/// fn context(&self) -> ToolPrompt { +/// ToolPrompt::Static("Use web_search to find information online.") +/// } +/// } +/// +/// impl ToolFactory for WebSearchFactory { +/// fn create(&self, ctx: &ToolBuildContext) -> Box { +/// Box::new(WebSearchTool::new(ctx)) +/// } +/// } +/// ``` +pub trait ToolFactory: ToolContext + Send + Sync + 'static { + /// Creates a tool from build-time context. + /// + /// Return a [`Box`] wrapping the concrete tool value + /// or framework-specific boxed trait object. Adapter crates decide the + /// expected downcast type. + fn create(&self, ctx: &ToolBuildContext) -> Box; +} diff --git a/src/reloaded-code-core/src/custom_tool/mod.rs b/src/reloaded-code-core/src/custom_tool/mod.rs new file mode 100644 index 0000000..5cdbf0f --- /dev/null +++ b/src/reloaded-code-core/src/custom_tool/mod.rs @@ -0,0 +1,120 @@ +//! Custom tool registration primitives. +//! +//! Embedders implement [`ToolFactory`] to provide custom tools that integrate +//! with framework adapters, permission rules, and system prompt builders without +//! depending on an agent runtime. +//! +//! # Public API +//! +//! - [`ToolFactory`] - Trait for creating custom tools at build time. Extends +//! [`ToolContext`](crate::ToolContext) so factories provide name and prompt +//! guidance the same way built-in tools do. +//! - [`ToolBuildContext`] - Context passed to [`ToolFactory::create`]. Built-in +//! tools get this too, plus whatever extra dependencies they need. +//! - [`CustomToolRegistry`] - Registry of custom tool factories. +//! - [`SharedToolRegistry`] - Shared wrapper around a registry for cheap cloning. +//! +//! # Usage +//! +//! ```rust +//! use reloaded_code_core::{CustomToolRegistry, ToolBuildContext, ToolFactory}; +//! use reloaded_code_core::context::{ToolContext, ToolPrompt}; +//! use std::any::Any; +//! +//! struct MyFactory; +//! struct MyTool; +//! +//! impl ToolContext for MyFactory { +//! fn name(&self) -> &'static str { "my_tool" } +//! fn context(&self) -> ToolPrompt { +//! ToolPrompt::Static("Use my_tool to do things.") +//! } +//! } +//! +//! impl ToolFactory for MyFactory { +//! fn create(&self, _ctx: &ToolBuildContext) -> Box { +//! Box::new(MyTool) +//! } +//! } +//! +//! let mut registry = CustomToolRegistry::new(); +//! registry.insert(MyFactory); +//! assert!(registry.get("my_tool").is_some()); +//! ``` + +pub(crate) mod factory; +pub(crate) mod registry; + +pub use crate::tool_context::ToolBuildContext; +pub use factory::ToolFactory; +pub use registry::{CustomToolRegistry, SharedToolRegistry}; + +#[cfg(test)] +pub(crate) mod test_stubs; + +#[cfg(test)] +mod tests { + use super::test_stubs::{EchoFactory, TestFactory}; + use super::*; + use crate::context::ToolContext; + use crate::context::ToolPrompt; + + #[test] + fn registry_inserts_and_retrieves_factory() { + let mut registry = CustomToolRegistry::new(); + assert!(registry.is_empty()); + + registry.insert(EchoFactory::new("echo")); + assert_eq!(registry.len(), 1); + assert!(!registry.is_empty()); + + let factory = registry.get("echo").expect("factory should exist"); + assert_eq!(factory.name(), "echo"); + } + + #[test] + fn registry_returns_none_for_unknown_name() { + let registry = CustomToolRegistry::new(); + assert!(registry.get("missing").is_none()); + } + + #[test] + fn registry_insert_replaces_existing() { + let mut registry = CustomToolRegistry::new(); + registry.insert(EchoFactory::new("tool_a")); + registry.insert(EchoFactory::new("tool_a")); + assert_eq!(registry.len(), 1); + } + + #[test] + fn factory_create_returns_boxed_value() { + let factory = EchoFactory::new("echo"); + let ctx = ToolBuildContext::new(std::path::Path::new("/tmp"), None).unwrap(); + let boxed = factory.create(&ctx); + let value = boxed.downcast::().expect("should downcast to usize"); + assert_eq!(*value, 42); + } + + #[test] + fn factory_context_returns_prompt() { + let factory = EchoFactory::new("echo"); + assert!(matches!(factory.context(), ToolPrompt::Static(_))); + } + + #[test] + fn factory_context_can_skip_guidance_with_empty_static_prompt() { + let factory = TestFactory::new("no_context", ""); + assert!(matches!(factory.context(), ToolPrompt::Static(""))); + } + + #[test] + fn shared_registry_clones_and_accesses_factories() { + let mut registry = CustomToolRegistry::new(); + registry.insert(EchoFactory::new("echo")); + let shared = SharedToolRegistry::from_registry(registry); + + let cloned = shared.clone(); + assert!(cloned.get("echo").is_some()); + assert_eq!(cloned.len(), 1); + } +} diff --git a/src/reloaded-code-core/src/custom_tool/registry.rs b/src/reloaded-code-core/src/custom_tool/registry.rs new file mode 100644 index 0000000..e26eacd --- /dev/null +++ b/src/reloaded-code-core/src/custom_tool/registry.rs @@ -0,0 +1,103 @@ +//! Registries for storing and looking up custom tool factories. + +use super::factory::ToolFactory; +use std::collections::HashMap; +use std::ops::Deref; +use std::sync::Arc; + +/// Registry of custom tool factories, keyed by tool name. +#[derive(Default)] +pub struct CustomToolRegistry { + factories: HashMap<&'static str, Box>, +} + +impl std::fmt::Debug for CustomToolRegistry { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("CustomToolRegistry") + .field("factories", &self.factories.keys().collect::>()) + .finish() + } +} + +impl CustomToolRegistry { + /// Creates an empty registry. + #[inline] + #[must_use] + pub fn new() -> Self { + Self::default() + } + + /// Inserts a custom tool factory, keyed by [`ToolContext::name`]. + /// + /// If a factory with the same name already exists, it is replaced. + /// + /// [`ToolContext::name`]: crate::ToolContext::name + pub fn insert(&mut self, factory: impl ToolFactory + 'static) { + self.factories.insert(factory.name(), Box::new(factory)); + } + + /// Looks up a factory by tool name. + /// + /// Returns `None` when no factory is registered under `name`. + #[inline] + pub fn get(&self, name: &str) -> Option<&dyn ToolFactory> { + self.factories.get(name).map(|f| f.as_ref()) + } + + /// Returns `true` if the registry contains no factories. + #[inline] + pub fn is_empty(&self) -> bool { + self.factories.is_empty() + } + + /// Returns the number of registered factories. + #[inline] + pub fn len(&self) -> usize { + self.factories.len() + } +} + +/// Shared wrapper around a [`CustomToolRegistry`], cheaply cloneable via [`Arc`]. +/// +/// Cloning shares the same underlying map, making it cheap to pass through +/// runtime builders and framework adapters. +#[derive(Debug, Clone)] +pub struct SharedToolRegistry { + inner: Arc, +} + +impl SharedToolRegistry { + /// Creates an empty registry. + #[inline] + #[must_use] + pub fn new() -> Self { + Self { + inner: Arc::new(CustomToolRegistry::new()), + } + } + + /// Creates a shared registry from a populated [`CustomToolRegistry`]. + #[inline] + #[must_use] + pub fn from_registry(registry: CustomToolRegistry) -> Self { + Self { + inner: Arc::new(registry), + } + } +} + +impl Deref for SharedToolRegistry { + type Target = CustomToolRegistry; + + #[inline] + fn deref(&self) -> &Self::Target { + &self.inner + } +} + +impl Default for SharedToolRegistry { + #[inline] + fn default() -> Self { + Self::new() + } +} diff --git a/src/reloaded-code-core/src/custom_tool/test_stubs.rs b/src/reloaded-code-core/src/custom_tool/test_stubs.rs new file mode 100644 index 0000000..560be17 --- /dev/null +++ b/src/reloaded-code-core/src/custom_tool/test_stubs.rs @@ -0,0 +1,65 @@ +//! Shared test stubs for custom tool tests. + +use super::{ToolBuildContext, ToolFactory}; +use crate::context::{ToolContext, ToolPrompt}; +use std::any::Any; + +/// Minimal factory returning a configurable prompt and empty boxed value. +pub(crate) struct TestFactory { + pub(crate) tool_name: &'static str, + pub(crate) prompt: &'static str, +} + +impl TestFactory { + pub(crate) fn new(name: &'static str, prompt: &'static str) -> Self { + Self { + tool_name: name, + prompt, + } + } +} + +impl ToolContext for TestFactory { + fn name(&self) -> &'static str { + self.tool_name + } + + fn context(&self) -> ToolPrompt { + ToolPrompt::Static(self.prompt) + } +} + +impl ToolFactory for TestFactory { + fn create(&self, _ctx: &ToolBuildContext) -> Box { + Box::new(()) + } +} + +/// Factory that returns a downcastable integer for registry tests. +pub(crate) struct EchoFactory { + /// Tool name passed to [`ToolContext::name`]. + pub(crate) tool_name: &'static str, +} + +impl EchoFactory { + /// Creates a new [`EchoFactory`] with the given tool name. + pub(crate) fn new(name: &'static str) -> Self { + Self { tool_name: name } + } +} + +impl ToolContext for EchoFactory { + fn name(&self) -> &'static str { + self.tool_name + } + + fn context(&self) -> ToolPrompt { + ToolPrompt::Static("echo tool prompt") + } +} + +impl ToolFactory for EchoFactory { + fn create(&self, _ctx: &ToolBuildContext) -> Box { + Box::new(42_usize) + } +} diff --git a/src/reloaded-code-core/src/lib.rs b/src/reloaded-code-core/src/lib.rs index 32698e0..ebd5657 100644 --- a/src/reloaded-code-core/src/lib.rs +++ b/src/reloaded-code-core/src/lib.rs @@ -9,6 +9,7 @@ compile_error!("Either an async runtime (e.g., `tokio`) or `blocking` feature mu pub mod context; pub mod credentials; +pub mod custom_tool; pub mod error; pub mod fs; pub mod models; @@ -17,6 +18,7 @@ pub mod path; pub mod permissions; pub mod permissions_ext; pub mod system_prompt; +pub mod tool_catalog; pub mod tool_context; pub mod tool_metadata; pub mod tools; @@ -27,11 +29,12 @@ mod internal; pub use context::ToolContext; pub use credentials::{CredentialLookup, CredentialResolver}; +pub use custom_tool::{CustomToolRegistry, SharedToolRegistry, ToolBuildContext, ToolFactory}; pub use error::{ToolError, ToolResult}; pub use output::ToolOutput; pub use path::{AbsolutePathResolver, AllowedGlobResolver, AllowedPathResolver, PathResolver}; pub use system_prompt::SystemPromptBuilder; -pub use tool_context::ToolBuildContext; +pub use tool_catalog::{default_tools, ToolCatalogEntry, ToolCatalogKind}; pub use workspace::resolve_workspace_root; // Re-export tools (always available, sync or async based on runtime feature) diff --git a/src/reloaded-code-core/src/system_prompt.rs b/src/reloaded-code-core/src/system_prompt.rs index 6532f01..df5e5db 100644 --- a/src/reloaded-code-core/src/system_prompt.rs +++ b/src/reloaded-code-core/src/system_prompt.rs @@ -123,6 +123,19 @@ impl SystemPromptBuilder { tool } + /// Records a raw context entry without wrapping a tool. + /// + /// Use this when you have name and prompt but no tool instance to wrap + /// via [`Self::track`]. + /// + /// # Arguments + /// + /// * `name` - Tool name for the section header (e.g., `"web_search"`). + /// * `prompt` - Guidance to render for this tool. + pub fn track_entry(&mut self, name: &'static str, prompt: ToolPrompt) { + self.entries.push(ContextEntry { name, prompt }); + } + /// Adds supplemental context to the system prompt. /// /// Supplemental context appears in a separate "Supplemental Context" section diff --git a/src/reloaded-code-agents/src/runtime/tool_catalog.rs b/src/reloaded-code-core/src/tool_catalog.rs similarity index 79% rename from src/reloaded-code-agents/src/runtime/tool_catalog.rs rename to src/reloaded-code-core/src/tool_catalog.rs index c13c13c..f42ad22 100644 --- a/src/reloaded-code-agents/src/runtime/tool_catalog.rs +++ b/src/reloaded-code-core/src/tool_catalog.rs @@ -1,23 +1,16 @@ -//! Lists which tools your agents can use. +//! Tool catalog entries shared across runtimes and framework adapters. //! -//! Each [`ToolCatalogEntry`] pairs a tool name with its type ([`ToolCatalogKind`]). -//! -//! # Public API -//! -//! - [`ToolCatalogEntry`] - One tool the runtime can provide to agents -//! - [`ToolCatalogKind`] - The tools your agents can use -//! - [`default_tools()`] - The standard tool set -//! -//! The default tools are: read, write, edit, glob, grep, bash, webfetch, todoread, -//! todowrite, task. +//! Each [`ToolCatalogEntry`] pairs a model-facing tool name with its +//! [`ToolCatalogKind`]. The catalog describes what can be materialized; adapter +//! crates decide how each kind is built for their framework. -use reloaded_code_core::tool_metadata::{ +use crate::tool_metadata::{ bash as bash_meta, edit as edit_meta, glob as glob_meta, grep as grep_meta, read as read_meta, task as task_meta, todo_read as todo_read_meta, todo_write as todo_write_meta, webfetch as webfetch_meta, write as write_meta, }; -/// One tool the runtime can provide to agents. +/// One tool an integration can provide. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct ToolCatalogEntry { /// Tool name exposed to models. @@ -28,12 +21,13 @@ pub struct ToolCatalogEntry { impl ToolCatalogEntry { /// Creates a tool entry from its name and kind. + #[must_use] pub const fn new(name: &'static str, kind: ToolCatalogKind) -> Self { Self { name, kind } } } -/// The tools your agents can use. +/// Standard and custom tool kinds understood by reloaded-code adapters. #[derive(Debug, Clone, Copy, PartialEq, Eq)] #[non_exhaustive] pub enum ToolCatalogKind { @@ -55,8 +49,10 @@ pub enum ToolCatalogKind { TodoRead, /// Create and update todo items. TodoWrite, - /// Delegate to subagent via Task tool. + /// Delegate to another runtime target via Task. Task, + /// User-defined custom tool. + Custom, } const DEFAULT_TOOLS: [ToolCatalogEntry; 10] = [ @@ -73,6 +69,7 @@ const DEFAULT_TOOLS: [ToolCatalogEntry; 10] = [ ]; /// Returns the standard tool set. +#[must_use] pub fn default_tools() -> Vec { DEFAULT_TOOLS.to_vec() } @@ -80,7 +77,7 @@ pub fn default_tools() -> Vec { #[cfg(test)] mod tests { use super::{default_tools, ToolCatalogEntry, ToolCatalogKind}; - use reloaded_code_core::tool_metadata::{ + use crate::tool_metadata::{ bash as bash_meta, edit as edit_meta, glob as glob_meta, grep as grep_meta, read as read_meta, task as task_meta, todo_read as todo_read_meta, todo_write as todo_write_meta, webfetch as webfetch_meta, write as write_meta, @@ -104,4 +101,11 @@ mod tests { ], ); } + + #[test] + fn custom_variant_creates_catalog_entry() { + let entry = ToolCatalogEntry::new("web_search", ToolCatalogKind::Custom); + assert_eq!(entry.name, "web_search"); + assert_eq!(entry.kind, ToolCatalogKind::Custom); + } } diff --git a/src/reloaded-code-serdesai/README.md b/src/reloaded-code-serdesai/README.md index a5b6862..02d9c87 100644 --- a/src/reloaded-code-serdesai/README.md +++ b/src/reloaded-code-serdesai/README.md @@ -162,6 +162,86 @@ If you already have your own `ModelCatalog`, you can use that instead of See [examples/serdesai-agents.rs](examples/serdesai-agents.rs) and [examples/serdesai-task.rs](examples/serdesai-task.rs). +## Custom tools + +Register custom tools that integrate with the SerdesAI agent builder. Your +tool must implement `serdes_ai::Tool<()>` and be wrapped by a core +[`ToolFactory`]: + +```rust,no_run +use reloaded_code_agents::AgentRuntimeBuilder; +use reloaded_code_core::{ToolBuildContext, ToolCatalogEntry, ToolCatalogKind, ToolContext, ToolFactory}; +use reloaded_code_core::context::ToolPrompt; +use serdes_ai::tools::{RunContext, SchemaBuilder, Tool, ToolDefinition, ToolResult, ToolReturn}; +use std::any::Any; +use async_trait::async_trait; + +// 1. Define the tool - implement Tool<()> with a definition and call handler +struct EchoTool; + +#[async_trait] +impl Tool<()> for EchoTool { + fn definition(&self) -> ToolDefinition { + // For tools without parameters, just use ToolDefinition::new(name, description) + ToolDefinition::new("echo", "Echo a message back") + .with_parameters( + SchemaBuilder::new() + .string("message", "Message to echo", true) + .build() + .unwrap(), + ) + } + + async fn call(&self, _ctx: &RunContext<()>, args: serde_json::Value) -> ToolResult { + let msg = args["message"].as_str().unwrap_or_default(); + Ok(ToolReturn::text(msg)) + } +} + +// 2. Provide name and prompt guidance via ToolContext +struct EchoFactory; +impl ToolContext for EchoFactory { + fn name(&self) -> &'static str { "echo" } + fn context(&self) -> ToolPrompt { + ToolPrompt::Static("Use echo to repeat a message.") + } +} + +// 3. Create the tool at build time via ToolFactory +impl ToolFactory for EchoFactory { + fn create(&self, _ctx: &ToolBuildContext) -> Box { + Box::new(Box::new(EchoTool) as Box>) + } +} + +// 4. Register and build +fn main() -> Result<(), Box> { + let tools = vec![ + // ...existing tools... + ToolCatalogEntry::new("echo", ToolCatalogKind::Custom), + ]; + + let runtime = AgentRuntimeBuilder::new() + .custom_tool(EchoFactory) + .tools(tools) + .build()?; + Ok(()) +} +``` + +The SerdesAI build layer automatically: + +1. Looks up the factory by name in the custom tool registry +2. Calls `create()` with a shared `ToolBuildContext` (workspace root + permissions) +3. Downcasts the type-erased return to `Box>` +4. Registers prompt guidance via `SystemPromptBuilder::track_entry()` +5. Attaches the tool to the agent builder + +If a catalog entry references a custom tool with no registered factory, the +build returns `AgentBuildError::UnknownCustomTool`. If `create()` returns a +value that cannot be downcast, it returns +`AgentBuildError::CustomToolDowncastFailed`. + ## Linux Shell Sandboxing Sandboxing is **not enabled by default** for the `bash` tool - it runs @@ -218,3 +298,4 @@ Apache 2.0 [models.dev]: https://models.dev [Documentation]: https://reloaded-project.github.io/ReloadedCode/ [API Reference]: https://docs.rs/reloaded-code-serdesai +[`ToolFactory`]: https://docs.rs/reloaded-code-core/latest/reloaded_code_core/trait.ToolFactory.html diff --git a/src/reloaded-code-serdesai/src/agent_ext.rs b/src/reloaded-code-serdesai/src/agent_ext.rs index 5da2502..22ee9a6 100644 --- a/src/reloaded-code-serdesai/src/agent_ext.rs +++ b/src/reloaded-code-serdesai/src/agent_ext.rs @@ -50,6 +50,26 @@ impl> ToolExecutor for ToolAsEx } } +/// Adapter for boxed trait object tools, similar to [`ToolAsExecutor`] but +/// for dynamically dispatched tools where the concrete type is not known +/// at compile time. +struct DynToolAsExecutor(Box + Send + Sync>); + +#[async_trait] +impl ToolExecutor for DynToolAsExecutor { + async fn execute( + &self, + args: JsonValue, + ctx: &AgentRunContext, + ) -> Result { + let tools_ctx = ToolsRunContext::from_arc(ctx.deps.clone(), &ctx.model_name) + .with_run_id(&ctx.run_id) + .with_model_settings(ctx.model_settings.clone()); + + self.0.call(&tools_ctx, args).await + } +} + /// Extension trait for [`AgentBuilder`] to add tools that implement [`Tool`]. pub trait AgentBuilderExt { /// Add a tool that implements the [`Tool`] trait. @@ -73,6 +93,16 @@ pub trait AgentBuilderExt { /// # } /// ``` fn tool + 'static>(self, tool: T) -> Self; + + /// Add a boxed trait object tool. + /// + /// This is useful for dynamically created tools where the concrete type + /// is not known at compile time (e.g., custom tools from a factory). + fn tool_dyn( + self, + definition: serdes_ai::ToolDefinition, + tool: Box + Send + Sync>, + ) -> Self; } impl AgentBuilderExt for AgentBuilder @@ -84,6 +114,14 @@ where let definition = tool.definition(); self.tool_with_executor(definition, ToolAsExecutor(tool)) } + + fn tool_dyn( + self, + definition: serdes_ai::ToolDefinition, + tool: Box + Send + Sync>, + ) -> Self { + self.tool_with_executor(definition, DynToolAsExecutor(tool)) + } } /// Extension for converting [`ToolError`] results into [`AgentBuildError`]. diff --git a/src/reloaded-code-serdesai/src/agent_runtime/build.rs b/src/reloaded-code-serdesai/src/agent_runtime/build.rs index 75e3957..24366fa 100644 --- a/src/reloaded-code-serdesai/src/agent_runtime/build.rs +++ b/src/reloaded-code-serdesai/src/agent_runtime/build.rs @@ -15,8 +15,9 @@ use crate::{ use indexmap::IndexMap; use reloaded_code_agents::{ AgentRuntime, AgentToolSettings, ModelResolutionError, PermissionRule, TaskTargetSummary, - ToolCatalogEntry, ToolCatalogKind, build_resolver_for_tool, + build_resolver_for_tool, }; +use reloaded_code_core::context::ToolPrompt; use reloaded_code_core::permissions::Ruleset; use reloaded_code_core::tool_context::ToolBuildContext; use reloaded_code_core::tool_metadata::{ @@ -26,7 +27,10 @@ use reloaded_code_core::tool_metadata::{ use reloaded_code_core::tools::{ GlobSettings, GrepFormattingSettings, GrepSettings, ReadSettings, WebFetchSettings, }; -use reloaded_code_core::{CredentialLookup, ToolError, models::ModelCatalog}; +use reloaded_code_core::{ + CredentialLookup, SharedToolRegistry, ToolCatalogEntry, ToolCatalogKind, ToolError, + models::ModelCatalog, +}; use serdes_ai::AgentBuilder; use serdes_ai_models::BoxedModel; use std::path::Path; @@ -48,6 +52,18 @@ pub enum AgentBuildError { /// The missing agent name. name: Box, }, + /// A custom tool catalog entry has no matching registered factory. + #[error("no factory registered for custom tool `{name}`")] + UnknownCustomTool { + /// The tool name with no registered factory. + name: Box, + }, + /// A custom tool factory returned a value that cannot be downcast to the expected type. + #[error("custom tool `{name}` factory returned incompatible type")] + CustomToolDowncastFailed { + /// The tool name whose factory returned the wrong type. + name: Box, + }, /// The runtime contains a tool kind this adapter cannot materialise. #[error("tool `{name}` is not supported")] UnsupportedToolKind { @@ -158,7 +174,13 @@ where /// # Errors /// /// Returns [`AgentBuildError::UnsupportedToolKind`] when the runtime catalog contains an -/// unrecognized [`ToolCatalogKind`] variant (this function only handles standard tools). +/// unrecognized [`ToolCatalogKind`] variant. +/// +/// Returns [`AgentBuildError::UnknownCustomTool`] when a [`ToolCatalogKind::Custom`] entry +/// names a tool absent from the custom-tool registry. +/// +/// Returns [`AgentBuildError::CustomToolDowncastFailed`] when the type-erased object +/// produced by a custom-tool factory cannot be downcast to `Box>`. /// /// Returns [`AgentBuildError::ToolSettingsValidation`] when resolver creation or settings /// building fails for any tool, including: @@ -171,6 +193,7 @@ pub(super) fn attach_standard_tools<'a, C>( task_handle: Option<&TaskHandle>, workspace_root: &Path, bash_sandbox: Option<&Arc>, + custom_tool_registry: &SharedToolRegistry, ) -> Result<(AgentBuilder<(), String>, SystemPromptBuilder), AgentBuildError> where C: CredentialLookup + Send + Sync + 'static, @@ -196,9 +219,7 @@ where source: ToolError::InvalidPath(e.to_string()), })?; - // Use pre-built permission ruleset from PreparedBuild for non-file tools. - let permission = prepared.permission.clone(); - let permission_config = &prepared.permission_config; + let permission_config = prepared.permission_config; for entry in prepared.tools.iter() { match entry.kind { @@ -247,7 +268,7 @@ where #[allow(unused_mut)] let mut tool = BashTool::new() .with_timeouts(Some(settings.timeout_ms), Some(settings.max_timeout_ms)) - .with_permission(permission.clone()); + .with_permission(prepared.permission.clone()); #[cfg(all(feature = "linux-bubblewrap", target_os = "linux"))] if let Some(profile) = bash_sandbox { tool = tool.with_linux_bwrap(profile.clone()); @@ -275,6 +296,39 @@ where ))); } } + ToolCatalogKind::Custom => { + let factory = custom_tool_registry.get(entry.name).ok_or_else(|| { + AgentBuildError::UnknownCustomTool { + name: entry.name.into(), + } + })?; + + // create() returns type-erased Box for the + // cross-crate boundary; downcast to the concrete Tool<()> + // that the serdesai builder expects. + let boxed = factory.create(&build_context); + + // Downcast yields Box>> because create() + // wrapped Box> as Box. + let double_boxed: Box>> = + boxed + .downcast() + .map_err(|_| AgentBuildError::CustomToolDowncastFailed { + name: entry.name.into(), + })?; + + // Use ToolContext (which ToolFactory extends) to get + // name and prompt guidance consistently with built-in tools. + let tool_prompt = factory.context(); + // ToolPrompt::Static("") means no guidance (equivalent to + // the old prompt() returning None). + if !matches!(tool_prompt, ToolPrompt::Static("")) { + prompt_builder.track_entry(factory.name(), tool_prompt); + } + // Unwrap the double box to get Box>. + let tool = *double_boxed; + builder = builder.tool_dyn(tool.definition(), tool); + } _ => { return Err(AgentBuildError::UnsupportedToolKind { name: entry.name.into(), @@ -334,10 +388,10 @@ mod tests { use ahash::AHashMap; use indexmap::IndexMap; use reloaded_code_agents::{ - AgentCatalog, AgentConfig, AgentDefaults, AgentMode, AgentRuntimeBuilder, + AgentCatalog, AgentConfig, AgentDefaults, AgentMode, AgentRuntime, AgentRuntimeBuilder, AgentToolSettings, PermissionRule, }; - use reloaded_code_core::CredentialResolver; + use reloaded_code_core::context::{ToolContext, ToolPrompt}; use reloaded_code_core::models::{ Modality, ModelCatalog, ModelInfo, ProviderIdx, ProviderInfo, ProviderModelSource, ProviderSource, ProviderType, @@ -346,6 +400,10 @@ mod tests { use reloaded_code_core::tool_metadata::{ bash as bash_meta, glob as glob_meta, grep as grep_meta, read as read_meta, }; + use reloaded_code_core::{ + CredentialResolver, SharedToolRegistry, ToolBuildContext, ToolCatalogEntry, + ToolCatalogKind, ToolFactory, + }; use serdes_ai::AgentBuilder; use serdes_ai_models::MockModel; use std::collections::HashSet; @@ -357,6 +415,17 @@ mod tests { prepared: &super::PreparedBuild<'_>, name: &str, ) -> serdes_ai::Agent<(), String> { + build_mock_agent(prepared, &SharedToolRegistry::new(), name) + .expect("build should succeed") + .0 + } + + /// Builds a mock agent from prepared state and returns the agent plus prompt. + fn build_mock_agent( + prepared: &super::PreparedBuild<'_>, + registry: &SharedToolRegistry, + name: &str, + ) -> Result<(serdes_ai::Agent<(), String>, String), AgentBuildError> { let workspace_root = reloaded_code_core::resolve_workspace_root().expect("workspace root"); let (builder, prompt_builder) = attach_standard_tools::( AgentBuilder::<(), String>::new(MockModel::new(name)), @@ -364,9 +433,11 @@ mod tests { None, &workspace_root, None, - ) - .expect("build should succeed"); - builder.system_prompt(prompt_builder.build()).build() + registry, + )?; + let prompt = prompt_builder.build(); + let agent = builder.system_prompt(prompt.clone()).build(); + Ok((agent, prompt)) } /// Creates a minimal agent config with no model or sampling overrides. @@ -453,6 +524,45 @@ mod tests { credentials } + /// Builds a test runtime with one custom tool and read permission. + fn custom_tool_runtime( + agent_name: &str, + custom_name: &'static str, + factory: impl ToolFactory + 'static, + ) -> Result { + AgentRuntimeBuilder::new() + .catalog(AgentCatalog::from_entries([agent( + agent_name, + allow_tools(&[read_meta::NAME, custom_name]), + "prompt", + )])) + .tools(vec![ + ToolCatalogEntry::new(read_meta::NAME, ToolCatalogKind::Read), + ToolCatalogEntry::new(custom_name, ToolCatalogKind::Custom), + ]) + .custom_tool(factory) + .defaults(AgentDefaults::with_model("openrouter/openai/gpt-4.1-mini")) + .build() + } + + /// Prepares a built agent and final system prompt for assertion. + fn attach_test_agent( + runtime: &AgentRuntime, + agent_name: &str, + ) -> Result<(serdes_ai::Agent<(), String>, String), AgentBuildError> { + let catalog = catalog(); + let credentials = credentials(); + let prepared = prepare_build(runtime, agent_name, &catalog, &credentials, true)?; + build_mock_agent(&prepared, runtime.custom_tool_registry(), agent_name) + } + + /// Asserts a tool name is present on the built agent. + fn assert_tool_attached(agent: &serdes_ai::Agent<(), String>, name: &str) { + let names: std::collections::HashSet<&str> = + agent.tools().iter().map(|t| t.name()).collect(); + assert!(names.contains(name), "{name} tool should be attached"); + } + #[test] fn build_filters_tools_by_permission() -> TestResult { let credentials = credentials(); @@ -641,4 +751,81 @@ mod tests { ); Ok(()) } + + #[test] + fn build_returns_unknown_custom_tool_error() -> TestResult { + let tools = vec![ + ToolCatalogEntry::new(read_meta::NAME, ToolCatalogKind::Read), + ToolCatalogEntry::new("custom_missing", ToolCatalogKind::Custom), + ]; + + let runtime = AgentRuntimeBuilder::new() + .catalog(AgentCatalog::from_entries([agent( + "tester", + allow_tools(&[read_meta::NAME, "custom_missing"]), + "prompt", + )])) + .tools(tools) + .defaults(AgentDefaults::with_model("openrouter/openai/gpt-4.1-mini")) + .build()?; + + let result = attach_test_agent(&runtime, "tester"); + assert!( + matches!(&result, Err(AgentBuildError::UnknownCustomTool { name } ) if &**name == "custom_missing"), + "expected UnknownCustomTool error for custom_missing, got a different result" + ); + Ok(()) + } + + #[test] + fn build_returns_error_on_custom_tool_downcast_failure() -> TestResult { + use std::any::Any; + + struct BadFactory; + impl ToolContext for BadFactory { + fn name(&self) -> &'static str { + "bad_tool" + } + fn context(&self) -> ToolPrompt { + ToolPrompt::Static("Bad tool guidance.") + } + } + impl ToolFactory for BadFactory { + fn create(&self, _ctx: &ToolBuildContext) -> Box { + // Returns wrong type - not Box> + Box::new(42_usize) + } + } + + let runtime = custom_tool_runtime("bad_agent", "bad_tool", BadFactory)?; + let result = attach_test_agent(&runtime, "bad_agent"); + assert!( + matches!(&result, Err(AgentBuildError::CustomToolDowncastFailed { name } ) if &**name == "bad_tool"), + "expected CustomToolDowncastFailed for bad_tool, got a different result" + ); + Ok(()) + } + + #[test] + fn build_attaches_custom_tool_with_prompt() -> TestResult { + use crate::agent_runtime::test_stubs::SerdesTestFactory; + + let runtime = custom_tool_runtime( + "pinger", + "ping", + SerdesTestFactory::new("ping", "Use ping to check connectivity.", "pong"), + )?; + let attached = attach_test_agent(&runtime, "pinger"); + let (agent, prompt) = + attached.expect("custom tool build should succeed with valid factory"); + + assert_tool_attached(&agent, "ping"); + assert_tool_attached(&agent, read_meta::NAME); + + assert!( + prompt.contains("Use ping to check connectivity"), + "custom tool prompt guidance should appear in system prompt" + ); + Ok(()) + } } diff --git a/src/reloaded-code-serdesai/src/agent_runtime/mod.rs b/src/reloaded-code-serdesai/src/agent_runtime/mod.rs index 0b3222e..dd679e2 100644 --- a/src/reloaded-code-serdesai/src/agent_runtime/mod.rs +++ b/src/reloaded-code-serdesai/src/agent_runtime/mod.rs @@ -1,7 +1,7 @@ //! SerdesAI adapter for the generic agent runtime. //! -//! The data-only runtime foundation lives in [`reloaded_code_agents`]. This -//! module re-exports those generic types and adds SerdesAI-specific build +//! The data-only runtime foundation lives in `reloaded-code-agents`. This +//! module re-exports agent runtime types and adds SerdesAI-specific build //! orchestration through [`AgentBuildContext`]. //! //! # Public API @@ -12,11 +12,13 @@ mod build; mod model; mod provider_bridge; mod task; +#[cfg(test)] +mod test_stubs; pub use build::AgentBuildError; pub use reloaded_code_agents::{ AgentDefaults, AgentRuntime, AgentRuntimeBuilder, ModelResolutionError, ResolvedModel, - ToolCatalogEntry, ToolCatalogKind, default_tools, resolve_model_with_catalog, + resolve_model_with_catalog, }; pub use task::AgentBuildContext; pub(crate) use task::{TaskBuildContext, build_agent}; diff --git a/src/reloaded-code-serdesai/src/agent_runtime/task.rs b/src/reloaded-code-serdesai/src/agent_runtime/task.rs index 04920ee..4c06749 100644 --- a/src/reloaded-code-serdesai/src/agent_runtime/task.rs +++ b/src/reloaded-code-serdesai/src/agent_runtime/task.rs @@ -172,6 +172,10 @@ where /// validation fails during the build. /// - Returns [`AgentBuildError::UnsupportedToolKind`] when the runtime /// contains a tool kind this adapter cannot materialise. + /// - Returns [`AgentBuildError::UnknownCustomTool`] when a custom tool + /// entry names a tool absent from the custom-tool registry. + /// - Returns [`AgentBuildError::CustomToolDowncastFailed`] when a + /// custom-tool factory produces an object that cannot be downcast. #[inline] pub fn build(&self, name: &str) -> Result, AgentBuildError> { build_agent(Arc::clone(&self.context), name, 0) @@ -300,6 +304,10 @@ where /// validation fails during the build. /// - Returns [`AgentBuildError::UnsupportedToolKind`] when the runtime /// contains a tool kind this adapter cannot materialise. +/// - Returns [`AgentBuildError::UnknownCustomTool`] when a custom tool entry +/// names a tool absent from the custom-tool registry. +/// - Returns [`AgentBuildError::CustomToolDowncastFailed`] when a custom-tool +/// factory produces an object that cannot be downcast. pub(crate) fn build_agent( context: Arc>, name: &str, @@ -337,6 +345,7 @@ where Some(&task_handle), &context.workspace_root, sandbox_ref, + context.runtime.custom_tool_registry(), )?; Ok(builder.system_prompt(prompt_builder.build()).build()) } diff --git a/src/reloaded-code-serdesai/src/agent_runtime/test_stubs.rs b/src/reloaded-code-serdesai/src/agent_runtime/test_stubs.rs new file mode 100644 index 0000000..55c3023 --- /dev/null +++ b/src/reloaded-code-serdesai/src/agent_runtime/test_stubs.rs @@ -0,0 +1,79 @@ +//! Shared test stubs for SerdesAI custom tool tests. +//! +//! These combine a [`serdes_ai::Tool<()>`] implementation with a +//! [`ToolFactory`] so tests can exercise the full agent-build pipeline +//! including tool attachment and prompt guidance injection. + +use async_trait::async_trait; +use reloaded_code_core::context::{ToolContext, ToolPrompt}; +use reloaded_code_core::{ToolBuildContext, ToolFactory}; +use serdes_ai::tools::{RunContext, ToolDefinition, ToolReturn}; +use std::any::Any; + +/// A minimal `serdes_ai::Tool<()>` that returns a configurable text response. +struct SerdesTestTool { + name: &'static str, + response: &'static str, +} + +#[async_trait] +impl serdes_ai::Tool<()> for SerdesTestTool { + fn definition(&self) -> ToolDefinition { + ToolDefinition::new(self.name, self.name) + } + + async fn call(&self, _ctx: &RunContext<()>, _args: serde_json::Value) -> serdes_ai::ToolResult { + Ok(ToolReturn::text(self.response)) + } +} + +/// A `ToolFactory` that creates a [`SerdesTestTool`] and returns it as a +/// double-boxed `Box`. +/// +/// `name` and `prompt` are surfaced via `ToolContext` for system-prompt +/// guidance injection. `response` is returned by the tool's `call()`. +#[derive(Debug)] +pub struct SerdesTestFactory { + /// Tool name passed to `ToolContext::name()` and `ToolDefinition::new()`. + pub name: &'static str, + /// Prompt text passed to `ToolContext::context()`. + pub prompt: &'static str, + /// Text returned by `SerdesTestTool::call()`. + pub response: &'static str, +} + +impl SerdesTestFactory { + /// Creates a new factory that produces a tool named `name`, with system-prompt + /// guidance `prompt`, and `call()` returning `response`. + #[inline] + pub fn new(name: &'static str, prompt: &'static str, response: &'static str) -> Self { + Self { + name, + prompt, + response, + } + } +} + +impl ToolContext for SerdesTestFactory { + #[inline] + fn name(&self) -> &'static str { + self.name + } + + #[inline] + fn context(&self) -> ToolPrompt { + ToolPrompt::Static(self.prompt) + } +} + +impl ToolFactory for SerdesTestFactory { + #[inline] + fn create(&self, _ctx: &ToolBuildContext) -> Box { + let tool: Box> = Box::new(SerdesTestTool { + name: self.name, + response: self.response, + }); + Box::new(tool) + } +} diff --git a/src/reloaded-code-serdesai/src/lib.rs b/src/reloaded-code-serdesai/src/lib.rs index fcd82ad..95169b8 100644 --- a/src/reloaded-code-serdesai/src/lib.rs +++ b/src/reloaded-code-serdesai/src/lib.rs @@ -8,7 +8,7 @@ pub mod task; pub mod tools; /// Re-export core types for convenience. -pub use reloaded_code_core::{ToolError, ToolOutput, ToolResult}; +pub use reloaded_code_core::{TaskSettings, ToolError, ToolOutput, ToolResult}; /// Re-export bash execution mode and mode-aware execution. pub use reloaded_code_core::{BashExecutionMode, execute_command_with_mode}; @@ -45,5 +45,5 @@ pub use reloaded_code_core::{ pub use agent_runtime::{AgentBuildContext, AgentBuildError}; pub use reloaded_code_agents::{ AgentDefaults, AgentRuntime, AgentRuntimeBuilder, ModelResolutionError, ResolvedModel, - TaskSettings, ToolCatalogEntry, ToolCatalogKind, default_tools, resolve_model_with_catalog, + resolve_model_with_catalog, };