From b1268c70ba8f70a9079499c4aa3611103b8bf1cb Mon Sep 17 00:00:00 2001 From: Sewer56 Date: Thu, 12 Mar 2026 15:49:11 +0000 Subject: [PATCH 01/11] Added: Tool catalog foundation for JIT runtime tool registration Provides a cloneable catalog entry struct and kind enum with 9 tool variants (Read, Write, Edit, Glob, Grep, Bash, WebFetch, TodoRead, TodoWrite). Excludes Task tools to keep runtime registration lightweight. Changes: - Added llm-coding-tools-agents and llm-coding-tools-models-dev dependencies - Created ToolCatalogEntry, ToolCatalogKind, and default_tools() in serdesai crate - Exported minimal API surface for runtime tool registration - Added unit tests verifying catalog contents and Task exclusion Benefits: - Enables just-in-time tool catalog construction at runtime - Keeps runtime dependency-free from Task tool implementation - Provides explicit registration API with test coverage --- src/Cargo.lock | 2 + src/llm-coding-tools-serdesai/Cargo.toml | 4 + src/llm-coding-tools-serdesai/src/lib.rs | 2 + .../src/tool_catalog.rs | 106 ++++++++++++++++++ 4 files changed, 114 insertions(+) create mode 100644 src/llm-coding-tools-serdesai/src/tool_catalog.rs diff --git a/src/Cargo.lock b/src/Cargo.lock index 9bf2c98e..396eadb8 100644 --- a/src/Cargo.lock +++ b/src/Cargo.lock @@ -1523,7 +1523,9 @@ version = "0.2.0" dependencies = [ "async-trait", "futures", + "llm-coding-tools-agents", "llm-coding-tools-core", + "llm-coding-tools-models-dev", "reqwest 0.13.1", "serde", "serde_json", diff --git a/src/llm-coding-tools-serdesai/Cargo.toml b/src/llm-coding-tools-serdesai/Cargo.toml index bc13dff0..63fbf0ff 100644 --- a/src/llm-coding-tools-serdesai/Cargo.toml +++ b/src/llm-coding-tools-serdesai/Cargo.toml @@ -14,6 +14,10 @@ llm-coding-tools-core = { version = "0.2.0", path = "../llm-coding-tools-core", "tokio", ] } +# Runtime foundation dependencies +llm-coding-tools-agents = { version = "0.1.0", path = "../llm-coding-tools-agents" } +llm-coding-tools-models-dev = { version = "0.1.0", path = "../llm-coding-tools-models-dev" } + # serdes-ai provides Tool trait, ToolDefinition, RunContext serdes-ai = "0.2.6" serdes-ai-models = { version = "0.2.6", features = ["openrouter"] } diff --git a/src/llm-coding-tools-serdesai/src/lib.rs b/src/llm-coding-tools-serdesai/src/lib.rs index 9e9e3e9e..fef286e5 100644 --- a/src/llm-coding-tools-serdesai/src/lib.rs +++ b/src/llm-coding-tools-serdesai/src/lib.rs @@ -8,6 +8,7 @@ pub mod bash; mod common; pub mod convert; pub mod todo; +pub mod tool_catalog; pub mod webfetch; /// Re-export core types for convenience. @@ -47,4 +48,5 @@ pub use llm_coding_tools_core::{ // Re-export standalone tools pub use bash::BashTool; pub use todo::{TodoReadTool, TodoWriteTool, create_todo_tools}; +pub use tool_catalog::{ToolCatalogEntry, ToolCatalogKind, default_tools}; pub use webfetch::WebFetchTool; diff --git a/src/llm-coding-tools-serdesai/src/tool_catalog.rs b/src/llm-coding-tools-serdesai/src/tool_catalog.rs new file mode 100644 index 00000000..70ce749f --- /dev/null +++ b/src/llm-coding-tools-serdesai/src/tool_catalog.rs @@ -0,0 +1,106 @@ +//! Explicit default runtime tool catalog for SerdesAI agent builds. +//! +//! This module provides a cloneable, data-only tool catalog used during JIT agent +//! construction. Each [`ToolCatalogEntry`] pairs a canonical tool name with a +//! [`ToolCatalogKind`] variant that later runtime layers can match on to instantiate +//! concrete tools on demand. +//! +//! The default catalog exposed by [`default_tools()`] covers the non-Task tool surface +//! (read, write, edit, glob, grep, bash, webfetch, todoread, todowrite). Task is +//! intentionally excluded to keep the catalog focused on standard runtime tools. + +use llm_coding_tools_core::tool_names; + +/// Cloneable metadata for a runtime tool that can be materialized later. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct ToolCatalogEntry { + /// Canonical tool name exposed to models. + pub name: &'static str, + /// Concrete tool variant used during later runtime instantiation. + pub kind: ToolCatalogKind, +} + +impl ToolCatalogEntry { + /// Creates a catalog entry from a canonical tool name and concrete kind. + pub const fn new(name: &'static str, kind: ToolCatalogKind) -> Self { + Self { name, kind } + } +} + +/// Explicit tool variants supported by the default SerdesAI runtime surface. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ToolCatalogKind { + /// Read file contents tool. + Read, + /// Write file contents tool. + Write, + /// Edit file contents tool. + Edit, + /// Glob file pattern matching tool. + Glob, + /// Grep text search tool. + Grep, + /// Bash command execution tool. + Bash, + /// Web fetch tool for HTTP requests. + WebFetch, + /// Todo read tool for reading todo items. + TodoRead, + /// Todo write tool for creating and updating todo items. + TodoWrite, +} + +const DEFAULT_TOOLS: [ToolCatalogEntry; 9] = [ + ToolCatalogEntry::new(tool_names::READ, ToolCatalogKind::Read), + ToolCatalogEntry::new(tool_names::WRITE, ToolCatalogKind::Write), + ToolCatalogEntry::new(tool_names::EDIT, ToolCatalogKind::Edit), + ToolCatalogEntry::new(tool_names::GLOB, ToolCatalogKind::Glob), + ToolCatalogEntry::new(tool_names::GREP, ToolCatalogKind::Grep), + ToolCatalogEntry::new(tool_names::BASH, ToolCatalogKind::Bash), + ToolCatalogEntry::new(tool_names::WEBFETCH, ToolCatalogKind::WebFetch), + ToolCatalogEntry::new(tool_names::TODO_READ, ToolCatalogKind::TodoRead), + ToolCatalogEntry::new(tool_names::TODO_WRITE, ToolCatalogKind::TodoWrite), +]; + +/// Returns the explicit default non-Task tool catalog for SerdesAI runtimes. +pub fn default_tools() -> Vec { + // Keep the exported value data-only so later prompts can instantiate tools explicitly. + DEFAULT_TOOLS.to_vec() +} + +#[cfg(test)] +mod tests { + use super::{default_tools, ToolCatalogEntry, ToolCatalogKind}; + use llm_coding_tools_core::tool_names; + use std::collections::BTreeSet; + + #[test] + fn default_tools_match_expected_catalog() { + assert_eq!( + default_tools(), + vec![ + ToolCatalogEntry::new(tool_names::READ, ToolCatalogKind::Read), + ToolCatalogEntry::new(tool_names::WRITE, ToolCatalogKind::Write), + ToolCatalogEntry::new(tool_names::EDIT, ToolCatalogKind::Edit), + ToolCatalogEntry::new(tool_names::GLOB, ToolCatalogKind::Glob), + ToolCatalogEntry::new(tool_names::GREP, ToolCatalogKind::Grep), + ToolCatalogEntry::new(tool_names::BASH, ToolCatalogKind::Bash), + ToolCatalogEntry::new(tool_names::WEBFETCH, ToolCatalogKind::WebFetch), + ToolCatalogEntry::new(tool_names::TODO_READ, ToolCatalogKind::TodoRead), + ToolCatalogEntry::new(tool_names::TODO_WRITE, ToolCatalogKind::TodoWrite), + ], + ); + } + + #[test] + fn default_tools_exclude_task_and_keep_names_unique() { + let tools = default_tools(); + assert!(tools.iter().all(|entry| entry.name != tool_names::TASK)); + + let unique_names = tools + .iter() + .map(|entry| entry.name) + .collect::>(); + assert_eq!(unique_names.len(), tools.len()); + } +} From 4d53077e3a9cbbdee26fb8a5983dad8fb8ee27f2 Mon Sep 17 00:00:00 2001 From: Sewer56 Date: Thu, 12 Mar 2026 16:30:40 +0000 Subject: [PATCH 02/11] Added: AgentRuntime types for owned runtime state management Provides a cloneable catalog entry struct and kind enum with 9 tool variants (Read, Write, Edit, Glob, Grep, Bash, WebFetch, TodoRead, TodoWrite). Excludes Task tools to keep runtime registration lightweight. Changes: - Added llm-coding-tools-agents and llm-coding-tools-models-dev dependencies - Created ToolCatalogEntry, ToolCatalogKind, and default_tools() in serdesai crate - Exported minimal API surface for runtime tool registration - Added unit tests verifying catalog contents and Task exclusion Benefits: - Enables just-in-time tool catalog construction at runtime - Keeps runtime dependency-free from Task tool implementation - Provides explicit registration API with test coverage --- .../src/agent_runtime/builder.rs | 122 ++++++++++++++++++ .../src/agent_runtime/mod.rs | 29 +++++ .../src/agent_runtime/runtime.rs | 56 ++++++++ src/llm-coding-tools-serdesai/src/lib.rs | 2 + .../src/tool_catalog.rs | 2 +- 5 files changed, 210 insertions(+), 1 deletion(-) create mode 100644 src/llm-coding-tools-serdesai/src/agent_runtime/builder.rs create mode 100644 src/llm-coding-tools-serdesai/src/agent_runtime/mod.rs create mode 100644 src/llm-coding-tools-serdesai/src/agent_runtime/runtime.rs diff --git a/src/llm-coding-tools-serdesai/src/agent_runtime/builder.rs b/src/llm-coding-tools-serdesai/src/agent_runtime/builder.rs new file mode 100644 index 00000000..b18024cc --- /dev/null +++ b/src/llm-coding-tools-serdesai/src/agent_runtime/builder.rs @@ -0,0 +1,122 @@ +//! Builder for assembling owned agent runtime state. + +use super::runtime::{AgentDefaults, AgentRuntime}; +use crate::tool_catalog::{ToolCatalogEntry, default_tools}; +use llm_coding_tools_agents::AgentCatalog; + +/// Single assembly path for owned runtime state. +#[derive(Debug, Clone)] +pub struct AgentRuntimeBuilder { + catalog: AgentCatalog, + defaults: AgentDefaults, + tools: Vec, +} + +impl Default for AgentRuntimeBuilder { + #[inline] + fn default() -> Self { + Self::new() + } +} + +impl AgentRuntimeBuilder { + /// Creates a builder seeded with empty catalog/defaults and the default tool catalog. + #[inline] + pub fn new() -> Self { + Self { + catalog: AgentCatalog::new(), + defaults: AgentDefaults::default(), + tools: default_tools(), + } + } + + /// Replaces the owned parsed catalog. + #[inline] + pub fn catalog(mut self, catalog: AgentCatalog) -> Self { + self.catalog = catalog; + self + } + + /// Replaces the owned runtime defaults. + #[inline] + pub fn defaults(mut self, defaults: AgentDefaults) -> Self { + self.defaults = defaults; + self + } + + /// Replaces the owned tool catalog metadata. + #[inline] + pub fn tools(mut self, tools: Vec) -> Self { + self.tools = tools; + self + } + + /// Consumes the builder and returns one owned runtime. + #[inline] + pub fn build(self) -> AgentRuntime { + AgentRuntime::from_parts(self.catalog, self.defaults, self.tools) + } +} + +#[cfg(test)] +mod tests { + use super::AgentRuntimeBuilder; + use crate::agent_runtime::AgentDefaults; + use crate::tool_catalog::{ToolCatalogEntry, ToolCatalogKind, default_tools}; + use llm_coding_tools_agents::{AgentCatalog, AgentConfig, AgentMode}; + use llm_coding_tools_core::tool_names; + + fn sample_config(name: &str, model: Option<&str>) -> AgentConfig { + AgentConfig { + name: name.to_string(), + mode: AgentMode::Subagent, + description: format!("{name} description"), + model: model.map(str::to_string), + hidden: false, + temperature: Some(0.3), + top_p: Some(0.8), + permission: Default::default(), + options: Default::default(), + prompt: format!("You are {name}."), + } + } + + #[test] + fn builder_builds_runtime_from_owned_inputs() { + let catalog = AgentCatalog::from_entries([sample_config("planner", Some("openai/gpt-4o"))]); + let defaults = AgentDefaults { + model: Some("openai/gpt-4.1-mini".to_string()), + temperature: Some(0.2), + top_p: Some(0.95), + }; + let tools = vec![ + ToolCatalogEntry::new(tool_names::READ, ToolCatalogKind::Read), + ToolCatalogEntry::new(tool_names::GLOB, ToolCatalogKind::Glob), + ]; + + let runtime = AgentRuntimeBuilder::new() + .catalog(catalog) + .defaults(defaults.clone()) + .tools(tools.clone()) + .build(); + + assert_eq!( + runtime + .catalog() + .by_name("planner") + .and_then(|config| config.model.as_deref()), + Some("openai/gpt-4o"), + ); + assert_eq!(runtime.defaults(), &defaults); + assert_eq!(runtime.tools(), tools.as_slice()); + } + + #[test] + fn builder_defaults_to_empty_catalog_defaults_and_default_tools() { + let runtime = AgentRuntimeBuilder::new().build(); + + assert_eq!(runtime.catalog().iter().count(), 0); + assert_eq!(runtime.defaults(), &AgentDefaults::default()); + assert_eq!(runtime.tools(), default_tools().as_slice()); + } +} diff --git a/src/llm-coding-tools-serdesai/src/agent_runtime/mod.rs b/src/llm-coding-tools-serdesai/src/agent_runtime/mod.rs new file mode 100644 index 00000000..57fe652f --- /dev/null +++ b/src/llm-coding-tools-serdesai/src/agent_runtime/mod.rs @@ -0,0 +1,29 @@ +//! Runtime state for building SerdesAI agents on demand. +//! +//! This module keeps the public runtime API centered on two concepts: +//! - [`AgentRuntime`]: owned data used to build agents later on demand +//! - [`AgentRuntimeBuilder`]: the single obvious assembly path for runtime state +//! +//! Build an [`AgentRuntime`] using [`AgentRuntimeBuilder`]: +//! +//! ```no_run +//! use llm_coding_tools_serdesai::agent_runtime::{AgentDefaults, AgentRuntimeBuilder}; +//! use llm_coding_tools_agents::AgentCatalog; +//! +//! let runtime = AgentRuntimeBuilder::new() +//! .catalog(AgentCatalog::new()) +//! .defaults(AgentDefaults { +//! model: Some("openai/gpt-4o".to_string()), +//! temperature: Some(0.7), +//! top_p: Some(0.9), +//! }) +//! .build(); +//! ``` +//! +//! [`AgentCatalog`]: llm_coding_tools_agents::AgentCatalog + +mod builder; +mod runtime; + +pub use builder::AgentRuntimeBuilder; +pub use runtime::{AgentDefaults, AgentRuntime}; diff --git a/src/llm-coding-tools-serdesai/src/agent_runtime/runtime.rs b/src/llm-coding-tools-serdesai/src/agent_runtime/runtime.rs new file mode 100644 index 00000000..d7024bfb --- /dev/null +++ b/src/llm-coding-tools-serdesai/src/agent_runtime/runtime.rs @@ -0,0 +1,56 @@ +//! Owned runtime state for later SerdesAI agent construction. + +use crate::tool_catalog::ToolCatalogEntry; +use llm_coding_tools_agents::AgentCatalog; + +/// Runtime-wide fallback settings applied when an agent config omits them. +#[derive(Debug, Clone, Default, PartialEq)] +pub struct AgentDefaults { + /// Default model identifier in `provider/model-id` format. + pub model: Option, + /// Default sampling temperature. + pub temperature: Option, + /// Default nucleus sampling top-p. + pub top_p: Option, +} + +/// Owned runtime state used for later on-demand SerdesAI agent construction. +#[derive(Debug, Clone)] +pub struct AgentRuntime { + catalog: AgentCatalog, + defaults: AgentDefaults, + tools: Vec, +} + +impl AgentRuntime { + #[inline] + pub(super) fn from_parts( + catalog: AgentCatalog, + defaults: AgentDefaults, + tools: Vec, + ) -> Self { + Self { + catalog, + defaults, + tools, + } + } + + /// Returns the parsed catalog that remains the runtime source of truth. + #[inline] + pub fn catalog(&self) -> &AgentCatalog { + &self.catalog + } + + /// Returns the runtime fallback settings. + #[inline] + pub fn defaults(&self) -> &AgentDefaults { + &self.defaults + } + + /// Returns the owned tool-catalog metadata used for later tool materialization. + #[inline] + pub fn tools(&self) -> &[ToolCatalogEntry] { + &self.tools + } +} diff --git a/src/llm-coding-tools-serdesai/src/lib.rs b/src/llm-coding-tools-serdesai/src/lib.rs index fef286e5..8957ff6f 100644 --- a/src/llm-coding-tools-serdesai/src/lib.rs +++ b/src/llm-coding-tools-serdesai/src/lib.rs @@ -3,6 +3,7 @@ pub mod absolute; pub mod agent_ext; +pub mod agent_runtime; pub mod allowed; pub mod bash; mod common; @@ -46,6 +47,7 @@ pub use llm_coding_tools_core::{ }; // Re-export standalone tools +pub use agent_runtime::{AgentDefaults, AgentRuntime, AgentRuntimeBuilder}; pub use bash::BashTool; pub use todo::{TodoReadTool, TodoWriteTool, create_todo_tools}; pub use tool_catalog::{ToolCatalogEntry, ToolCatalogKind, default_tools}; diff --git a/src/llm-coding-tools-serdesai/src/tool_catalog.rs b/src/llm-coding-tools-serdesai/src/tool_catalog.rs index 70ce749f..fd65f694 100644 --- a/src/llm-coding-tools-serdesai/src/tool_catalog.rs +++ b/src/llm-coding-tools-serdesai/src/tool_catalog.rs @@ -70,7 +70,7 @@ pub fn default_tools() -> Vec { #[cfg(test)] mod tests { - use super::{default_tools, ToolCatalogEntry, ToolCatalogKind}; + use super::{ToolCatalogEntry, ToolCatalogKind, default_tools}; use llm_coding_tools_core::tool_names; use std::collections::BTreeSet; From 5e4c9a16a8ecd50c1e26d62b33e9bb932358844c Mon Sep 17 00:00:00 2001 From: Sewer56 Date: Thu, 12 Mar 2026 23:40:26 +0000 Subject: [PATCH 03/11] Added: Private model translation helper with unit tests Introduces model.rs module for resolving runtime defaults and per-agent model overrides through models.dev catalog lookup, providing clear error handling for configuration issues. Changes: - Added ModelTranslationError enum with 5 variants for malformed models, unknown providers/models, missing effective model, and catalog load failures - Added resolve_model_config() async function for production catalog resolution - Added resolve_model_config_with_catalog() pure function for testability - Added effective_model_parts() with correct precedence (agent override > runtime defaults) - Added 7 comprehensive unit tests covering all requirements and error paths - Added ahash and indexmap dev-dependencies for test fixtures --- src/Cargo.lock | 2 + .../src/types/config.rs | 10 +- src/llm-coding-tools-serdesai/Cargo.toml | 2 + .../src/agent_runtime/mod.rs | 1 + .../src/agent_runtime/model.rs | 512 ++++++++++++++++++ 5 files changed, 522 insertions(+), 5 deletions(-) create mode 100644 src/llm-coding-tools-serdesai/src/agent_runtime/model.rs diff --git a/src/Cargo.lock b/src/Cargo.lock index 396eadb8..40f700e8 100644 --- a/src/Cargo.lock +++ b/src/Cargo.lock @@ -1521,8 +1521,10 @@ dependencies = [ name = "llm-coding-tools-serdesai" version = "0.2.0" dependencies = [ + "ahash", "async-trait", "futures", + "indexmap", "llm-coding-tools-agents", "llm-coding-tools-core", "llm-coding-tools-models-dev", diff --git a/src/llm-coding-tools-agents/src/types/config.rs b/src/llm-coding-tools-agents/src/types/config.rs index 1d4a74ab..9a389776 100644 --- a/src/llm-coding-tools-agents/src/types/config.rs +++ b/src/llm-coding-tools-agents/src/types/config.rs @@ -138,9 +138,9 @@ pub struct AgentConfig { } impl AgentConfig { - /// Returns the configured model split into `(provider, model)` parts. + /// Returns the provider+model split into `(provider, model)` parts. #[inline] - pub fn model_parts(&self) -> Option<(&str, &str)> { + pub fn get_provider_model(&self) -> Option<(&str, &str)> { let value = self.model.as_deref()?; let (provider, model) = value.split_once('/')?; if provider.is_empty() || model.is_empty() { @@ -193,7 +193,7 @@ mod tests { let config = config_with_model(Some("synthetic/hf:moonshotai/Kimi-K2.5")); assert_eq!( - config.model_parts(), + config.get_provider_model(), Some(("synthetic", "hf:moonshotai/Kimi-K2.5")) ); } @@ -202,13 +202,13 @@ mod tests { fn model_parts_rejects_missing_separator() { let config = config_with_model(Some("synthetic-only")); - assert_eq!(config.model_parts(), None); + assert_eq!(config.get_provider_model(), None); } #[test] fn model_parts_handles_absent_model() { let config = config_with_model(None); - assert_eq!(config.model_parts(), None); + assert_eq!(config.get_provider_model(), None); } } diff --git a/src/llm-coding-tools-serdesai/Cargo.toml b/src/llm-coding-tools-serdesai/Cargo.toml index 63fbf0ff..52f7739f 100644 --- a/src/llm-coding-tools-serdesai/Cargo.toml +++ b/src/llm-coding-tools-serdesai/Cargo.toml @@ -41,3 +41,5 @@ reqwest = { version = "0.13", default-features = false, features = [ tokio = { version = "1", features = ["macros", "rt-multi-thread"] } tempfile = "3" wiremock = "0.6" +ahash = "0.8" +indexmap = "2" diff --git a/src/llm-coding-tools-serdesai/src/agent_runtime/mod.rs b/src/llm-coding-tools-serdesai/src/agent_runtime/mod.rs index 57fe652f..beb90197 100644 --- a/src/llm-coding-tools-serdesai/src/agent_runtime/mod.rs +++ b/src/llm-coding-tools-serdesai/src/agent_runtime/mod.rs @@ -23,6 +23,7 @@ //! [`AgentCatalog`]: llm_coding_tools_agents::AgentCatalog mod builder; +mod model; mod runtime; pub use builder::AgentRuntimeBuilder; diff --git a/src/llm-coding-tools-serdesai/src/agent_runtime/model.rs b/src/llm-coding-tools-serdesai/src/agent_runtime/model.rs new file mode 100644 index 00000000..72ee806b --- /dev/null +++ b/src/llm-coding-tools-serdesai/src/agent_runtime/model.rs @@ -0,0 +1,512 @@ +//! Model configuration resolution for agent runtimes. +//! +//! This module translates agent model overrides and runtime defaults into +//! [`ModelConfig`] instances by validating against the models.dev catalog. +//! It provides deterministic resolution with clear error paths for invalid +//! or unknown model identifiers. +//! +//! # Public API +//! +//! ## Resolution +//! +//! - [`resolve_model_config`] - Async entrypoint that loads models.dev and resolves the effective model +//! - [`resolve_model_config_with_catalog`] - Pure testable variant accepting a pre-loaded catalog +//! +//! ## Errors +//! +//! - [`ModelTranslationError`] - All failure cases during model resolution +//! +//! # Model Resolution Precedence +//! +//! The effective model is determined by this precedence order: +//! +//! 1. **Agent override**: `model` field in agent markdown frontmatter +//! 2. **Runtime default**: `model` field in [`AgentDefaults`] +//! +//! If neither provides a valid model identifier, resolution fails with +//! [`ModelTranslationError::MissingEffectiveModel`]. +//! +//! # Identifier Format +//! +//! Model identifiers use `provider/model-id` syntax (e.g., `openai/gpt-4o`, +//! `openrouter/anthropic/claude-3-5-sonnet`). Invalid formats (missing `/`, +//! empty segments) produce [`ModelTranslationError::MalformedModelIdentifier`]. +//! +//! # Validation +//! +//! Resolved identifiers are validated against the models.dev catalog: +//! +//! - Unknown providers produce [`ModelTranslationError::UnknownProvider`] +//! - Unknown models produce [`ModelTranslationError::UnknownModel`] +//! - Catalog load failures produce [`ModelTranslationError::CatalogLoad`] +//! +//! # Usage +//! +//! Call [`resolve_model_config`] with agent defaults and config to obtain a +//! validated [`ModelConfig`]. The returned `model_config.spec` uses `provider:model-id` +//! format suitable for SerdesAI agent builders. +//! +//! For unit tests, use [`resolve_model_config_with_catalog`] with a synthetic catalog +//! to avoid network dependencies. +//! +//! [`ModelConfig`]: serdes_ai::ModelConfig +//! [`AgentDefaults`]: crate::agent_runtime::AgentDefaults + +#![allow(dead_code)] + +use crate::AgentDefaults; +use llm_coding_tools_agents::AgentConfig; +use llm_coding_tools_core::models::ModelCatalog; +use llm_coding_tools_models_dev::{CatalogError, ModelsDevCatalog}; +use serdes_ai::ModelConfig; + +/// Error type for model translation failures. +/// +/// This enum covers all error cases when resolving and validating model +/// identifiers from agent configs and runtime defaults. +#[derive(Debug)] +pub(super) enum ModelTranslationError { + MalformedModelIdentifier { + agent: String, + location: &'static str, + model: String, + }, + MissingEffectiveModel { + agent: String, + }, + UnknownProvider { + agent: String, + provider: String, + }, + UnknownModel { + agent: String, + provider: String, + model: String, + }, + CatalogLoad(CatalogError), +} + +impl core::fmt::Display for ModelTranslationError { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + match self { + Self::MalformedModelIdentifier { + agent, + location, + model, + } => write!( + f, + "agent `{agent}` has malformed {location} model `{model}`; expected `provider/model-id`", + ), + Self::MissingEffectiveModel { agent } => write!( + f, + "agent `{agent}` does not define a model override and runtime defaults do not define one either", + ), + Self::UnknownProvider { agent, provider } => { + write!( + f, + "agent `{agent}` references unknown provider `{provider}`" + ) + } + Self::UnknownModel { + agent, + provider, + model, + } => write!( + f, + "agent `{agent}` references unknown model `{provider}/{model}`", + ), + Self::CatalogLoad(source) => write!(f, "failed to load models.dev catalog: {source}"), + } + } +} + +impl std::error::Error for ModelTranslationError { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match self { + Self::CatalogLoad(source) => Some(source), + _ => None, + } + } +} + +/// Resolves the effective model configuration for an agent. +/// +/// Loads the models.dev catalog and delegates to the pure testable helper. +pub(super) async fn resolve_model_config( + defaults: &AgentDefaults, + agent: &AgentConfig, +) -> Result { + let load_result = ModelsDevCatalog::load() + .await + .map_err(ModelTranslationError::CatalogLoad)?; + resolve_model_config_with_catalog(&load_result.catalog, defaults, agent) +} + +struct ProviderModel<'a> { + provider: &'a str, + model: &'a str, +} + +/// Pure function for resolving model configuration with a provided catalog. +/// +/// Enables unit testing without network access by accepting a synthetic catalog. +fn resolve_model_config_with_catalog( + catalog: &ModelCatalog, + defaults: &AgentDefaults, + agent: &AgentConfig, +) -> Result { + let parts = get_provider_model(defaults, agent)?; + + if catalog.lookup_provider(parts.provider).is_none() { + return Err(ModelTranslationError::UnknownProvider { + agent: agent.name.clone(), + provider: parts.provider.to_string(), + }); + } + + if catalog + .lookup_provider_model(parts.provider, parts.model) + .is_none() + { + return Err(ModelTranslationError::UnknownModel { + agent: agent.name.clone(), + provider: parts.provider.to_string(), + model: parts.model.to_string(), + }); + } + + Ok(ModelConfig::new(format!( + "{}:{}", + parts.provider, parts.model + ))) +} + +/// Determines the effective model parts by applying override precedence. +/// +/// Agent override takes precedence over runtime defaults. Returns an error +/// if neither provides a valid model identifier. +fn get_provider_model<'a>( + defaults: &'a AgentDefaults, + agent: &'a AgentConfig, +) -> Result, ModelTranslationError> { + if let Some(raw) = agent.model.as_deref() { + let (provider, model) = agent.get_provider_model().ok_or_else(|| { + ModelTranslationError::MalformedModelIdentifier { + agent: agent.name.clone(), + location: "agent override", + model: raw.to_string(), + } + })?; + return Ok(ProviderModel { provider, model }); + } + + if let Some(raw) = defaults.model.as_deref() { + let parts = parse_model_parts(raw).ok_or_else(|| { + ModelTranslationError::MalformedModelIdentifier { + agent: agent.name.clone(), + location: "runtime default", + model: raw.to_string(), + } + })?; + return Ok(parts); + } + + Err(ModelTranslationError::MissingEffectiveModel { + agent: agent.name.clone(), + }) +} + +/// Parses a model identifier into `(provider, model)` parts. +/// +/// Mirrors `AgentConfig::model_parts()` semantics for runtime defaults. +/// Returns `None` if the value lacks a `/` separator or has empty segments. +fn parse_model_parts(value: &str) -> Option> { + let (provider, model) = value.split_once('/')?; + if provider.is_empty() || model.is_empty() { + return None; + } + + Some(ProviderModel { provider, model }) +} + +#[cfg(test)] +mod tests { + use super::{ModelTranslationError, resolve_model_config_with_catalog}; + use crate::agent_runtime::AgentDefaults; + use ahash::AHashMap; + use indexmap::IndexMap; + use llm_coding_tools_agents::{AgentConfig, AgentMode}; + use llm_coding_tools_core::models::{ + Modality, ModelCatalog, ModelInfo, ProviderIdx, ProviderInfo, ProviderModelSource, + ProviderSource, ProviderType, + }; + + fn config_with_model(name: &str, model: Option<&str>) -> AgentConfig { + AgentConfig { + name: name.to_string(), + mode: AgentMode::All, + description: String::new(), + model: model.map(str::to_string), + hidden: false, + temperature: None, + top_p: None, + permission: IndexMap::new(), + options: AHashMap::new(), + prompt: String::new(), + } + } + + fn provider(api_url: &str, env_vars: &[&str], api_type: ProviderType) -> ProviderInfo { + ProviderInfo { + api_url: api_url.to_string(), + env_vars: env_vars.iter().map(|value| (*value).to_string()).collect(), + api_type, + } + } + + fn model_info(max_input: u32, max_output: u32) -> ModelInfo { + ModelInfo { + modalities: Modality::TEXT, + max_input, + max_output, + temperature: Some(0.2), + top_p: Some(0.95), + } + } + + fn build_catalog( + providers: Vec<(&str, ProviderInfo)>, + provider_models: Vec<(&str, &str, ModelInfo)>, + ) -> ModelCatalog { + let provider_sources: Vec = providers + .into_iter() + .map(|(key, info)| ProviderSource::new(key, info)) + .collect(); + let provider_model_sources: Vec> = provider_models + .into_iter() + .map(|(provider_key, model_key, info)| { + let provider_idx = ProviderIdx::new( + provider_sources + .iter() + .position(|provider| provider.provider_key == provider_key) + .expect("provider key should exist") as u16, + ); + ProviderModelSource::new(provider_idx, model_key, info) + }) + .collect(); + + ModelCatalog::build(&provider_sources, &provider_model_sources) + .expect("catalog fixture should build") + } + + #[test] + fn resolves_runtime_default_when_agent_has_no_override() { + let catalog = build_catalog( + vec![( + "openai", + provider( + "https://api.openai.com/v1", + &["OPENAI_API_KEY"], + ProviderType::OpenAiResponses, + ), + )], + vec![("openai", "gpt-4.1-mini", model_info(128_000, 16_384))], + ); + let defaults = AgentDefaults { + model: Some("openai/gpt-4.1-mini".to_string()), + temperature: None, + top_p: None, + }; + let agent = config_with_model("planner", None); + + let resolved = resolve_model_config_with_catalog(&catalog, &defaults, &agent) + .expect("runtime default should resolve"); + + assert_eq!(resolved.spec, "openai:gpt-4.1-mini"); + } + + #[test] + fn agent_override_wins_over_runtime_default() { + let catalog = build_catalog( + vec![( + "openrouter", + provider( + "https://openrouter.ai/api/v1", + &["OPENROUTER_API_KEY"], + ProviderType::OpenRouter, + ), + )], + vec![ + ( + "openrouter", + "openai/gpt-4.1-mini", + model_info(128_000, 16_384), + ), + ("openrouter", "openai/gpt-4o", model_info(128_000, 16_384)), + ], + ); + let defaults = AgentDefaults { + model: Some("openrouter/openai/gpt-4.1-mini".to_string()), + temperature: None, + top_p: None, + }; + let agent = config_with_model("planner", Some("openrouter/openai/gpt-4o")); + + let resolved = resolve_model_config_with_catalog(&catalog, &defaults, &agent) + .expect("override should resolve"); + + assert_eq!(resolved.spec, "openrouter:openai/gpt-4o"); + } + + #[test] + fn malformed_agent_override_does_not_fall_back_to_defaults() { + let catalog = build_catalog( + vec![( + "openai", + provider( + "https://api.openai.com/v1", + &["OPENAI_API_KEY"], + ProviderType::OpenAiResponses, + ), + )], + vec![("openai", "gpt-4.1-mini", model_info(128_000, 16_384))], + ); + let defaults = AgentDefaults { + model: Some("openai/gpt-4.1-mini".to_string()), + temperature: None, + top_p: None, + }; + let agent = config_with_model("planner", Some("openai-only")); + + let err = resolve_model_config_with_catalog(&catalog, &defaults, &agent) + .expect_err("malformed override should fail"); + + match err { + ModelTranslationError::MalformedModelIdentifier { + location, model, .. + } => { + assert_eq!(location, "agent override"); + assert_eq!(model, "openai-only"); + } + other => panic!("unexpected error: {other}"), + } + } + + #[test] + fn unknown_provider_returns_specific_error() { + let catalog = build_catalog( + vec![( + "openai", + provider( + "https://api.openai.com/v1", + &["OPENAI_API_KEY"], + ProviderType::OpenAiResponses, + ), + )], + vec![("openai", "gpt-4o", model_info(128_000, 16_384))], + ); + let defaults = AgentDefaults::default(); + let agent = config_with_model("planner", Some("anthropic/claude-3-5-sonnet")); + + let err = resolve_model_config_with_catalog(&catalog, &defaults, &agent) + .expect_err("missing provider should fail"); + + match err { + ModelTranslationError::UnknownProvider { provider, .. } => { + assert_eq!(provider, "anthropic"); + } + other => panic!("unexpected error: {other}"), + } + } + + #[test] + fn unknown_model_returns_specific_error() { + let catalog = build_catalog( + vec![( + "openai", + provider( + "https://api.openai.com/v1", + &["OPENAI_API_KEY"], + ProviderType::OpenAiResponses, + ), + )], + vec![("openai", "gpt-4o", model_info(128_000, 16_384))], + ); + let defaults = AgentDefaults::default(); + let agent = config_with_model("planner", Some("openai/gpt-4.1-mini")); + + let err = resolve_model_config_with_catalog(&catalog, &defaults, &agent) + .expect_err("missing provider/model pair should fail"); + + match err { + ModelTranslationError::UnknownModel { + provider, model, .. + } => { + assert_eq!(provider, "openai"); + assert_eq!(model, "gpt-4.1-mini"); + } + other => panic!("unexpected error: {other}"), + } + } + + #[test] + fn missing_agent_override_and_runtime_default_returns_dedicated_error() { + let catalog = build_catalog( + vec![( + "openai", + provider( + "https://api.openai.com/v1", + &["OPENAI_API_KEY"], + ProviderType::OpenAiResponses, + ), + )], + vec![("openai", "gpt-4o", model_info(128_000, 16_384))], + ); + let defaults = AgentDefaults::default(); + let agent = config_with_model("planner", None); + + let err = resolve_model_config_with_catalog(&catalog, &defaults, &agent) + .expect_err("missing effective model should fail"); + + match err { + ModelTranslationError::MissingEffectiveModel { agent } => { + assert_eq!(agent, "planner"); + } + other => panic!("unexpected error: {other}"), + } + } + + #[test] + fn malformed_runtime_default_returns_clear_error() { + let catalog = build_catalog( + vec![( + "openai", + provider( + "https://api.openai.com/v1", + &["OPENAI_API_KEY"], + ProviderType::OpenAiResponses, + ), + )], + vec![("openai", "gpt-4o", model_info(128_000, 16_384))], + ); + let defaults = AgentDefaults { + model: Some("openai-only".to_string()), + temperature: None, + top_p: None, + }; + let agent = config_with_model("planner", None); + + let err = resolve_model_config_with_catalog(&catalog, &defaults, &agent) + .expect_err("malformed runtime default should fail"); + + match err { + ModelTranslationError::MalformedModelIdentifier { + location, model, .. + } => { + assert_eq!(location, "runtime default"); + assert_eq!(model, "openai-only"); + } + other => panic!("unexpected error: {other}"), + } + } +} From 0991a3e1d9ccf7f0792a3984d54cec2d6205f056 Mon Sep 17 00:00:00 2001 From: Sewer56 Date: Fri, 13 Mar 2026 01:37:05 +0000 Subject: [PATCH 04/11] Refactored: move generic agent runtime into llm-coding-tools-agents Make llm-coding-tools-agents the canonical home for reusable runtime state, tool metadata, and model resolution so framework adapters can build agents on demand without duplicating generic logic. --- src/llm-coding-tools-agents/README.md | 60 ++-- src/llm-coding-tools-agents/src/catalog.rs | 21 +- src/llm-coding-tools-agents/src/lib.rs | 9 +- src/llm-coding-tools-agents/src/loader.rs | 48 ++- src/llm-coding-tools-agents/src/parser/mod.rs | 8 +- .../src/runtime}/builder.rs | 27 +- .../src/runtime/mod.rs | 46 +++ .../src/runtime}/model.rs | 289 +++++++++--------- .../src/runtime/state.rs} | 24 +- .../src/runtime}/tool_catalog.rs | 9 +- .../src/types/config.rs | 88 ++++-- src/llm-coding-tools-agents/src/types/mod.rs | 2 +- .../src/agent_runtime/mod.rs | 30 -- src/llm-coding-tools-serdesai/src/lib.rs | 4 - 14 files changed, 372 insertions(+), 293 deletions(-) rename src/{llm-coding-tools-serdesai/src/agent_runtime => llm-coding-tools-agents/src/runtime}/builder.rs (81%) create mode 100644 src/llm-coding-tools-agents/src/runtime/mod.rs rename src/{llm-coding-tools-serdesai/src/agent_runtime => llm-coding-tools-agents/src/runtime}/model.rs (61%) rename src/{llm-coding-tools-serdesai/src/agent_runtime/runtime.rs => llm-coding-tools-agents/src/runtime/state.rs} (62%) rename src/{llm-coding-tools-serdesai/src => llm-coding-tools-agents/src/runtime}/tool_catalog.rs (90%) delete mode 100644 src/llm-coding-tools-serdesai/src/agent_runtime/mod.rs diff --git a/src/llm-coding-tools-agents/README.md b/src/llm-coding-tools-agents/README.md index e6d6bb85..2595f56c 100644 --- a/src/llm-coding-tools-agents/README.md +++ b/src/llm-coding-tools-agents/README.md @@ -1,21 +1,16 @@ # llm-coding-tools-agents -Load OpenCode agent markdown files into a typed Rust catalogue. +Load OpenCode agent markdown files into Rust. -This crate is a loader for the [OpenCode agent schema](https://opencode.ai/docs/agents/). +This crate reads agent definitions from markdown files with YAML frontmatter, +following the [OpenCode agent schema](https://opencode.ai/docs/agents/). -It is a drop-in replacement for OpenCode agent files: agents you create for -OpenCode should load here unchanged. +Agents you create for OpenCode work here unchanged. -## What it provides +## Loading agents -- [`AgentLoader`] for loading agent configs from directories, files, or - in-memory markdown. -- [`AgentCatalog`] for storing and looking up loaded [`AgentConfig`] entries. -- [`RulesetExt`] for converting frontmatter `permission` data into runtime - [`Ruleset`]s. - -## Quick start +Use [`AgentLoader`] to read agent files from a directory, then store them in +an [`AgentCatalog`] for lookup by name: ```rust,no_run use llm_coding_tools_agents::{AgentCatalog, AgentLoader}; @@ -51,6 +46,35 @@ For field behaviour, see OpenCode docs for [`model`](https://opencode.ai/docs/agents#model), and [`permissions`](https://opencode.ai/docs/agents#permissions). +## Building agents + +Framework adapters (like `llm-coding-tools-serdesai`) use [`AgentRuntime`] to +build runnable agents. An `AgentRuntime` bundles your loaded agents with default +settings and available tools: + +```rust,no_run +use llm_coding_tools_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 { + model: Some("openai/gpt-4o-mini".into()), + temperature: Some(0.2), + top_p: Some(0.95), + }) + // .tools(my_custom_tools) // optional; defaults to read/write/edit/glob/grep/bash/webfetch/todo + .build(); + +// Pass `runtime` to your framework adapter to build agents by name +# Ok::<(), llm_coding_tools_agents::AgentLoadError>(()) +``` + ## Compatibility notes This library does not provide interactive UX extensions (for example, TUI @@ -65,15 +89,3 @@ while settings with no runtime effect are accepted and ignored: ([docs](https://opencode.ai/docs/permissions#what-ask-does)). - [`hidden`](https://opencode.ai/docs/agents#hidden) is accepted for compatibility, but ignored at runtime. - -## Integration - -This crate only loads and validates agent configs. -Pass [`AgentCatalog`] to your runtime adapter (for example, -`llm-coding-tools-serdesai`) to build registries and Task tooling. - -If you want to validate `model` strings against a catalog, call -[`AgentConfig::model_parts`] and pass the returned `(provider, model)` into -your lookup layer. - -[`Ruleset`]: llm_coding_tools_core::permissions::Ruleset diff --git a/src/llm-coding-tools-agents/src/catalog.rs b/src/llm-coding-tools-agents/src/catalog.rs index 2a764b8e..82b70cfe 100644 --- a/src/llm-coding-tools-agents/src/catalog.rs +++ b/src/llm-coding-tools-agents/src/catalog.rs @@ -71,7 +71,7 @@ impl AgentCatalog { /// - [`Option::Some`] with the previous [`AgentConfig`] if the name already existed. /// - [`Option::None`] if the name was not present. pub(crate) fn insert(&mut self, config: AgentConfig) -> Option { - self.agents.insert(config.name.clone(), config) + self.agents.insert(config.name.to_string(), config) } /// Creates a catalog from an iterator of agent configurations. @@ -84,7 +84,10 @@ impl AgentCatalog { /// - If duplicate names exist, the last entry for each name is retained. pub fn from_entries(entries: impl IntoIterator) -> Self { Self { - agents: entries.into_iter().map(|c| (c.name.clone(), c)).collect(), + agents: entries + .into_iter() + .map(|c| (c.name.to_string(), c)) + .collect(), } } } @@ -100,31 +103,31 @@ mod tests { fn catalog_iter_and_by_name() { let mut catalog = AgentCatalog::new(); catalog.insert(AgentConfig { - name: "alpha".to_string(), + name: "alpha".into(), mode: AgentMode::Subagent, - description: String::new(), + description: Default::default(), model: None, hidden: false, temperature: None, top_p: None, permission: IndexMap::new(), options: AHashMap::new(), - prompt: String::new(), + prompt: Default::default(), }); catalog.insert(AgentConfig { - name: "beta".to_string(), + name: "beta".into(), mode: AgentMode::Subagent, - description: String::new(), + description: Default::default(), model: None, hidden: false, temperature: None, top_p: None, permission: IndexMap::new(), options: AHashMap::new(), - prompt: String::new(), + prompt: Default::default(), }); - let names: Vec<_> = catalog.iter().map(|config| config.name.as_str()).collect(); + let names: Vec<_> = catalog.iter().map(|config| &*config.name).collect(); assert!(names.contains(&"alpha")); assert!(names.contains(&"beta")); assert!(catalog.by_name("beta").is_some()); diff --git a/src/llm-coding-tools-agents/src/lib.rs b/src/llm-coding-tools-agents/src/lib.rs index 84c07887..875a1f5f 100644 --- a/src/llm-coding-tools-agents/src/lib.rs +++ b/src/llm-coding-tools-agents/src/lib.rs @@ -4,10 +4,17 @@ mod catalog; mod extensions; mod loader; mod parser; +mod runtime; mod types; pub use catalog::AgentCatalog; pub use extensions::RulesetExt; pub use loader::AgentLoader; pub use parser::AgentParseError; -pub use types::{AgentConfig, AgentLoadError, AgentLoadResult, AgentMode, PermissionRule}; +pub use runtime::{ + default_tools, resolve_model_with_catalog, AgentDefaults, AgentRuntime, AgentRuntimeBuilder, + ModelResolutionError, ResolvedModel, ToolCatalogEntry, ToolCatalogKind, +}; +pub use types::{ + parse_model_parts, AgentConfig, AgentLoadError, AgentLoadResult, AgentMode, PermissionRule, +}; diff --git a/src/llm-coding-tools-agents/src/loader.rs b/src/llm-coding-tools-agents/src/loader.rs index 1ad1c7a7..155391ad 100644 --- a/src/llm-coding-tools-agents/src/loader.rs +++ b/src/llm-coding-tools-agents/src/loader.rs @@ -115,7 +115,7 @@ impl AgentLoader { ) -> AgentLoadResult<()> { let dir = directory.into(); load_directory_with(&dir, |path, name| { - match load_agent_file(path, name.to_string()) { + match load_agent_file(path, name) { Ok(config) => { catalog.insert(config); } @@ -169,7 +169,7 @@ impl AgentLoader { &self, catalog: &mut AgentCatalog, path: impl Into, - name: impl Into, + name: impl Into>, ) -> AgentLoadResult<()> { let path = path.into(); let override_name = name.into(); @@ -179,7 +179,7 @@ impl AgentLoader { "agent name is empty", )); } - let mut config = load_agent_file(&path, String::new())?; + let mut config = load_agent_file(&path, Box::default())?; config.name = override_name; catalog.insert(config); Ok(()) @@ -221,7 +221,7 @@ impl AgentLoader { &self, catalog: &mut AgentCatalog, markdown: impl Into, - default_name: impl Into, + default_name: impl Into>, ) -> AgentLoadResult<()> { let config = config_from_str_strict(markdown, default_name)?; catalog.insert(config); @@ -242,7 +242,7 @@ impl AgentLoader { &self, catalog: &mut AgentCatalog, bytes: impl AsRef<[u8]>, - default_name: impl Into, + default_name: impl Into>, ) -> AgentLoadResult<()> { let content = std::str::from_utf8(bytes.as_ref()).map_err(|err| { AgentLoadError::schema_validation(None, format!("invalid UTF-8: {err}")) @@ -318,7 +318,7 @@ fn load_directory_with( /// Shared parse helper that reuses existing loader parsing. fn parse_agent_config( content: String, - default_name: String, + default_name: impl Into>, ) -> Result { let result = parse_agent::(content)?; Ok(AgentConfig::from_raw( @@ -338,21 +338,19 @@ fn map_parse_error(path: Option, err: AgentParseError) -> AgentLoadErro } /// Loads a single agent configuration from a file. -fn load_agent_file(path: &Path, name: String) -> AgentLoadResult { +fn load_agent_file(path: &Path, name: impl Into>) -> AgentLoadResult { let content = fs::read_to_string(path).map_err(|e| AgentLoadError::io(Some(path.to_path_buf()), e))?; - parse_agent_config(content, name).map_err(|err| map_parse_error(Some(path.to_path_buf()), err)) } /// Strict parser for catalog-only string loading (validates non-empty name). fn config_from_str_strict( markdown: impl Into, - default_name: impl Into, + default_name: impl Into>, ) -> AgentLoadResult { - let name = default_name.into(); - let config = - parse_agent_config(markdown.into(), name).map_err(|err| map_parse_error(None, err))?; + let config = parse_agent_config(markdown.into(), default_name) + .map_err(|err| map_parse_error(None, err))?; if config.name.is_empty() { return Err(AgentLoadError::schema_validation( None, @@ -492,8 +490,8 @@ mod tests { loader.add_directory(&mut catalog, dir.path()).unwrap(); assert!(catalog.by_name("test-agent").is_some()); - assert_eq!(catalog.by_name("test-agent").unwrap().description, "Test"); - assert_eq!(catalog.by_name("test-agent").unwrap().prompt, "Prompt"); + assert_eq!(&*catalog.by_name("test-agent").unwrap().description, "Test"); + assert_eq!(&*catalog.by_name("test-agent").unwrap().prompt, "Prompt"); } #[test] @@ -617,8 +615,8 @@ mod tests { loader.add_directory(&mut catalog, dir.path()).unwrap(); assert_eq!( - catalog.by_name("test").unwrap().model, - Some("provider/model:tag".to_string()) + catalog.by_name("test").unwrap().model.as_deref(), + Some("provider/model:tag") ); } @@ -672,16 +670,16 @@ mod tests { fn make_agent(name: &str, description: &str) -> AgentConfig { AgentConfig { - name: name.to_string(), + name: name.into(), mode: AgentMode::Subagent, - description: description.to_string(), + description: description.into(), model: None, hidden: false, temperature: None, top_p: None, permission: IndexMap::new(), options: AHashMap::new(), - prompt: String::new(), + prompt: Default::default(), } } @@ -772,7 +770,7 @@ mod tests { .add_config(&mut catalog, make_agent("agent", "Second")) .unwrap(); - assert_eq!(catalog.by_name("agent").unwrap().description, "Second"); + assert_eq!(&*catalog.by_name("agent").unwrap().description, "Second"); } #[test] @@ -811,7 +809,7 @@ mod tests { .unwrap(); let agent = catalog.by_name("explicit").unwrap(); - assert_eq!(agent.description, "Explicit"); + assert_eq!(&*agent.description, "Explicit"); } #[test] @@ -859,7 +857,7 @@ mod tests { .add_config(&mut catalog, make_agent("override", "new")) .unwrap(); - assert_eq!(catalog.by_name("override").unwrap().description, "new"); + assert_eq!(&*catalog.by_name("override").unwrap().description, "new"); } // ========== String/Bytes Tests ========== @@ -881,7 +879,7 @@ mod tests { .unwrap(); let agent = catalog.by_name("string-agent").unwrap(); - assert_eq!(agent.description, "From string"); + assert_eq!(&*agent.description, "From string"); } #[test] @@ -1013,7 +1011,7 @@ mod tests { let agent = catalog.by_name("no-mode").unwrap(); assert_eq!(agent.mode, AgentMode::All); - assert_eq!(agent.description, "Test agent"); + assert_eq!(&*agent.description, "Test agent"); } #[test] @@ -1161,6 +1159,6 @@ mod tests { .unwrap(); let agent = catalog.by_name("hidden-agent").unwrap(); assert!(agent.hidden); - assert_eq!(agent.description, "Hidden agent"); + assert_eq!(&*agent.description, "Hidden agent"); } } diff --git a/src/llm-coding-tools-agents/src/parser/mod.rs b/src/llm-coding-tools-agents/src/parser/mod.rs index 03919b1d..3f6c4a85 100644 --- a/src/llm-coding-tools-agents/src/parser/mod.rs +++ b/src/llm-coding-tools-agents/src/parser/mod.rs @@ -262,7 +262,7 @@ mod tests { }; let result: AgentParseResult = parse_agent(input.to_string()).unwrap(); - assert_eq!(result.data.description, "Test agent".to_string()); + assert_eq!(&*result.data.description, "Test agent"); assert_eq!(result.content, "Prompt body here."); } @@ -409,7 +409,7 @@ mod tests { let result: AgentParseResult = parse_agent(input.to_string()).unwrap(); // Model should NOT have trailing newline - assert_eq!(result.data.model, Some("provider/model:tag".to_string())); + assert_eq!(result.data.model.as_deref(), Some("provider/model:tag")); } #[test] @@ -483,7 +483,7 @@ mod tests { body" }; let result: AgentParseResult = parse_agent(input.to_string()).unwrap(); - assert_eq!(result.data.description, "Test"); + assert_eq!(&*result.data.description, "Test"); } #[test] @@ -496,7 +496,7 @@ mod tests { body" }; let result: AgentParseResult = parse_agent(input.to_string()).unwrap(); - assert_eq!(result.data.description, "Test"); + assert_eq!(&*result.data.description, "Test"); assert!(result.data.hidden); } } diff --git a/src/llm-coding-tools-serdesai/src/agent_runtime/builder.rs b/src/llm-coding-tools-agents/src/runtime/builder.rs similarity index 81% rename from src/llm-coding-tools-serdesai/src/agent_runtime/builder.rs rename to src/llm-coding-tools-agents/src/runtime/builder.rs index b18024cc..c822f496 100644 --- a/src/llm-coding-tools-serdesai/src/agent_runtime/builder.rs +++ b/src/llm-coding-tools-agents/src/runtime/builder.rs @@ -1,8 +1,13 @@ //! Builder for assembling owned agent runtime state. +//! +//! # Public API +//! +//! - [`AgentRuntimeBuilder`]: Builder for constructing [`AgentRuntime`] with +//! custom catalog, defaults, and tool catalog. -use super::runtime::{AgentDefaults, AgentRuntime}; -use crate::tool_catalog::{ToolCatalogEntry, default_tools}; -use llm_coding_tools_agents::AgentCatalog; +use super::state::{AgentDefaults, AgentRuntime}; +use super::tool_catalog::{default_tools, ToolCatalogEntry}; +use crate::AgentCatalog; /// Single assembly path for owned runtime state. #[derive(Debug, Clone)] @@ -61,23 +66,23 @@ impl AgentRuntimeBuilder { #[cfg(test)] mod tests { use super::AgentRuntimeBuilder; - use crate::agent_runtime::AgentDefaults; - use crate::tool_catalog::{ToolCatalogEntry, ToolCatalogKind, default_tools}; - use llm_coding_tools_agents::{AgentCatalog, AgentConfig, AgentMode}; + use crate::runtime::tool_catalog::{default_tools, ToolCatalogEntry, ToolCatalogKind}; + use crate::runtime::AgentDefaults; + use crate::{AgentCatalog, AgentConfig, AgentMode}; use llm_coding_tools_core::tool_names; fn sample_config(name: &str, model: Option<&str>) -> AgentConfig { AgentConfig { - name: name.to_string(), + name: name.into(), mode: AgentMode::Subagent, - description: format!("{name} description"), - model: model.map(str::to_string), + description: format!("{name} description").into(), + model: model.map(Into::into), hidden: false, temperature: Some(0.3), top_p: Some(0.8), permission: Default::default(), options: Default::default(), - prompt: format!("You are {name}."), + prompt: format!("You are {name}.").into(), } } @@ -85,7 +90,7 @@ mod tests { fn builder_builds_runtime_from_owned_inputs() { let catalog = AgentCatalog::from_entries([sample_config("planner", Some("openai/gpt-4o"))]); let defaults = AgentDefaults { - model: Some("openai/gpt-4.1-mini".to_string()), + model: Some("openai/gpt-4.1-mini".into()), temperature: Some(0.2), top_p: Some(0.95), }; diff --git a/src/llm-coding-tools-agents/src/runtime/mod.rs b/src/llm-coding-tools-agents/src/runtime/mod.rs new file mode 100644 index 00000000..a535bd93 --- /dev/null +++ b/src/llm-coding-tools-agents/src/runtime/mod.rs @@ -0,0 +1,46 @@ +//! Generic JIT runtime foundation for agent construction. +//! +//! This module provides framework-agnostic runtime types used for on-demand +//! agent construction. Framework adapters (like `llm-coding-tools-serdesai`) +//! consume these types and add concrete execution/build behavior. +//! +//! # Public API +//! +//! - [`AgentDefaults`] - Runtime-wide fallback settings +//! - [`AgentRuntime`] - Owned runtime state for later agent construction +//! - [`AgentRuntimeBuilder`] - Builder for assembling runtime state +//! - [`ToolCatalogEntry`] - Cloneable metadata for a runtime tool +//! - [`ToolCatalogKind`] - Tool variants supported by the default surface +//! - [`default_tools()`] - Returns the default non-Task tool catalog +//! - [`ResolvedModel`] - A resolved and validated model identifier +//! - [`ModelResolutionError`] - Error type for model resolution failures +//! - [`resolve_model_with_catalog`] - Resolves the effective model for an agent +//! +//! # Usage +//! +//! Build an [`AgentRuntime`] using [`AgentRuntimeBuilder`]: +//! +//! ```no_run +//! use llm_coding_tools_agents::{AgentCatalog, AgentDefaults, AgentRuntimeBuilder}; +//! +//! let runtime = AgentRuntimeBuilder::new() +//! .catalog(AgentCatalog::new()) +//! .defaults(AgentDefaults { +//! model: Some("openai/gpt-4o".into()), +//! temperature: Some(0.7), +//! top_p: Some(0.9), +//! }) +//! .build(); +//! +//! assert!(runtime.catalog().iter().count() == 0); +//! ``` + +mod builder; +mod model; +mod state; +mod tool_catalog; + +pub use builder::AgentRuntimeBuilder; +pub use model::{resolve_model_with_catalog, ModelResolutionError, ResolvedModel}; +pub use state::{AgentDefaults, AgentRuntime}; +pub use tool_catalog::{default_tools, ToolCatalogEntry, ToolCatalogKind}; diff --git a/src/llm-coding-tools-serdesai/src/agent_runtime/model.rs b/src/llm-coding-tools-agents/src/runtime/model.rs similarity index 61% rename from src/llm-coding-tools-serdesai/src/agent_runtime/model.rs rename to src/llm-coding-tools-agents/src/runtime/model.rs index 72ee806b..be0a3356 100644 --- a/src/llm-coding-tools-serdesai/src/agent_runtime/model.rs +++ b/src/llm-coding-tools-agents/src/runtime/model.rs @@ -1,7 +1,7 @@ -//! Model configuration resolution for agent runtimes. +//! Generic model configuration resolution for agent runtimes. //! -//! This module translates agent model overrides and runtime defaults into -//! [`ModelConfig`] instances by validating against the models.dev catalog. +//! This module provides framework-agnostic model resolution that validates +//! agent model overrides and runtime defaults against a provided catalog. //! It provides deterministic resolution with clear error paths for invalid //! or unknown model identifiers. //! @@ -9,12 +9,11 @@ //! //! ## Resolution //! -//! - [`resolve_model_config`] - Async entrypoint that loads models.dev and resolves the effective model -//! - [`resolve_model_config_with_catalog`] - Pure testable variant accepting a pre-loaded catalog +//! - [`resolve_model_with_catalog`] - Pure function that resolves the effective model //! //! ## Errors //! -//! - [`ModelTranslationError`] - All failure cases during model resolution +//! - [`ModelResolutionError`] - All failure cases during model resolution //! //! # Model Resolution Precedence //! @@ -24,69 +23,103 @@ //! 2. **Runtime default**: `model` field in [`AgentDefaults`] //! //! If neither provides a valid model identifier, resolution fails with -//! [`ModelTranslationError::MissingEffectiveModel`]. +//! [`ModelResolutionError::MissingEffectiveModel`]. //! //! # Identifier Format //! //! Model identifiers use `provider/model-id` syntax (e.g., `openai/gpt-4o`, //! `openrouter/anthropic/claude-3-5-sonnet`). Invalid formats (missing `/`, -//! empty segments) produce [`ModelTranslationError::MalformedModelIdentifier`]. +//! empty segments) produce [`ModelResolutionError::MalformedModelIdentifier`]. //! //! # Validation //! -//! Resolved identifiers are validated against the models.dev catalog: +//! Resolved identifiers are validated against a provided [`ModelCatalog`]: //! -//! - Unknown providers produce [`ModelTranslationError::UnknownProvider`] -//! - Unknown models produce [`ModelTranslationError::UnknownModel`] -//! - Catalog load failures produce [`ModelTranslationError::CatalogLoad`] +//! - Unknown providers produce [`ModelResolutionError::UnknownProvider`] +//! - Unknown models produce [`ModelResolutionError::UnknownModel`] //! //! # Usage //! -//! Call [`resolve_model_config`] with agent defaults and config to obtain a -//! validated [`ModelConfig`]. The returned `model_config.spec` uses `provider:model-id` -//! format suitable for SerdesAI agent builders. +//! Call [`resolve_model_with_catalog`] with a catalog, agent defaults, and config +//! to obtain a validated [`ResolvedModel`]. Framework adapters can then convert +//! the resolved model into framework-specific model configuration. //! -//! For unit tests, use [`resolve_model_config_with_catalog`] with a synthetic catalog -//! to avoid network dependencies. -//! -//! [`ModelConfig`]: serdes_ai::ModelConfig -//! [`AgentDefaults`]: crate::agent_runtime::AgentDefaults - -#![allow(dead_code)] +//! [`AgentDefaults`]: super::state::AgentDefaults +//! [`ModelCatalog`]: llm_coding_tools_core::models::ModelCatalog -use crate::AgentDefaults; -use llm_coding_tools_agents::AgentConfig; +use crate::AgentConfig; use llm_coding_tools_core::models::ModelCatalog; -use llm_coding_tools_models_dev::{CatalogError, ModelsDevCatalog}; -use serdes_ai::ModelConfig; -/// Error type for model translation failures. +/// A resolved and validated model identifier. +/// +/// This value type represents a model that has been validated against +/// a model catalog. Framework adapters convert this into their specific +/// model configuration types. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ResolvedModel { + provider: Box, + model: Box, +} + +impl ResolvedModel { + /// Returns the provider identifier. + #[inline] + pub fn provider(&self) -> &str { + &self.provider + } + + /// Returns the model identifier within the provider. + #[inline] + pub fn model(&self) -> &str { + &self.model + } + + /// Returns the slash-formatted spec: `provider/model-id`. + #[inline] + pub fn slash_spec(&self) -> String { + format!("{}/{}", self.provider, self.model) + } +} + +/// Error type for model resolution failures. /// /// This enum covers all error cases when resolving and validating model /// identifiers from agent configs and runtime defaults. #[derive(Debug)] -pub(super) enum ModelTranslationError { +pub enum ModelResolutionError { + /// Model identifier is malformed (missing `/` or empty segments). MalformedModelIdentifier { - agent: String, + /// Agent name for error context. + agent: Box, + /// Source of the malformed identifier. location: &'static str, - model: String, + /// The raw malformed identifier. + model: Box, }, + /// Neither agent override nor runtime default provides a model. MissingEffectiveModel { - agent: String, + /// Agent name for error context. + agent: Box, }, + /// Provider is not found in the catalog. UnknownProvider { - agent: String, - provider: String, + /// Agent name for error context. + agent: Box, + /// The unknown provider identifier. + provider: Box, }, + /// Model is not found for the given provider in the catalog. UnknownModel { - agent: String, - provider: String, - model: String, + /// Agent name for error context. + agent: Box, + /// Provider identifier. + provider: Box, + /// Model identifier within the provider. + model: Box, }, - CatalogLoad(CatalogError), } -impl core::fmt::Display for ModelTranslationError { +impl core::fmt::Display for ModelResolutionError { fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { match self { Self::MalformedModelIdentifier { @@ -115,70 +148,53 @@ impl core::fmt::Display for ModelTranslationError { f, "agent `{agent}` references unknown model `{provider}/{model}`", ), - Self::CatalogLoad(source) => write!(f, "failed to load models.dev catalog: {source}"), } } } -impl std::error::Error for ModelTranslationError { - fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { - match self { - Self::CatalogLoad(source) => Some(source), - _ => None, - } - } -} +impl std::error::Error for ModelResolutionError {} -/// Resolves the effective model configuration for an agent. +/// Resolves the effective model for an agent using a provided catalog. /// -/// Loads the models.dev catalog and delegates to the pure testable helper. -pub(super) async fn resolve_model_config( - defaults: &AgentDefaults, - agent: &AgentConfig, -) -> Result { - let load_result = ModelsDevCatalog::load() - .await - .map_err(ModelTranslationError::CatalogLoad)?; - resolve_model_config_with_catalog(&load_result.catalog, defaults, agent) -} - -struct ProviderModel<'a> { - provider: &'a str, - model: &'a str, -} - -/// Pure function for resolving model configuration with a provided catalog. +/// This is the primary entrypoint for model resolution. It applies the +/// precedence rules (agent override first, then runtime default) and +/// validates the result against the catalog. +/// +/// # Arguments +/// +/// * `catalog` - Model catalog for validation +/// * `defaults` - Runtime-wide fallback settings +/// * `agent` - Agent configuration being resolved /// -/// Enables unit testing without network access by accepting a synthetic catalog. -fn resolve_model_config_with_catalog( +/// # Returns +/// +/// A [`ResolvedModel`] on success, or a [`ModelResolutionError`] on failure. +pub fn resolve_model_with_catalog( catalog: &ModelCatalog, - defaults: &AgentDefaults, + defaults: &super::state::AgentDefaults, agent: &AgentConfig, -) -> Result { - let parts = get_provider_model(defaults, agent)?; +) -> Result { + let (provider, model) = get_provider_model(defaults, agent)?; - if catalog.lookup_provider(parts.provider).is_none() { - return Err(ModelTranslationError::UnknownProvider { + if catalog.lookup_provider(provider).is_none() { + return Err(ModelResolutionError::UnknownProvider { agent: agent.name.clone(), - provider: parts.provider.to_string(), + provider: provider.into(), }); } - if catalog - .lookup_provider_model(parts.provider, parts.model) - .is_none() - { - return Err(ModelTranslationError::UnknownModel { + if catalog.lookup_provider_model(provider, model).is_none() { + return Err(ModelResolutionError::UnknownModel { agent: agent.name.clone(), - provider: parts.provider.to_string(), - model: parts.model.to_string(), + provider: provider.into(), + model: model.into(), }); } - Ok(ModelConfig::new(format!( - "{}:{}", - parts.provider, parts.model - ))) + Ok(ResolvedModel { + provider: provider.into(), + model: model.into(), + }) } /// Determines the effective model parts by applying override precedence. @@ -186,73 +202,59 @@ fn resolve_model_config_with_catalog( /// Agent override takes precedence over runtime defaults. Returns an error /// if neither provides a valid model identifier. fn get_provider_model<'a>( - defaults: &'a AgentDefaults, + defaults: &'a super::state::AgentDefaults, agent: &'a AgentConfig, -) -> Result, ModelTranslationError> { +) -> Result<(&'a str, &'a str), ModelResolutionError> { if let Some(raw) = agent.model.as_deref() { - let (provider, model) = agent.get_provider_model().ok_or_else(|| { - ModelTranslationError::MalformedModelIdentifier { + let (provider, model) = crate::parse_model_parts(raw).ok_or_else(|| { + ModelResolutionError::MalformedModelIdentifier { agent: agent.name.clone(), location: "agent override", - model: raw.to_string(), + model: raw.into(), } })?; - return Ok(ProviderModel { provider, model }); + return Ok((provider, model)); } if let Some(raw) = defaults.model.as_deref() { - let parts = parse_model_parts(raw).ok_or_else(|| { - ModelTranslationError::MalformedModelIdentifier { + let (provider, model) = crate::parse_model_parts(raw).ok_or_else(|| { + ModelResolutionError::MalformedModelIdentifier { agent: agent.name.clone(), location: "runtime default", - model: raw.to_string(), + model: raw.into(), } })?; - return Ok(parts); + return Ok((provider, model)); } - Err(ModelTranslationError::MissingEffectiveModel { + Err(ModelResolutionError::MissingEffectiveModel { agent: agent.name.clone(), }) } -/// Parses a model identifier into `(provider, model)` parts. -/// -/// Mirrors `AgentConfig::model_parts()` semantics for runtime defaults. -/// Returns `None` if the value lacks a `/` separator or has empty segments. -fn parse_model_parts(value: &str) -> Option> { - let (provider, model) = value.split_once('/')?; - if provider.is_empty() || model.is_empty() { - return None; - } - - Some(ProviderModel { provider, model }) -} - #[cfg(test)] mod tests { - use super::{ModelTranslationError, resolve_model_config_with_catalog}; - use crate::agent_runtime::AgentDefaults; + use super::{resolve_model_with_catalog, ModelResolutionError}; + use crate::runtime::AgentDefaults; use ahash::AHashMap; use indexmap::IndexMap; - use llm_coding_tools_agents::{AgentConfig, AgentMode}; use llm_coding_tools_core::models::{ Modality, ModelCatalog, ModelInfo, ProviderIdx, ProviderInfo, ProviderModelSource, ProviderSource, ProviderType, }; - fn config_with_model(name: &str, model: Option<&str>) -> AgentConfig { - AgentConfig { - name: name.to_string(), - mode: AgentMode::All, - description: String::new(), - model: model.map(str::to_string), + fn config_with_model(name: &str, model: Option<&str>) -> crate::AgentConfig { + crate::AgentConfig { + name: name.into(), + mode: crate::AgentMode::All, + description: Default::default(), + model: model.map(Into::into), hidden: false, temperature: None, top_p: None, permission: IndexMap::new(), options: AHashMap::new(), - prompt: String::new(), + prompt: Default::default(), } } @@ -313,16 +315,18 @@ mod tests { vec![("openai", "gpt-4.1-mini", model_info(128_000, 16_384))], ); let defaults = AgentDefaults { - model: Some("openai/gpt-4.1-mini".to_string()), + model: Some("openai/gpt-4.1-mini".into()), temperature: None, top_p: None, }; let agent = config_with_model("planner", None); - let resolved = resolve_model_config_with_catalog(&catalog, &defaults, &agent) + let resolved = resolve_model_with_catalog(&catalog, &defaults, &agent) .expect("runtime default should resolve"); - assert_eq!(resolved.spec, "openai:gpt-4.1-mini"); + assert_eq!(resolved.provider(), "openai"); + assert_eq!(resolved.model(), "gpt-4.1-mini"); + assert_eq!(resolved.slash_spec(), "openai/gpt-4.1-mini"); } #[test] @@ -346,16 +350,17 @@ mod tests { ], ); let defaults = AgentDefaults { - model: Some("openrouter/openai/gpt-4.1-mini".to_string()), + model: Some("openrouter/openai/gpt-4.1-mini".into()), temperature: None, top_p: None, }; let agent = config_with_model("planner", Some("openrouter/openai/gpt-4o")); - let resolved = resolve_model_config_with_catalog(&catalog, &defaults, &agent) + let resolved = resolve_model_with_catalog(&catalog, &defaults, &agent) .expect("override should resolve"); - assert_eq!(resolved.spec, "openrouter:openai/gpt-4o"); + assert_eq!(resolved.provider(), "openrouter"); + assert_eq!(resolved.model(), "openai/gpt-4o"); } #[test] @@ -372,21 +377,21 @@ mod tests { vec![("openai", "gpt-4.1-mini", model_info(128_000, 16_384))], ); let defaults = AgentDefaults { - model: Some("openai/gpt-4.1-mini".to_string()), + model: Some("openai/gpt-4.1-mini".into()), temperature: None, top_p: None, }; let agent = config_with_model("planner", Some("openai-only")); - let err = resolve_model_config_with_catalog(&catalog, &defaults, &agent) + let err = resolve_model_with_catalog(&catalog, &defaults, &agent) .expect_err("malformed override should fail"); match err { - ModelTranslationError::MalformedModelIdentifier { + ModelResolutionError::MalformedModelIdentifier { location, model, .. } => { assert_eq!(location, "agent override"); - assert_eq!(model, "openai-only"); + assert_eq!(&*model, "openai-only"); } other => panic!("unexpected error: {other}"), } @@ -408,12 +413,12 @@ mod tests { let defaults = AgentDefaults::default(); let agent = config_with_model("planner", Some("anthropic/claude-3-5-sonnet")); - let err = resolve_model_config_with_catalog(&catalog, &defaults, &agent) + let err = resolve_model_with_catalog(&catalog, &defaults, &agent) .expect_err("missing provider should fail"); match err { - ModelTranslationError::UnknownProvider { provider, .. } => { - assert_eq!(provider, "anthropic"); + ModelResolutionError::UnknownProvider { provider, .. } => { + assert_eq!(&*provider, "anthropic"); } other => panic!("unexpected error: {other}"), } @@ -435,15 +440,15 @@ mod tests { let defaults = AgentDefaults::default(); let agent = config_with_model("planner", Some("openai/gpt-4.1-mini")); - let err = resolve_model_config_with_catalog(&catalog, &defaults, &agent) + let err = resolve_model_with_catalog(&catalog, &defaults, &agent) .expect_err("missing provider/model pair should fail"); match err { - ModelTranslationError::UnknownModel { + ModelResolutionError::UnknownModel { provider, model, .. } => { - assert_eq!(provider, "openai"); - assert_eq!(model, "gpt-4.1-mini"); + assert_eq!(&*provider, "openai"); + assert_eq!(&*model, "gpt-4.1-mini"); } other => panic!("unexpected error: {other}"), } @@ -465,12 +470,12 @@ mod tests { let defaults = AgentDefaults::default(); let agent = config_with_model("planner", None); - let err = resolve_model_config_with_catalog(&catalog, &defaults, &agent) + let err = resolve_model_with_catalog(&catalog, &defaults, &agent) .expect_err("missing effective model should fail"); match err { - ModelTranslationError::MissingEffectiveModel { agent } => { - assert_eq!(agent, "planner"); + ModelResolutionError::MissingEffectiveModel { agent } => { + assert_eq!(&*agent, "planner"); } other => panic!("unexpected error: {other}"), } @@ -490,21 +495,21 @@ mod tests { vec![("openai", "gpt-4o", model_info(128_000, 16_384))], ); let defaults = AgentDefaults { - model: Some("openai-only".to_string()), + model: Some("openai-only".into()), temperature: None, top_p: None, }; let agent = config_with_model("planner", None); - let err = resolve_model_config_with_catalog(&catalog, &defaults, &agent) + let err = resolve_model_with_catalog(&catalog, &defaults, &agent) .expect_err("malformed runtime default should fail"); match err { - ModelTranslationError::MalformedModelIdentifier { + ModelResolutionError::MalformedModelIdentifier { location, model, .. } => { assert_eq!(location, "runtime default"); - assert_eq!(model, "openai-only"); + assert_eq!(&*model, "openai-only"); } other => panic!("unexpected error: {other}"), } diff --git a/src/llm-coding-tools-serdesai/src/agent_runtime/runtime.rs b/src/llm-coding-tools-agents/src/runtime/state.rs similarity index 62% rename from src/llm-coding-tools-serdesai/src/agent_runtime/runtime.rs rename to src/llm-coding-tools-agents/src/runtime/state.rs index d7024bfb..447608b9 100644 --- a/src/llm-coding-tools-serdesai/src/agent_runtime/runtime.rs +++ b/src/llm-coding-tools-agents/src/runtime/state.rs @@ -1,20 +1,30 @@ -//! Owned runtime state for later SerdesAI agent construction. +//! Owned runtime state for later agent construction. +//! +//! This module provides the core [`AgentRuntime`] type that holds catalog, +//! defaults, and tool metadata for on-demand agent materialization. Framework +//! adapters consume this state and add concrete execution/build behavior. +//! +//! # Public API +//! +//! - [`AgentDefaults`] - Runtime-wide fallback settings applied when agent +//! configs omit them +//! - [`AgentRuntime`] - Owned runtime state used for later agent construction -use crate::tool_catalog::ToolCatalogEntry; -use llm_coding_tools_agents::AgentCatalog; +use super::tool_catalog::ToolCatalogEntry; +use crate::AgentCatalog; /// Runtime-wide fallback settings applied when an agent config omits them. #[derive(Debug, Clone, Default, PartialEq)] pub struct AgentDefaults { /// Default model identifier in `provider/model-id` format. - pub model: Option, + pub model: Option>, /// Default sampling temperature. - pub temperature: Option, + pub temperature: Option, /// Default nucleus sampling top-p. - pub top_p: Option, + pub top_p: Option, } -/// Owned runtime state used for later on-demand SerdesAI agent construction. +/// Owned runtime state used for later on-demand agent construction. #[derive(Debug, Clone)] pub struct AgentRuntime { catalog: AgentCatalog, diff --git a/src/llm-coding-tools-serdesai/src/tool_catalog.rs b/src/llm-coding-tools-agents/src/runtime/tool_catalog.rs similarity index 90% rename from src/llm-coding-tools-serdesai/src/tool_catalog.rs rename to src/llm-coding-tools-agents/src/runtime/tool_catalog.rs index fd65f694..7350cba3 100644 --- a/src/llm-coding-tools-serdesai/src/tool_catalog.rs +++ b/src/llm-coding-tools-agents/src/runtime/tool_catalog.rs @@ -1,4 +1,4 @@ -//! Explicit default runtime tool catalog for SerdesAI agent builds. +//! Explicit default runtime tool catalog for agent builds. //! //! This module provides a cloneable, data-only tool catalog used during JIT agent //! construction. Each [`ToolCatalogEntry`] pairs a canonical tool name with a @@ -27,7 +27,7 @@ impl ToolCatalogEntry { } } -/// Explicit tool variants supported by the default SerdesAI runtime surface. +/// Explicit tool variants supported by the default runtime surface. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum ToolCatalogKind { /// Read file contents tool. @@ -62,15 +62,14 @@ const DEFAULT_TOOLS: [ToolCatalogEntry; 9] = [ ToolCatalogEntry::new(tool_names::TODO_WRITE, ToolCatalogKind::TodoWrite), ]; -/// Returns the explicit default non-Task tool catalog for SerdesAI runtimes. +/// Returns the explicit default non-Task tool catalog for runtimes. pub fn default_tools() -> Vec { - // Keep the exported value data-only so later prompts can instantiate tools explicitly. DEFAULT_TOOLS.to_vec() } #[cfg(test)] mod tests { - use super::{ToolCatalogEntry, ToolCatalogKind, default_tools}; + use super::{default_tools, ToolCatalogEntry, ToolCatalogKind}; use llm_coding_tools_core::tool_names; use std::collections::BTreeSet; diff --git a/src/llm-coding-tools-agents/src/types/config.rs b/src/llm-coding-tools-agents/src/types/config.rs index 9a389776..d12f43fc 100644 --- a/src/llm-coding-tools-agents/src/types/config.rs +++ b/src/llm-coding-tools-agents/src/types/config.rs @@ -72,21 +72,21 @@ impl Default for PermissionRule { #[derive(Debug, Clone, Deserialize)] pub(crate) struct RawFrontmatter { #[serde(default)] - pub name: Option, + pub name: Option>, #[serde(default)] pub mode: AgentMode, - pub description: String, + pub description: Box, #[serde(default)] - pub model: Option, + pub model: Option>, /// Legacy visibility flag accepted for compatibility only. /// /// Runtime behavior in headless mode ignores this field. #[serde(default)] pub hidden: bool, #[serde(default)] - pub temperature: Option, + pub temperature: Option, #[serde(default)] - pub top_p: Option, + pub top_p: Option, #[serde(default)] pub permission: IndexMap, #[serde(default)] @@ -100,18 +100,18 @@ pub struct AgentConfig { /// /// This comes from frontmatter `name` when present; otherwise a loader- /// provided default (for example, derived from a file path) is used. - pub name: String, + pub name: Box, /// Execution mode. #[serde(default)] pub mode: AgentMode, /// Human-readable description. #[serde(default)] - pub description: String, + pub description: Box, /// Optional model override (format: "provider/model-id"). /// - /// Use [`AgentConfig::model_parts`] before catalog lookup. + /// Use [`AgentConfig::get_provider_model`] before catalog lookup. #[serde(default)] - pub model: Option, + pub model: Option>, /// Legacy visibility flag accepted for compatibility only. /// /// Runtime behavior in headless mode ignores this field. @@ -119,10 +119,10 @@ pub struct AgentConfig { pub hidden: bool, /// Temperature for sampling. #[serde(default)] - pub temperature: Option, + pub temperature: Option, /// Top-p for nucleus sampling. #[serde(default)] - pub top_p: Option, + pub top_p: Option, /// Tool permissions map. #[serde(default)] pub permission: IndexMap, @@ -134,26 +134,37 @@ pub struct AgentConfig { /// The parser stores this with LF line endings and trims surrounding ASCII /// whitespace. #[serde(skip)] - pub prompt: String, + pub prompt: Box, } impl AgentConfig { - /// Returns the provider+model split into `(provider, model)` parts. + /// Returns the provider and model identifier from [`AgentConfig::model`]. + /// + /// Delegates to [`parse_model_parts`] for parsing. + /// + /// ## Expected Format + /// `"provider/model-id"` (e.g., `"openai/gpt-4"`, `"synthetic/hf:moonshotai/Kimi-K2.5"`). + /// + /// ## Returns + /// - `Some(("provider", "model-id"))` on valid input + /// - `None` if [`AgentConfig::model`] is unset or malformed (missing `/` or empty segments) #[inline] pub fn get_provider_model(&self) -> Option<(&str, &str)> { - let value = self.model.as_deref()?; - let (provider, model) = value.split_once('/')?; - if provider.is_empty() || model.is_empty() { - return None; - } - - Some((provider, model)) + self.model.as_deref().and_then(parse_model_parts) } /// Creates an [`AgentConfig`] from raw frontmatter and parsed prompt body. - pub(crate) fn from_raw(default_name: String, raw: RawFrontmatter, prompt: String) -> Self { + pub(crate) fn from_raw( + default_name: impl Into>, + raw: RawFrontmatter, + prompt: impl Into>, + ) -> Self { + let name = match raw.name { + Some(s) => s, + None => default_name.into(), + }; Self { - name: raw.name.unwrap_or(default_name), + name, mode: raw.mode, description: raw.description, model: raw.model, @@ -162,11 +173,28 @@ impl AgentConfig { top_p: raw.top_p, permission: raw.permission, options: raw.options, - prompt, + prompt: prompt.into(), } } } +/// Parses a model identifier string into `(provider, model)` parts. +/// +/// ## Expected Format +/// `"provider/model-id"` (e.g., `"openai/gpt-4"`, `"synthetic/hf:moonshotai/Kimi-K2.5"`). +/// +/// ## Returns +/// - `Some(("provider", "model-id"))` on valid input +/// - `None` if the value lacks a `/` separator or has empty segments +#[inline] +pub fn parse_model_parts(value: &str) -> Option<(&str, &str)> { + let (provider, model) = value.split_once('/')?; + if provider.is_empty() || model.is_empty() { + return None; + } + Some((provider, model)) +} + #[cfg(test)] mod tests { use super::{AgentConfig, AgentMode}; @@ -175,21 +203,21 @@ mod tests { fn config_with_model(model: Option<&str>) -> AgentConfig { AgentConfig { - name: "example".to_string(), + name: "example".into(), mode: AgentMode::All, - description: String::new(), - model: model.map(str::to_string), + description: Default::default(), + model: model.map(Into::into), hidden: false, temperature: None, top_p: None, permission: IndexMap::new(), options: AHashMap::new(), - prompt: String::new(), + prompt: Default::default(), } } #[test] - fn model_parts_returns_provider_and_model() { + fn get_provider_model_returns_provider_and_model() { let config = config_with_model(Some("synthetic/hf:moonshotai/Kimi-K2.5")); assert_eq!( @@ -199,14 +227,14 @@ mod tests { } #[test] - fn model_parts_rejects_missing_separator() { + fn get_provider_model_rejects_missing_separator() { let config = config_with_model(Some("synthetic-only")); assert_eq!(config.get_provider_model(), None); } #[test] - fn model_parts_handles_absent_model() { + fn get_provider_model_handles_absent_model() { let config = config_with_model(None); assert_eq!(config.get_provider_model(), None); diff --git a/src/llm-coding-tools-agents/src/types/mod.rs b/src/llm-coding-tools-agents/src/types/mod.rs index 24fd665b..0b3de3ca 100644 --- a/src/llm-coding-tools-agents/src/types/mod.rs +++ b/src/llm-coding-tools-agents/src/types/mod.rs @@ -9,7 +9,7 @@ mod config; mod error; -pub use config::{AgentConfig, AgentMode, PermissionRule}; +pub use config::{parse_model_parts, AgentConfig, AgentMode, PermissionRule}; pub use error::{AgentLoadError, AgentLoadResult}; pub(crate) use config::RawFrontmatter; diff --git a/src/llm-coding-tools-serdesai/src/agent_runtime/mod.rs b/src/llm-coding-tools-serdesai/src/agent_runtime/mod.rs deleted file mode 100644 index beb90197..00000000 --- a/src/llm-coding-tools-serdesai/src/agent_runtime/mod.rs +++ /dev/null @@ -1,30 +0,0 @@ -//! Runtime state for building SerdesAI agents on demand. -//! -//! This module keeps the public runtime API centered on two concepts: -//! - [`AgentRuntime`]: owned data used to build agents later on demand -//! - [`AgentRuntimeBuilder`]: the single obvious assembly path for runtime state -//! -//! Build an [`AgentRuntime`] using [`AgentRuntimeBuilder`]: -//! -//! ```no_run -//! use llm_coding_tools_serdesai::agent_runtime::{AgentDefaults, AgentRuntimeBuilder}; -//! use llm_coding_tools_agents::AgentCatalog; -//! -//! let runtime = AgentRuntimeBuilder::new() -//! .catalog(AgentCatalog::new()) -//! .defaults(AgentDefaults { -//! model: Some("openai/gpt-4o".to_string()), -//! temperature: Some(0.7), -//! top_p: Some(0.9), -//! }) -//! .build(); -//! ``` -//! -//! [`AgentCatalog`]: llm_coding_tools_agents::AgentCatalog - -mod builder; -mod model; -mod runtime; - -pub use builder::AgentRuntimeBuilder; -pub use runtime::{AgentDefaults, AgentRuntime}; diff --git a/src/llm-coding-tools-serdesai/src/lib.rs b/src/llm-coding-tools-serdesai/src/lib.rs index 8957ff6f..9e9e3e9e 100644 --- a/src/llm-coding-tools-serdesai/src/lib.rs +++ b/src/llm-coding-tools-serdesai/src/lib.rs @@ -3,13 +3,11 @@ pub mod absolute; pub mod agent_ext; -pub mod agent_runtime; pub mod allowed; pub mod bash; mod common; pub mod convert; pub mod todo; -pub mod tool_catalog; pub mod webfetch; /// Re-export core types for convenience. @@ -47,8 +45,6 @@ pub use llm_coding_tools_core::{ }; // Re-export standalone tools -pub use agent_runtime::{AgentDefaults, AgentRuntime, AgentRuntimeBuilder}; pub use bash::BashTool; pub use todo::{TodoReadTool, TodoWriteTool, create_todo_tools}; -pub use tool_catalog::{ToolCatalogEntry, ToolCatalogKind, default_tools}; pub use webfetch::WebFetchTool; From daad5bf3183b2f0b90ac3ac3ed2d798b8b3af2d6 Mon Sep 17 00:00:00 2001 From: Sewer56 Date: Fri, 13 Mar 2026 01:53:53 +0000 Subject: [PATCH 05/11] Changed: Rewrite runtime module docs in plain language Replace jargon-heavy documentation with clear, user-focused descriptions while maintaining proper structure with Public API sections. Changes: - Replaced terms like "JIT runtime foundation", "on-demand agent materialization", "framework-agnostic" with plain language - Added Public API sections grouped by role (Runtime construction, Tools, Model resolution) - Added focused headings: Precedence, Identifier Format, Validation - Shortened doc comments on methods and types to be direct Benefits: - Easier for newcomers to understand what the module does - Still follows documentation standards with proper structure - README-style accessibility in API docs --- src/llm-coding-tools-agents/src/parser/mod.rs | 2 +- .../src/runtime/builder.rs | 19 ++-- .../src/runtime/mod.rs | 32 +++--- .../src/runtime/model.rs | 105 +++++++----------- .../src/runtime/state.rs | 24 ++-- .../src/runtime/tool_catalog.rs | 48 ++++---- .../src/fs/blocking_impl.rs | 2 +- .../src/fs/tokio_impl.rs | 2 +- 8 files changed, 99 insertions(+), 135 deletions(-) diff --git a/src/llm-coding-tools-agents/src/parser/mod.rs b/src/llm-coding-tools-agents/src/parser/mod.rs index 3f6c4a85..1cce8948 100644 --- a/src/llm-coding-tools-agents/src/parser/mod.rs +++ b/src/llm-coding-tools-agents/src/parser/mod.rs @@ -126,7 +126,7 @@ fn validate_headless_compatibility(frontmatter: &Value) -> Result<(), AgentParse return Ok(()); }; - // Reject "ask" — requires interactive user confirmation + // Reject "ask" - requires interactive user confirmation if task_rule_contains_ask(task_rule) { return Err(AgentParseError::SchemaValidation { message: "permission.task: ask is unsupported; use allow or deny".to_string(), diff --git a/src/llm-coding-tools-agents/src/runtime/builder.rs b/src/llm-coding-tools-agents/src/runtime/builder.rs index c822f496..0396cd57 100644 --- a/src/llm-coding-tools-agents/src/runtime/builder.rs +++ b/src/llm-coding-tools-agents/src/runtime/builder.rs @@ -1,15 +1,10 @@ -//! Builder for assembling owned agent runtime state. -//! -//! # Public API -//! -//! - [`AgentRuntimeBuilder`]: Builder for constructing [`AgentRuntime`] with -//! custom catalog, defaults, and tool catalog. +//! Builds an [`AgentRuntime`] from your agents, defaults, and tools. use super::state::{AgentDefaults, AgentRuntime}; use super::tool_catalog::{default_tools, ToolCatalogEntry}; use crate::AgentCatalog; -/// Single assembly path for owned runtime state. +/// Builds an [`AgentRuntime`] step by step. #[derive(Debug, Clone)] pub struct AgentRuntimeBuilder { catalog: AgentCatalog, @@ -25,7 +20,7 @@ impl Default for AgentRuntimeBuilder { } impl AgentRuntimeBuilder { - /// Creates a builder seeded with empty catalog/defaults and the default tool catalog. + /// Creates a builder with empty catalog, empty defaults, and the standard tool set. #[inline] pub fn new() -> Self { Self { @@ -35,28 +30,28 @@ impl AgentRuntimeBuilder { } } - /// Replaces the owned parsed catalog. + /// Sets the agent catalog. #[inline] pub fn catalog(mut self, catalog: AgentCatalog) -> Self { self.catalog = catalog; self } - /// Replaces the owned runtime defaults. + /// Sets the default settings. #[inline] pub fn defaults(mut self, defaults: AgentDefaults) -> Self { self.defaults = defaults; self } - /// Replaces the owned tool catalog metadata. + /// Sets the available tools. #[inline] pub fn tools(mut self, tools: Vec) -> Self { self.tools = tools; self } - /// Consumes the builder and returns one owned runtime. + /// Finishes building and returns the [`AgentRuntime`]. #[inline] pub fn build(self) -> AgentRuntime { AgentRuntime::from_parts(self.catalog, self.defaults, self.tools) diff --git a/src/llm-coding-tools-agents/src/runtime/mod.rs b/src/llm-coding-tools-agents/src/runtime/mod.rs index a535bd93..1d0581aa 100644 --- a/src/llm-coding-tools-agents/src/runtime/mod.rs +++ b/src/llm-coding-tools-agents/src/runtime/mod.rs @@ -1,24 +1,26 @@ -//! Generic JIT runtime foundation for agent construction. +//! Build agents with tools and default settings. //! -//! This module provides framework-agnostic runtime types used for on-demand -//! agent construction. Framework adapters (like `llm-coding-tools-serdesai`) -//! consume these types and add concrete execution/build behavior. +//! This module holds everything you need to prepare agents for use: +//! loaded agent definitions, default settings, and available tools. //! //! # Public API //! -//! - [`AgentDefaults`] - Runtime-wide fallback settings -//! - [`AgentRuntime`] - Owned runtime state for later agent construction -//! - [`AgentRuntimeBuilder`] - Builder for assembling runtime state -//! - [`ToolCatalogEntry`] - Cloneable metadata for a runtime tool -//! - [`ToolCatalogKind`] - Tool variants supported by the default surface -//! - [`default_tools()`] - Returns the default non-Task tool catalog -//! - [`ResolvedModel`] - A resolved and validated model identifier -//! - [`ModelResolutionError`] - Error type for model resolution failures -//! - [`resolve_model_with_catalog`] - Resolves the effective model for an agent +//! Runtime construction: +//! - [`AgentRuntime`] - Your agents plus their default settings and tools +//! - [`AgentRuntimeBuilder`] - Builds an [`AgentRuntime`] +//! - [`AgentDefaults`] - Default model, temperature, and top-p when agents don't specify them //! -//! # Usage +//! 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) //! -//! Build an [`AgentRuntime`] using [`AgentRuntimeBuilder`]: +//! Model resolution: +//! - [`ResolvedModel`] - A model identifier that's been validated against your catalog +//! - [`resolve_model_with_catalog()`] - Picks which model an agent will use +//! - [`ModelResolutionError`] - Errors when model selection fails +//! +//! # Example //! //! ```no_run //! use llm_coding_tools_agents::{AgentCatalog, AgentDefaults, AgentRuntimeBuilder}; diff --git a/src/llm-coding-tools-agents/src/runtime/model.rs b/src/llm-coding-tools-agents/src/runtime/model.rs index be0a3356..f44c6636 100644 --- a/src/llm-coding-tools-agents/src/runtime/model.rs +++ b/src/llm-coding-tools-agents/src/runtime/model.rs @@ -1,48 +1,31 @@ -//! Generic model configuration resolution for agent runtimes. +//! Picks which model an agent uses. //! -//! This module provides framework-agnostic model resolution that validates -//! agent model overrides and runtime defaults against a provided catalog. -//! It provides deterministic resolution with clear error paths for invalid -//! or unknown model identifiers. +//! An agent can specify its own model, or fall back to the runtime default. +//! This module validates that the chosen model exists in your catalog. //! //! # Public API //! -//! ## Resolution +//! - [`resolve_model_with_catalog()`] - Picks which model an agent will use +//! - [`ResolvedModel`] - A model identifier that's been validated +//! - [`ModelResolutionError`] - Errors when model selection fails //! -//! - [`resolve_model_with_catalog`] - Pure function that resolves the effective model +//! # Precedence //! -//! ## Errors -//! -//! - [`ModelResolutionError`] - All failure cases during model resolution -//! -//! # Model Resolution Precedence -//! -//! The effective model is determined by this precedence order: -//! -//! 1. **Agent override**: `model` field in agent markdown frontmatter -//! 2. **Runtime default**: `model` field in [`AgentDefaults`] -//! -//! If neither provides a valid model identifier, resolution fails with -//! [`ModelResolutionError::MissingEffectiveModel`]. +//! 1. If the agent's markdown file specifies a model, use that +//! 2. Otherwise, use the default from [`AgentDefaults`] +//! 3. If neither is set, return [`ModelResolutionError::MissingEffectiveModel`] //! //! # Identifier Format //! -//! Model identifiers use `provider/model-id` syntax (e.g., `openai/gpt-4o`, -//! `openrouter/anthropic/claude-3-5-sonnet`). Invalid formats (missing `/`, -//! empty segments) produce [`ModelResolutionError::MalformedModelIdentifier`]. +//! Models use `provider/model-id` format, like `openai/gpt-4o` or +//! `openrouter/anthropic/claude-3-5-sonnet`. Invalid formats (missing `/` +//! or empty segments) produce [`ModelResolutionError::MalformedModelIdentifier`]. //! //! # Validation //! -//! Resolved identifiers are validated against a provided [`ModelCatalog`]: -//! -//! - Unknown providers produce [`ModelResolutionError::UnknownProvider`] -//! - Unknown models produce [`ModelResolutionError::UnknownModel`] -//! -//! # Usage -//! -//! Call [`resolve_model_with_catalog`] with a catalog, agent defaults, and config -//! to obtain a validated [`ResolvedModel`]. Framework adapters can then convert -//! the resolved model into framework-specific model configuration. +//! The resolved model is validated against a [`ModelCatalog`]: +//! - Unknown provider → [`ModelResolutionError::UnknownProvider`] +//! - Unknown model for that provider → [`ModelResolutionError::UnknownModel`] //! //! [`AgentDefaults`]: super::state::AgentDefaults //! [`ModelCatalog`]: llm_coding_tools_core::models::ModelCatalog @@ -50,11 +33,10 @@ use crate::AgentConfig; use llm_coding_tools_core::models::ModelCatalog; -/// A resolved and validated model identifier. +/// A model identifier that's been validated against your catalog. /// -/// This value type represents a model that has been validated against -/// a model catalog. Framework adapters convert this into their specific -/// model configuration types. +/// Use [`provider()`][`Self::provider()`] and [`model()`][`Self::model()`] to get the +/// parts, or [`slash_spec()`][`Self::slash_spec()`] for the combined `provider/model-id` string. #[derive(Debug, Clone, PartialEq, Eq)] pub struct ResolvedModel { provider: Box, @@ -62,59 +44,56 @@ pub struct ResolvedModel { } impl ResolvedModel { - /// Returns the provider identifier. + /// Returns the provider (e.g., `openai`). #[inline] pub fn provider(&self) -> &str { &self.provider } - /// Returns the model identifier within the provider. + /// Returns the model name within the provider. #[inline] pub fn model(&self) -> &str { &self.model } - /// Returns the slash-formatted spec: `provider/model-id`. + /// Returns `provider/model-id` format. #[inline] pub fn slash_spec(&self) -> String { format!("{}/{}", self.provider, self.model) } } -/// Error type for model resolution failures. -/// -/// This enum covers all error cases when resolving and validating model -/// identifiers from agent configs and runtime defaults. +/// Errors when picking or validating a model. #[derive(Debug)] pub enum ModelResolutionError { - /// Model identifier is malformed (missing `/` or empty segments). + /// Model string is malformed (missing `/` or empty parts). MalformedModelIdentifier { /// Agent name for error context. agent: Box, - /// Source of the malformed identifier. + /// Where the bad model string came from. location: &'static str, - /// The raw malformed identifier. + /// The malformed model string. model: Box, }, - /// Neither agent override nor runtime default provides a model. + /// Neither the agent nor the runtime default specifies a model. MissingEffectiveModel { /// Agent name for error context. agent: Box, }, - /// Provider is not found in the catalog. + /// The provider isn't in the catalog. UnknownProvider { /// Agent name for error context. agent: Box, - /// The unknown provider identifier. + /// The unknown provider. provider: Box, }, - /// Model is not found for the given provider in the catalog. + /// The model isn't in the catalog for this provider. UnknownModel { /// Agent name for error context. agent: Box, - /// Provider identifier. + /// Provider. provider: Box, - /// Model identifier within the provider. + /// Model name within the provider. model: Box, }, } @@ -154,21 +133,20 @@ impl core::fmt::Display for ModelResolutionError { impl std::error::Error for ModelResolutionError {} -/// Resolves the effective model for an agent using a provided catalog. +/// Picks which model an agent will use. /// -/// This is the primary entrypoint for model resolution. It applies the -/// precedence rules (agent override first, then runtime default) and -/// validates the result against the catalog. +/// Checks the agent's model first, then the runtime default. +/// Validates the result against the catalog. /// /// # Arguments /// -/// * `catalog` - Model catalog for validation -/// * `defaults` - Runtime-wide fallback settings -/// * `agent` - Agent configuration being resolved +/// * `catalog` - Your model catalog for validation +/// * `defaults` - Default settings (used if agent doesn't specify a model) +/// * `agent` - The agent configuration /// /// # Returns /// -/// A [`ResolvedModel`] on success, or a [`ModelResolutionError`] on failure. +/// A [`ResolvedModel`] on success, or a [`ModelResolutionError`] if something's wrong. pub fn resolve_model_with_catalog( catalog: &ModelCatalog, defaults: &super::state::AgentDefaults, @@ -197,10 +175,7 @@ pub fn resolve_model_with_catalog( }) } -/// Determines the effective model parts by applying override precedence. -/// -/// Agent override takes precedence over runtime defaults. Returns an error -/// if neither provides a valid model identifier. +/// Extracts provider and model parts, checking agent override first. fn get_provider_model<'a>( defaults: &'a super::state::AgentDefaults, agent: &'a AgentConfig, diff --git a/src/llm-coding-tools-agents/src/runtime/state.rs b/src/llm-coding-tools-agents/src/runtime/state.rs index 447608b9..70b3ab6f 100644 --- a/src/llm-coding-tools-agents/src/runtime/state.rs +++ b/src/llm-coding-tools-agents/src/runtime/state.rs @@ -1,22 +1,12 @@ -//! Owned runtime state for later agent construction. -//! -//! This module provides the core [`AgentRuntime`] type that holds catalog, -//! defaults, and tool metadata for on-demand agent materialization. Framework -//! adapters consume this state and add concrete execution/build behavior. -//! -//! # Public API -//! -//! - [`AgentDefaults`] - Runtime-wide fallback settings applied when agent -//! configs omit them -//! - [`AgentRuntime`] - Owned runtime state used for later agent construction +//! Holds your loaded agents, default settings, and available tools. use super::tool_catalog::ToolCatalogEntry; use crate::AgentCatalog; -/// Runtime-wide fallback settings applied when an agent config omits them. +/// Default settings used when an agent doesn't specify them. #[derive(Debug, Clone, Default, PartialEq)] pub struct AgentDefaults { - /// Default model identifier in `provider/model-id` format. + /// Default model in `provider/model-id` format. pub model: Option>, /// Default sampling temperature. pub temperature: Option, @@ -24,7 +14,7 @@ pub struct AgentDefaults { pub top_p: Option, } -/// Owned runtime state used for later on-demand agent construction. +/// Your loaded agents plus their default settings and available tools. #[derive(Debug, Clone)] pub struct AgentRuntime { catalog: AgentCatalog, @@ -46,19 +36,19 @@ impl AgentRuntime { } } - /// Returns the parsed catalog that remains the runtime source of truth. + /// Returns the loaded agent definitions. #[inline] pub fn catalog(&self) -> &AgentCatalog { &self.catalog } - /// Returns the runtime fallback settings. + /// Returns the default settings. #[inline] pub fn defaults(&self) -> &AgentDefaults { &self.defaults } - /// Returns the owned tool-catalog metadata used for later tool materialization. + /// Returns the tools available to agents. #[inline] pub fn tools(&self) -> &[ToolCatalogEntry] { &self.tools diff --git a/src/llm-coding-tools-agents/src/runtime/tool_catalog.rs b/src/llm-coding-tools-agents/src/runtime/tool_catalog.rs index 7350cba3..04113e23 100644 --- a/src/llm-coding-tools-agents/src/runtime/tool_catalog.rs +++ b/src/llm-coding-tools-agents/src/runtime/tool_catalog.rs @@ -1,52 +1,54 @@ -//! Explicit default runtime tool catalog for agent builds. +//! Lists which tools your agents can use. //! -//! This module provides a cloneable, data-only tool catalog used during JIT agent -//! construction. Each [`ToolCatalogEntry`] pairs a canonical tool name with a -//! [`ToolCatalogKind`] variant that later runtime layers can match on to instantiate -//! concrete tools on demand. +//! Each [`ToolCatalogEntry`] pairs a tool name with its type ([`ToolCatalogKind`]). //! -//! The default catalog exposed by [`default_tools()`] covers the non-Task tool surface -//! (read, write, edit, glob, grep, bash, webfetch, todoread, todowrite). Task is -//! intentionally excluded to keep the catalog focused on standard runtime tools. +//! # 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. The "task" tool is excluded since it's handled separately. use llm_coding_tools_core::tool_names; -/// Cloneable metadata for a runtime tool that can be materialized later. +/// One tool the runtime can provide to agents. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct ToolCatalogEntry { - /// Canonical tool name exposed to models. + /// Tool name exposed to models. pub name: &'static str, - /// Concrete tool variant used during later runtime instantiation. + /// Which tool this is. pub kind: ToolCatalogKind, } impl ToolCatalogEntry { - /// Creates a catalog entry from a canonical tool name and concrete kind. + /// Creates a tool entry from its name and kind. pub const fn new(name: &'static str, kind: ToolCatalogKind) -> Self { Self { name, kind } } } -/// Explicit tool variants supported by the default runtime surface. +/// The tools your agents can use. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum ToolCatalogKind { - /// Read file contents tool. + /// Read file contents. Read, - /// Write file contents tool. + /// Write file contents. Write, - /// Edit file contents tool. + /// Edit file contents. Edit, - /// Glob file pattern matching tool. + /// Glob file pattern matching. Glob, - /// Grep text search tool. + /// Grep text search. Grep, - /// Bash command execution tool. + /// Bash command execution. Bash, - /// Web fetch tool for HTTP requests. + /// Web fetch for HTTP requests. WebFetch, - /// Todo read tool for reading todo items. + /// Read todo items. TodoRead, - /// Todo write tool for creating and updating todo items. + /// Create and update todo items. TodoWrite, } @@ -62,7 +64,7 @@ const DEFAULT_TOOLS: [ToolCatalogEntry; 9] = [ ToolCatalogEntry::new(tool_names::TODO_WRITE, ToolCatalogKind::TodoWrite), ]; -/// Returns the explicit default non-Task tool catalog for runtimes. +/// Returns the standard tool set. pub fn default_tools() -> Vec { DEFAULT_TOOLS.to_vec() } diff --git a/src/llm-coding-tools-models-dev/src/fs/blocking_impl.rs b/src/llm-coding-tools-models-dev/src/fs/blocking_impl.rs index 01252a9b..3dc6f62c 100644 --- a/src/llm-coding-tools-models-dev/src/fs/blocking_impl.rs +++ b/src/llm-coding-tools-models-dev/src/fs/blocking_impl.rs @@ -9,7 +9,7 @@ use std::path::Path; /// /// We snapshot file length then call `read_exact`, which would miss data appended after /// the metadata call if the file grew mid-read. However, within this codebase all -/// writes go to a temp file first, then rename to target — so files are never +/// writes go to a temp file first, then rename to target - so files are never /// appended to in place. /// Therefore this race cannot occur. #[inline] diff --git a/src/llm-coding-tools-models-dev/src/fs/tokio_impl.rs b/src/llm-coding-tools-models-dev/src/fs/tokio_impl.rs index 29d04d2c..92bca908 100644 --- a/src/llm-coding-tools-models-dev/src/fs/tokio_impl.rs +++ b/src/llm-coding-tools-models-dev/src/fs/tokio_impl.rs @@ -10,7 +10,7 @@ use tokio::io::AsyncReadExt as _; /// /// We snapshot file length then call `read_exact`, which would miss data appended after /// the metadata call if the file grew mid-read. However, within this codebase all -/// writes go to a temp file first, then rename to target — so files are never +/// writes go to a temp file first, then rename to target - so files are never /// appended to in place. /// Therefore this race cannot occur. #[inline] From d799025b4ed702218f60c4df4601a8c4aa4495fc Mon Sep 17 00:00:00 2001 From: Sewer56 Date: Fri, 13 Mar 2026 02:06:39 +0000 Subject: [PATCH 06/11] Revert Cargo.toml to match main branch --- src/Cargo.lock | 4 ---- src/llm-coding-tools-serdesai/Cargo.toml | 6 ------ 2 files changed, 10 deletions(-) diff --git a/src/Cargo.lock b/src/Cargo.lock index 40f700e8..9bf2c98e 100644 --- a/src/Cargo.lock +++ b/src/Cargo.lock @@ -1521,13 +1521,9 @@ dependencies = [ name = "llm-coding-tools-serdesai" version = "0.2.0" dependencies = [ - "ahash", "async-trait", "futures", - "indexmap", - "llm-coding-tools-agents", "llm-coding-tools-core", - "llm-coding-tools-models-dev", "reqwest 0.13.1", "serde", "serde_json", diff --git a/src/llm-coding-tools-serdesai/Cargo.toml b/src/llm-coding-tools-serdesai/Cargo.toml index 52f7739f..bc13dff0 100644 --- a/src/llm-coding-tools-serdesai/Cargo.toml +++ b/src/llm-coding-tools-serdesai/Cargo.toml @@ -14,10 +14,6 @@ llm-coding-tools-core = { version = "0.2.0", path = "../llm-coding-tools-core", "tokio", ] } -# Runtime foundation dependencies -llm-coding-tools-agents = { version = "0.1.0", path = "../llm-coding-tools-agents" } -llm-coding-tools-models-dev = { version = "0.1.0", path = "../llm-coding-tools-models-dev" } - # serdes-ai provides Tool trait, ToolDefinition, RunContext serdes-ai = "0.2.6" serdes-ai-models = { version = "0.2.6", features = ["openrouter"] } @@ -41,5 +37,3 @@ reqwest = { version = "0.13", default-features = false, features = [ tokio = { version = "1", features = ["macros", "rt-multi-thread"] } tempfile = "3" wiremock = "0.6" -ahash = "0.8" -indexmap = "2" From 98997116edda0e74a1f9f263e10e3502b0c076a8 Mon Sep 17 00:00:00 2001 From: Sewer56 Date: Fri, 13 Mar 2026 13:37:38 +0000 Subject: [PATCH 07/11] Changed: Update module docs to reflect parse_model_parts re-export Add parse_model_parts to the "## Re-exports" documentation section to match the actual pub use statement that re-exports it from config. Changes: - Added parse_model_parts to config types list in module documentation Benefits: - Documentation now accurately reflects exported items - Easier for users to discover available functions --- src/llm-coding-tools-agents/src/types/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/llm-coding-tools-agents/src/types/mod.rs b/src/llm-coding-tools-agents/src/types/mod.rs index 0b3de3ca..a2af7b74 100644 --- a/src/llm-coding-tools-agents/src/types/mod.rs +++ b/src/llm-coding-tools-agents/src/types/mod.rs @@ -3,7 +3,7 @@ //! Central module for types used across loading and catalog operations. //! //! ## Re-exports -//! - Config types: [`AgentConfig`], [`AgentMode`], [`PermissionRule`] +//! - Config types: [`AgentConfig`], [`AgentMode`], [`PermissionRule`], [`parse_model_parts`] //! - Load errors: [`AgentLoadError`], [`AgentLoadResult`] mod config; From 993193a285467aceadefecec8b556555c339a249 Mon Sep 17 00:00:00 2001 From: Sewer56 Date: Fri, 13 Mar 2026 13:43:17 +0000 Subject: [PATCH 08/11] Changed: Mark public enums as non_exhaustive for API stability Add #[non_exhaustive] to ToolCatalogKind and ModelResolutionError to allow adding variants without breaking downstream matches. Changes: - Added #[non_exhaustive] to ToolCatalogKind enum - Added #[non_exhaustive] to ModelResolutionError enum Benefits: - Future variant additions won't break external pattern matches - Maintains backward compatibility when extending the API --- src/llm-coding-tools-agents/src/runtime/model.rs | 1 + src/llm-coding-tools-agents/src/runtime/tool_catalog.rs | 1 + 2 files changed, 2 insertions(+) diff --git a/src/llm-coding-tools-agents/src/runtime/model.rs b/src/llm-coding-tools-agents/src/runtime/model.rs index f44c6636..2ae24f0f 100644 --- a/src/llm-coding-tools-agents/src/runtime/model.rs +++ b/src/llm-coding-tools-agents/src/runtime/model.rs @@ -65,6 +65,7 @@ impl ResolvedModel { /// Errors when picking or validating a model. #[derive(Debug)] +#[non_exhaustive] pub enum ModelResolutionError { /// Model string is malformed (missing `/` or empty parts). MalformedModelIdentifier { diff --git a/src/llm-coding-tools-agents/src/runtime/tool_catalog.rs b/src/llm-coding-tools-agents/src/runtime/tool_catalog.rs index 04113e23..9aa8c464 100644 --- a/src/llm-coding-tools-agents/src/runtime/tool_catalog.rs +++ b/src/llm-coding-tools-agents/src/runtime/tool_catalog.rs @@ -31,6 +31,7 @@ impl ToolCatalogEntry { /// The tools your agents can use. #[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[non_exhaustive] pub enum ToolCatalogKind { /// Read file contents. Read, From d6ad44b2b7337595792ff11599734f91b0eca86b Mon Sep 17 00:00:00 2001 From: Sewer56 Date: Fri, 13 Mar 2026 13:43:36 +0000 Subject: [PATCH 09/11] Changed: Update tool documentation and add non_exhaustive to enums Clarify that todo tools are two separate tools (todoread/todowrite) rather than a single "todo" tool. Add #[non_exhaustive] to public enums for future-proofing. Changes: - README: replace "todo" with "todoread/todowrite" in default tools list - Add #[non_exhaustive] to ModelResolutionError enum - Add #[non_exhaustive] to ToolCatalogKind enum Benefits: - Users understand the actual runtime default tools - Enums can be extended without breaking changes --- src/llm-coding-tools-agents/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/llm-coding-tools-agents/README.md b/src/llm-coding-tools-agents/README.md index 2595f56c..274a6d48 100644 --- a/src/llm-coding-tools-agents/README.md +++ b/src/llm-coding-tools-agents/README.md @@ -68,7 +68,7 @@ let runtime = AgentRuntimeBuilder::new() temperature: Some(0.2), top_p: Some(0.95), }) - // .tools(my_custom_tools) // optional; defaults to read/write/edit/glob/grep/bash/webfetch/todo + // .tools(my_custom_tools) // optional; defaults to read/write/edit/glob/grep/bash/webfetch/todoread/todowrite .build(); // Pass `runtime` to your framework adapter to build agents by name From 4b6952e06d833b5ca602ad87a3c02c8890b776fc Mon Sep 17 00:00:00 2001 From: Sewer56 Date: Fri, 13 Mar 2026 18:08:37 +0000 Subject: [PATCH 10/11] Fixed: Clarify model resolution errors with source location Error messages for unknown provider/model now indicate whether the value came from agent override or runtime defaults, preventing misleading blame on the agent when the issue is in runtime configuration. Changes: - Added `location` field to `UnknownProvider` and `UnknownModel` variants - Updated `get_provider_model()` to return source location - Changed error messages to say "effective provider/model from {location}" Benefits: - Users can clearly identify the source of misconfiguration - Runtime defaults issues no longer incorrectly blame agent config --- .../src/runtime/model.rs | 38 ++++++++++++++----- 1 file changed, 28 insertions(+), 10 deletions(-) diff --git a/src/llm-coding-tools-agents/src/runtime/model.rs b/src/llm-coding-tools-agents/src/runtime/model.rs index 2ae24f0f..696fe7d0 100644 --- a/src/llm-coding-tools-agents/src/runtime/model.rs +++ b/src/llm-coding-tools-agents/src/runtime/model.rs @@ -85,6 +85,8 @@ pub enum ModelResolutionError { UnknownProvider { /// Agent name for error context. agent: Box, + /// Where the provider came from. + location: &'static str, /// The unknown provider. provider: Box, }, @@ -92,6 +94,8 @@ pub enum ModelResolutionError { UnknownModel { /// Agent name for error context. agent: Box, + /// Where the model came from. + location: &'static str, /// Provider. provider: Box, /// Model name within the provider. @@ -114,19 +118,24 @@ impl core::fmt::Display for ModelResolutionError { f, "agent `{agent}` does not define a model override and runtime defaults do not define one either", ), - Self::UnknownProvider { agent, provider } => { + Self::UnknownProvider { + agent: _, + location, + provider, + } => { write!( f, - "agent `{agent}` references unknown provider `{provider}`" + "effective provider `{provider}` from {location} is not in catalog" ) } Self::UnknownModel { - agent, + agent: _, + location, provider, model, } => write!( f, - "agent `{agent}` references unknown model `{provider}/{model}`", + "effective model `{provider}/{model}` from {location} is not in catalog", ), } } @@ -153,11 +162,12 @@ pub fn resolve_model_with_catalog( defaults: &super::state::AgentDefaults, agent: &AgentConfig, ) -> Result { - let (provider, model) = get_provider_model(defaults, agent)?; + let (provider, model, location) = get_provider_model(defaults, agent)?; if catalog.lookup_provider(provider).is_none() { return Err(ModelResolutionError::UnknownProvider { agent: agent.name.clone(), + location, provider: provider.into(), }); } @@ -165,6 +175,7 @@ pub fn resolve_model_with_catalog( if catalog.lookup_provider_model(provider, model).is_none() { return Err(ModelResolutionError::UnknownModel { agent: agent.name.clone(), + location, provider: provider.into(), model: model.into(), }); @@ -180,7 +191,7 @@ pub fn resolve_model_with_catalog( fn get_provider_model<'a>( defaults: &'a super::state::AgentDefaults, agent: &'a AgentConfig, -) -> Result<(&'a str, &'a str), ModelResolutionError> { +) -> Result<(&'a str, &'a str, &'static str), ModelResolutionError> { if let Some(raw) = agent.model.as_deref() { let (provider, model) = crate::parse_model_parts(raw).ok_or_else(|| { ModelResolutionError::MalformedModelIdentifier { @@ -189,7 +200,7 @@ fn get_provider_model<'a>( model: raw.into(), } })?; - return Ok((provider, model)); + return Ok((provider, model, "agent override")); } if let Some(raw) = defaults.model.as_deref() { @@ -200,7 +211,7 @@ fn get_provider_model<'a>( model: raw.into(), } })?; - return Ok((provider, model)); + return Ok((provider, model, "runtime default")); } Err(ModelResolutionError::MissingEffectiveModel { @@ -393,7 +404,10 @@ mod tests { .expect_err("missing provider should fail"); match err { - ModelResolutionError::UnknownProvider { provider, .. } => { + ModelResolutionError::UnknownProvider { + location, provider, .. + } => { + assert_eq!(location, "agent override"); assert_eq!(&*provider, "anthropic"); } other => panic!("unexpected error: {other}"), @@ -421,8 +435,12 @@ mod tests { match err { ModelResolutionError::UnknownModel { - provider, model, .. + location, + provider, + model, + .. } => { + assert_eq!(location, "agent override"); assert_eq!(&*provider, "openai"); assert_eq!(&*model, "gpt-4.1-mini"); } From efffefe733d4f7504aeceb5017b0e43660001e14 Mon Sep 17 00:00:00 2001 From: Sewer56 Date: Fri, 13 Mar 2026 18:18:02 +0000 Subject: [PATCH 11/11] Changed: Expand documentation for agent runtime state Add module-level docs with Public API section and improve struct docs. Changes: - Added module docs with purpose and Public API list - Expanded AgentDefaults docs with usage guidance Benefits: - Clearer entry points for callers - Better discoverability of key types --- src/llm-coding-tools-agents/src/runtime/state.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/llm-coding-tools-agents/src/runtime/state.rs b/src/llm-coding-tools-agents/src/runtime/state.rs index 70b3ab6f..79f79355 100644 --- a/src/llm-coding-tools-agents/src/runtime/state.rs +++ b/src/llm-coding-tools-agents/src/runtime/state.rs @@ -1,4 +1,9 @@ //! Holds your loaded agents, default settings, and available tools. +//! +//! ## Public API +//! +//! - [`AgentRuntime`] — Container for loaded agents, defaults, and tools. +//! - [`AgentDefaults`] — Fallback settings when an agent doesn't specify them. use super::tool_catalog::ToolCatalogEntry; use crate::AgentCatalog;