Skip to main content

distri_types/
agent.rs

1use crate::AgentError;
2use crate::a2a::AgentSkill;
3use crate::browser::{BrowserAgentConfig, BrowsrClientConfig};
4use crate::configuration::DefinitionOverrides;
5use schemars::JsonSchema;
6use serde::{Deserialize, Serialize};
7use std::default::Default;
8
9/// Default timeout for external tool execution in seconds
10pub const DEFAULT_EXTERNAL_TOOL_TIMEOUT_SECS: u64 = 120;
11
12/// A reference to a stored skill that an agent can load on demand
13#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, Default)]
14pub struct AvailableSkill {
15    /// The skill ID (UUID)
16    pub id: String,
17    /// Human-readable skill name (for display in the partial)
18    pub name: String,
19    /// Brief description of what this skill does (shown to the agent)
20    #[serde(default, skip_serializing_if = "Option::is_none")]
21    pub description: Option<String>,
22}
23
24/// Unified Agent Strategy Configuration
25#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
26#[serde(deny_unknown_fields, rename_all = "snake_case")]
27#[derive(Default)]
28pub struct AgentStrategy {
29    /// Depth of reasoning (shallow, standard, deep)
30    #[serde(default, skip_serializing_if = "Option::is_none")]
31    pub reasoning_depth: Option<ReasoningDepth>,
32
33    /// Execution mode - tools vs code
34    #[serde(default, skip_serializing_if = "Option::is_none")]
35    pub execution_mode: Option<ExecutionMode>,
36    /// When to replan
37    #[serde(default, skip_serializing_if = "Option::is_none")]
38    pub replanning: Option<ReplanningConfig>,
39
40    /// Timeout in seconds for external tool execution (default: 120)
41    /// External tools are tools that delegate execution to the frontend/client.
42    #[serde(default, skip_serializing_if = "Option::is_none")]
43    pub external_tool_timeout_secs: Option<u64>,
44}
45
46impl AgentStrategy {
47    /// Get reasoning depth with default fallback
48    pub fn get_reasoning_depth(&self) -> ReasoningDepth {
49        self.reasoning_depth.clone().unwrap_or_default()
50    }
51
52    /// Get execution mode with default fallback
53    pub fn get_execution_mode(&self) -> ExecutionMode {
54        self.execution_mode.clone().unwrap_or_default()
55    }
56
57    /// Get replanning config with default fallback
58    pub fn get_replanning(&self) -> ReplanningConfig {
59        self.replanning.clone().unwrap_or_default()
60    }
61
62    /// Get external tool timeout with default fallback
63    pub fn get_external_tool_timeout_secs(&self) -> u64 {
64        self.external_tool_timeout_secs
65            .unwrap_or(DEFAULT_EXTERNAL_TOOL_TIMEOUT_SECS)
66    }
67}
68
69#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, Default, PartialEq)]
70#[serde(rename_all = "snake_case")]
71pub enum CodeLanguage {
72    #[default]
73    Typescript,
74}
75
76impl std::fmt::Display for CodeLanguage {
77    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
78        write!(f, "{}", self.to_string())
79    }
80}
81
82impl CodeLanguage {
83    pub fn to_string(&self) -> String {
84        match self {
85            CodeLanguage::Typescript => "typescript".to_string(),
86        }
87    }
88}
89
90/// Reflection configuration
91#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, Default)]
92pub struct ReflectionConfig {
93    /// Whether to enable reflection
94    #[serde(default)]
95    pub enabled: bool,
96    /// Name of the agent definition to use for reflection.
97    /// Must be an agent that has the "reflect" tool configured.
98    /// If not set, uses the built-in reflection_agent.
99    #[serde(default, skip_serializing_if = "Option::is_none")]
100    pub reflection_agent: Option<String>,
101    /// When to trigger reflection
102    #[serde(default)]
103    pub trigger: ReflectionTrigger,
104    /// Depth of reflection
105    #[serde(default)]
106    pub depth: ReflectionDepth,
107}
108
109/// When to trigger reflection
110#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, Default)]
111#[serde(rename_all = "snake_case")]
112pub enum ReflectionTrigger {
113    /// At the end of execution
114    #[default]
115    EndOfExecution,
116    /// After each step
117    AfterEachStep,
118    /// After failures only
119    AfterFailures,
120    /// After N steps
121    AfterNSteps(usize),
122}
123
124/// Depth of reflection
125#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, Default)]
126#[serde(rename_all = "snake_case")]
127pub enum ReflectionDepth {
128    /// Light reflection
129    #[default]
130    Light,
131    /// Standard reflection
132    Standard,
133    /// Deep reflection
134    Deep,
135}
136
137/// Configuration for planning operations
138#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
139pub struct PlanConfig {
140    /// The model settings for the planning agent
141    #[serde(default, skip_serializing_if = "Option::is_none")]
142    pub model_settings: Option<ModelSettings>,
143    /// The maximum number of iterations allowed during planning
144    #[serde(default = "default_plan_max_iterations")]
145    pub max_iterations: usize,
146}
147
148impl Default for PlanConfig {
149    fn default() -> Self {
150        Self {
151            model_settings: None,
152            max_iterations: default_plan_max_iterations(),
153        }
154    }
155}
156
157fn default_plan_max_iterations() -> usize {
158    10
159}
160
161/// Depth of reasoning for planning
162#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, Default, PartialEq)]
163#[serde(rename_all = "snake_case")]
164pub enum ReasoningDepth {
165    /// Shallow reasoning - direct action with minimal thought, skip reasoning sections
166    Shallow,
167    /// Standard reasoning - moderate planning and thought
168    #[default]
169    Standard,
170    /// Deep reasoning - extensive planning, multi-step analysis, and comprehensive thinking
171    Deep,
172}
173
174/// Execution mode - tools vs code
175#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, Default, PartialEq)]
176#[serde(rename_all = "snake_case", tag = "type")]
177pub enum ExecutionMode {
178    /// Use tools for execution
179    #[default]
180    Tools,
181    /// Use code execution
182    Code { language: CodeLanguage },
183}
184
185/// Replanning configuration
186#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, Default)]
187#[serde(rename_all = "snake_case")]
188pub struct ReplanningConfig {
189    /// When to trigger replanning
190    #[serde(default, skip_serializing_if = "Option::is_none")]
191    pub trigger: Option<ReplanningTrigger>,
192    /// Whether to replan at all
193    #[serde(default, skip_serializing_if = "Option::is_none")]
194    pub enabled: Option<bool>,
195}
196
197/// When to trigger replanning
198#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, Default)]
199#[serde(rename_all = "snake_case")]
200pub enum ReplanningTrigger {
201    /// Never replan (default)
202    #[default]
203    Never,
204    /// Replan after execution reflection
205    AfterReflection,
206    /// Replan after N iterations
207    AfterNIterations(usize),
208    /// Replan after failures
209    AfterFailures,
210}
211
212impl ReplanningConfig {
213    /// Get trigger with default fallback
214    pub fn get_trigger(&self) -> ReplanningTrigger {
215        self.trigger.clone().unwrap_or_default()
216    }
217
218    /// Get enabled with default fallback
219    pub fn is_enabled(&self) -> bool {
220        self.enabled.unwrap_or(false)
221    }
222}
223
224#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, Default)]
225#[serde(rename_all = "snake_case")]
226pub enum ExecutionKind {
227    #[default]
228    Retriable,
229    Interleaved,
230}
231
232#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, Default)]
233#[serde(rename_all = "snake_case")]
234pub enum MemoryKind {
235    #[default]
236    None,
237    ShortTerm,
238    LongTerm,
239}
240
241/// How tools are delivered to the LLM
242#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, Default, PartialEq)]
243#[serde(rename_all = "snake_case")]
244pub enum ToolDeliveryMode {
245    /// Send all tool schemas upfront in the system message / tools parameter (default)
246    #[default]
247    AllTools,
248    /// Only send tool names+descriptions; agent uses a `tool_search` tool to fetch full schemas on demand
249    ToolSearch,
250}
251
252/// Supported tool call formats
253#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, Default, PartialEq)]
254#[serde(rename_all = "snake_case")]
255pub enum ToolCallFormat {
256    /// New XML format: Streaming-capable XML tool calls
257    /// Example: <search><query>test</query></search>
258    #[default]
259    Xml,
260    /// New JSON format: JSONL with tool_calls blocks
261    /// Example: ```tool_calls\n{"name":"search","arguments":{"query":"test"}}```
262    JsonL,
263
264    /// Code execution format: TypeScript/JavaScript code blocks
265    /// Example: ```typescript ... ```
266    Code,
267    #[serde(rename = "provider")]
268    Provider,
269    None,
270}
271
272#[derive(Debug, Serialize, Deserialize, Clone, JsonSchema, Default)]
273pub struct UserMessageOverrides {
274    /// The parts to include in the user message
275    pub parts: Vec<PartDefinition>,
276    /// If true, artifacts will be expanded to their actual content (e.g., image artifacts become Part::Image)
277    #[serde(default)]
278    pub include_artifacts: bool,
279    /// If true (default), step count information will be included at the end of the user message
280    #[serde(default = "default_include_step_count")]
281    pub include_step_count: Option<bool>,
282}
283
284fn default_include_step_count() -> Option<bool> {
285    Some(true)
286}
287
288#[derive(Debug, Serialize, Deserialize, Clone, JsonSchema)]
289#[serde(tag = "type", content = "source", rename_all = "snake_case")]
290pub enum PartDefinition {
291    Template(String),   // Prompt Template Key
292    SessionKey(String), // Session key reference
293}
294
295#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
296#[serde(deny_unknown_fields)]
297pub struct LlmDefinition {
298    /// The name of the agent.
299    pub name: String,
300    /// Settings related to the model used by the agent.
301    #[serde(default, skip_serializing_if = "Option::is_none")]
302    pub model_settings: Option<ModelSettings>,
303    /// Tool calling configuration
304    #[serde(default)]
305    pub tool_format: ToolCallFormat,
306    /// How tools are delivered to the LLM (all upfront vs on-demand search)
307    #[serde(default)]
308    pub tool_delivery_mode: ToolDeliveryMode,
309}
310
311impl LlmDefinition {
312    /// Get a reference to model_settings.
313    /// Returns an error if model_settings is None.
314    pub fn ms(&self) -> Result<&ModelSettings, String> {
315        self.model_settings.as_ref().ok_or_else(|| {
316            "No model configured. Please set a default model in Agent Settings → Default Model."
317                .to_string()
318        })
319    }
320
321    /// Get a mutable reference to model_settings.
322    /// Returns an error if model_settings is None.
323    pub fn ms_mut(&mut self) -> Result<&mut ModelSettings, String> {
324        self.model_settings.as_mut().ok_or_else(|| {
325            "No model configured. Please set a default model in Agent Settings → Default Model."
326                .to_string()
327        })
328    }
329}
330
331/// Agent definition - complete configuration for an agent
332#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, Default)]
333pub struct StandardDefinition {
334    /// The name of the agent.
335    pub name: String,
336    /// Optional package name that registered this agent (workspace/plugin metadata)
337    #[serde(default, skip_serializing_if = "Option::is_none")]
338    pub package_name: Option<String>,
339    /// A brief description of the agent's purpose.
340    #[serde(default)]
341    pub description: String,
342
343    /// The version of the agent.
344    #[serde(default = "default_agent_version")]
345    pub version: Option<String>,
346
347    /// Instructions for the agent - serves as an introduction defining what the agent is and does.
348    #[serde(default)]
349    pub instructions: String,
350
351    /// A list of MCP server definitions associated with the agent.
352    #[serde(default)]
353    pub mcp_servers: Option<Vec<McpDefinition>>,
354    /// Settings related to the model used by the agent.
355    /// When `None`, the agent inherits model settings from the orchestrator context defaults.
356    #[serde(default, skip_serializing_if = "Option::is_none")]
357    pub model_settings: Option<ModelSettings>,
358    /// Optional lower-level model settings for lightweight analysis helpers
359    #[serde(default, skip_serializing_if = "Option::is_none")]
360    pub analysis_model_settings: Option<ModelSettings>,
361
362    /// The size of the history to maintain for the agent.
363    #[serde(default = "default_history_size")]
364    pub history_size: Option<usize>,
365    /// The new strategy configuration for the agent.
366    #[serde(default, skip_serializing_if = "Option::is_none")]
367    pub strategy: Option<AgentStrategy>,
368    /// A2A-specific fields
369    #[serde(default)]
370    pub icon_url: Option<String>,
371
372    #[serde(default, skip_serializing_if = "Option::is_none")]
373    pub max_iterations: Option<usize>,
374
375    /// A2A agent card skills metadata (describes capabilities for agent-to-agent protocol)
376    #[serde(default, skip_serializing_if = "Vec::is_empty")]
377    pub skills_description: Vec<AgentSkill>,
378
379    /// Skills available for on-demand loading by this agent
380    #[serde(default, skip_serializing_if = "Vec::is_empty")]
381    pub available_skills: Vec<AvailableSkill>,
382
383    /// List of sub-agents that this agent can transfer control to
384    #[serde(default)]
385    pub sub_agents: Vec<String>,
386
387    /// Tool calling configuration
388    #[serde(default)]
389    pub tool_format: ToolCallFormat,
390
391    /// How tools are delivered to the LLM (all upfront vs on-demand search)
392    #[serde(default)]
393    pub tool_delivery_mode: ToolDeliveryMode,
394
395    /// Tools configuration for this agent
396    #[serde(default, skip_serializing_if = "Option::is_none")]
397    pub tools: Option<ToolsConfig>,
398
399    /// Where filesystem and artifact tools should run (server or local)
400    #[serde(default)]
401    pub file_system: FileSystemMode,
402
403    /// Custom handlebars partials (name -> template path) for use in custom prompts
404    #[serde(default, skip_serializing_if = "std::collections::HashMap::is_empty")]
405    pub partials: std::collections::HashMap<String, String>,
406
407    /// Whether to write large tool responses to filesystem as artifacts (default: false)
408    #[serde(default, skip_serializing_if = "Option::is_none")]
409    pub write_large_tool_responses_to_fs: Option<bool>,
410
411    /// Reflection configuration for post-execution analysis using a subagent
412    #[serde(default, skip_serializing_if = "Option::is_none")]
413    pub reflection: Option<ReflectionConfig>,
414    /// Whether to enable TODO management functionality
415    #[serde(default, skip_serializing_if = "Option::is_none")]
416    pub enable_todos: Option<bool>,
417
418    /// Browser configuration for this agent (enables shared Chromium automation)
419    #[serde(default, skip_serializing_if = "Option::is_none")]
420    pub browser_config: Option<BrowserAgentConfig>,
421
422    /// Whether to include shell/code execution tools (start_shell, execute_shell, stop_shell)
423    #[serde(default, skip_serializing_if = "Option::is_none")]
424    pub include_shell: Option<bool>,
425
426    /// Context size override for this agent (overrides model_settings.context_size)
427    #[serde(default, skip_serializing_if = "Option::is_none")]
428    pub context_size: Option<u32>,
429
430    /// Strategy for prompt construction (append default template vs fully custom)
431    #[serde(
432        skip_serializing_if = "Option::is_none",
433        default = "default_append_default_instructions"
434    )]
435    pub append_default_instructions: Option<bool>,
436    /// Whether to include the built-in scratchpad/history in prompts (default: true)
437    #[serde(
438        skip_serializing_if = "Option::is_none",
439        default = "default_include_scratchpad"
440    )]
441    pub include_scratchpad: Option<bool>,
442
443    /// Optional hook names to attach to this agent
444    #[serde(default, skip_serializing_if = "Vec::is_empty")]
445    pub hooks: Vec<String>,
446
447    /// Custom user message construction (dynamic prompting)
448    #[serde(default, skip_serializing_if = "Option::is_none")]
449    pub user_message_overrides: Option<UserMessageOverrides>,
450
451    /// Whether context compaction is enabled for this agent (default: true)
452    #[serde(
453        default = "default_compaction_enabled",
454        skip_serializing_if = "is_true"
455    )]
456    pub compaction_enabled: bool,
457}
458fn default_append_default_instructions() -> Option<bool> {
459    Some(true)
460}
461fn default_include_scratchpad() -> Option<bool> {
462    Some(true)
463}
464fn default_compaction_enabled() -> bool {
465    true
466}
467fn is_true(v: &bool) -> bool {
468    *v
469}
470impl StandardDefinition {
471    /// Check if large tool responses should be written to filesystem (default: false)
472    pub fn should_write_large_tool_responses_to_fs(&self) -> bool {
473        self.write_large_tool_responses_to_fs.unwrap_or(false)
474    }
475
476    /// Check if browser should be initialized automatically in orchestrator (default: false)
477    pub fn should_use_browser(&self) -> bool {
478        self.browser_config
479            .as_ref()
480            .map(|cfg| cfg.is_enabled())
481            .unwrap_or(false)
482    }
483
484    /// Returns browser config if defined
485    pub fn browser_settings(&self) -> Option<&BrowserAgentConfig> {
486        self.browser_config.as_ref()
487    }
488
489    /// Returns the runtime Chromium driver configuration if enabled
490    pub fn browser_runtime_config(&self) -> Option<BrowsrClientConfig> {
491        self.browser_config.as_ref().map(|cfg| cfg.runtime_config())
492    }
493
494    /// Should browser session state be serialized after tool runs
495    pub fn should_persist_browser_session(&self) -> bool {
496        self.browser_config
497            .as_ref()
498            .map(|cfg| cfg.should_persist_session())
499            .unwrap_or(false)
500    }
501
502    /// Check if reflection is enabled (default: false)
503    pub fn is_reflection_enabled(&self) -> bool {
504        self.reflection.as_ref().map(|r| r.enabled).unwrap_or(false)
505    }
506
507    /// Get the reflection configuration, if any
508    pub fn reflection_config(&self) -> Option<&ReflectionConfig> {
509        self.reflection.as_ref().filter(|r| r.enabled)
510    }
511    /// Check if TODO management functionality is enabled (default: false)
512    pub fn is_todos_enabled(&self) -> bool {
513        self.enable_todos.unwrap_or(false)
514    }
515
516    /// Check if shell/code execution tools should be included (default: false)
517    pub fn should_include_shell(&self) -> bool {
518        self.include_shell.unwrap_or(false)
519    }
520
521    /// Get model settings if configured.
522    pub fn model_settings(&self) -> Option<&ModelSettings> {
523        self.model_settings.as_ref()
524    }
525
526    /// Get a mutable reference to model settings, if present.
527    pub fn model_settings_mut(&mut self) -> Option<&mut ModelSettings> {
528        self.model_settings.as_mut()
529    }
530
531    /// Get the effective context size (agent-level override or model settings)
532    pub fn get_effective_context_size(&self) -> u32 {
533        self.context_size
534            .or_else(|| self.model_settings().map(|ms| ms.inner.context_size))
535            .unwrap_or_else(default_context_size)
536    }
537
538    /// Model settings to use for lightweight browser analysis helpers (e.g., observe_summary commands)
539    pub fn analysis_model_settings_config(&self) -> Option<&ModelSettings> {
540        self.analysis_model_settings
541            .as_ref()
542            .or_else(|| self.model_settings())
543    }
544
545    /// Whether to include the persistent scratchpad/history in prompts
546    pub fn include_scratchpad(&self) -> bool {
547        self.include_scratchpad.unwrap_or(true)
548    }
549
550    /// Apply definition overrides to this agent definition
551    pub fn apply_overrides(&mut self, overrides: DefinitionOverrides) {
552        // Override model settings (only if model_settings already exists)
553        if let Some(ref mut ms) = self.model_settings {
554            if let Some(model) = overrides.model {
555                // Strip provider prefix if present (e.g. "custom_microsoft_foundry/gpt-5.4" → "gpt-5.4")
556                ms.model = model
557                    .split_once('/')
558                    .map(|(_, m)| m.to_string())
559                    .unwrap_or(model);
560            }
561            if let Some(temperature) = overrides.temperature {
562                ms.inner.temperature = Some(temperature);
563            }
564            if let Some(max_tokens) = overrides.max_tokens {
565                ms.inner.max_tokens = Some(max_tokens);
566            }
567        }
568
569        // Override max_iterations
570        if let Some(max_iterations) = overrides.max_iterations {
571            self.max_iterations = Some(max_iterations);
572        }
573
574        // Override instructions
575        if let Some(instructions) = overrides.instructions {
576            self.instructions = instructions;
577        }
578
579        if let Some(use_browser) = overrides.use_browser {
580            let mut config = self.browser_config.clone().unwrap_or_default();
581            config.enabled = use_browser;
582            self.browser_config = Some(config);
583        }
584
585        // Append dynamic tool factories
586        if let Some(dynamic_tools) = overrides.dynamic_tools {
587            let tools = self.tools.get_or_insert_with(ToolsConfig::default);
588            tools.dynamic.extend(dynamic_tools);
589        }
590    }
591}
592
593/// Tools configuration for agents
594/// Canonical list of valid builtin tool names.
595///
596/// Includes both server-executed tools (search, start_shell, etc.) and
597/// client-executed tools (http_request). Agent definitions reference these
598/// by name in `tools.builtin = [...]`.
599pub const VALID_BUILTIN_TOOLS: &[&str] = &[
600    // Agent control
601    "final",
602    "reflect",
603    "transfer_to_agent",
604    // Browser & scraping
605    "browsr_scrape",
606    "browsr_browser",
607    "browsr_crawl",
608    "browser_step",
609    "search",
610    // Shell
611    "start_shell",
612    "execute_shell",
613    "stop_shell",
614    // Code execution
615    "distri_execute_code",
616    // Tool discovery
617    "tool_search",
618    "load_skill",
619    // Connection & secrets
620    "inject_connection_env",
621    // Logging
622    "console_log",
623    // Artifacts & filesystem
624    "artifact_tool",
625    // Todos
626    "todos",
627];
628
629#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, Default)]
630#[serde(deny_unknown_fields)]
631pub struct ToolsConfig {
632    /// Built-in tools to include (e.g., ["final", "transfer_to_agent"])
633    #[serde(default, skip_serializing_if = "Vec::is_empty")]
634    pub builtin: Vec<String>,
635
636    /// Dynamic tool factories — each creates a named tool at runtime.
637    #[serde(default, skip_serializing_if = "Vec::is_empty")]
638    pub dynamic: Vec<crate::dynamic_tool::DynamicToolFactory>,
639
640    /// MCP server tool configurations
641    #[serde(default, skip_serializing_if = "Vec::is_empty")]
642    pub mcp: Vec<McpToolConfig>,
643
644    /// External tools to include from client
645    #[serde(default, skip_serializing_if = "Option::is_none")]
646    pub external: Option<Vec<String>>,
647}
648
649impl ToolsConfig {
650    /// Validate that all builtin tool names are recognized.
651    /// Returns a list of invalid tool names, or empty if all are valid.
652    pub fn invalid_builtin_tools(&self) -> Vec<String> {
653        self.builtin
654            .iter()
655            .filter(|name| !VALID_BUILTIN_TOOLS.contains(&name.as_str()))
656            .cloned()
657            .collect()
658    }
659}
660
661/// Where filesystem and artifact tools should execute
662#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, Default, PartialEq, Eq)]
663#[serde(rename_all = "snake_case")]
664pub enum FileSystemMode {
665    /// Run filesystem/artifact tools on the server (default)
666    #[default]
667    Remote,
668    /// Handle filesystem/artifact tools locally via external tool callbacks
669    Local,
670}
671
672impl FileSystemMode {
673    pub fn include_server_tools(&self) -> bool {
674        !matches!(self, FileSystemMode::Local)
675    }
676}
677
678/// Configuration for tools from an MCP server
679#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
680#[serde(deny_unknown_fields)]
681pub struct McpToolConfig {
682    /// Name of the MCP server
683    pub server: String,
684
685    /// Include patterns (glob-style, e.g., ["fetch_*", "extract_text"])
686    /// Use ["*"] to include all tools from the server
687    #[serde(default, skip_serializing_if = "Vec::is_empty")]
688    pub include: Vec<String>,
689
690    /// Exclude patterns (glob-style, e.g., ["delete_*", "rm_*"])
691    #[serde(default, skip_serializing_if = "Vec::is_empty")]
692    pub exclude: Vec<String>,
693}
694
695#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
696#[serde(deny_unknown_fields)]
697pub struct McpDefinition {
698    /// The filter applied to the tools in this MCP definition.
699    #[serde(default)]
700    pub filter: Option<Vec<String>>,
701    /// The name of the MCP server.
702    pub name: String,
703    /// The type of the MCP server (Tool or Agent).
704    #[serde(default)]
705    pub r#type: McpServerType,
706    /// Authentication configuration for this MCP server.
707    #[serde(default)]
708    pub auth_config: Option<crate::a2a::SecurityScheme>,
709}
710
711#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema, PartialEq)]
712#[serde(rename_all = "lowercase")]
713pub enum McpServerType {
714    #[default]
715    Tool,
716    Agent,
717}
718
719#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
720#[serde(deny_unknown_fields, rename_all = "lowercase", tag = "name")]
721pub enum ModelProvider {
722    #[serde(rename = "openai")]
723    OpenAI {},
724    #[serde(rename = "openai_compat")]
725    OpenAICompatible {
726        base_url: String,
727        api_key: Option<String>,
728        project_id: Option<String>,
729    },
730    #[serde(rename = "azure_openai")]
731    AzureOpenAI {
732        /// Azure resource endpoint, e.g. "https://<resource>.openai.azure.com"
733        base_url: String,
734        /// Azure API key (or fetched from AZURE_OPENAI_API_KEY secret)
735        api_key: Option<String>,
736        /// Azure deployment name
737        deployment: String,
738        /// API version, e.g. "2024-06-01"
739        #[serde(default = "ModelProvider::azure_api_version")]
740        api_version: String,
741    },
742    #[serde(rename = "anthropic")]
743    Anthropic {
744        #[serde(default = "ModelProvider::anthropic_base_url")]
745        base_url: Option<String>,
746        api_key: Option<String>,
747    },
748    #[serde(rename = "vllora")]
749    Vllora {
750        #[serde(default = "ModelProvider::vllora_url")]
751        base_url: String,
752    },
753}
754/// Defines the secret requirements for a provider
755#[derive(Debug, Clone, Serialize, Deserialize)]
756pub struct ProviderSecretDefinition {
757    /// Provider identifier (e.g., "openai", "anthropic")
758    pub id: String,
759    /// Human-readable label
760    pub label: String,
761    /// List of required secret keys with metadata
762    pub keys: Vec<SecretKeyDefinition>,
763}
764
765/// Defines a single secret key requirement
766#[derive(Debug, Clone, Serialize, Deserialize)]
767pub struct SecretKeyDefinition {
768    /// The environment variable / secret store key (e.g., "OPENAI_API_KEY")
769    pub key: String,
770    /// Human-readable label
771    pub label: String,
772    /// Placeholder for UI input
773    pub placeholder: String,
774    /// Whether this secret is required (vs optional)
775    #[serde(default = "default_required")]
776    pub required: bool,
777    /// Whether this field contains sensitive data (masked in UI, stored encrypted).
778    /// Defaults to true. Set to false for non-sensitive config like URLs, project IDs.
779    #[serde(default = "default_sensitive")]
780    pub sensitive: bool,
781}
782
783fn default_required() -> bool {
784    true
785}
786
787fn default_sensitive() -> bool {
788    true
789}
790
791/// A model entry within a provider
792#[derive(Debug, Clone, Serialize, Deserialize)]
793pub struct ModelInfo {
794    /// Model identifier (e.g., "gpt-4o", "claude-sonnet-4")
795    pub id: String,
796    /// Human-readable name
797    pub name: String,
798}
799
800/// Combined provider definition used in default_models.json.
801/// Merges secret key definitions and well-known models into one entry per provider.
802#[derive(Debug, Clone, Serialize, Deserialize)]
803struct DefaultProviderEntry {
804    id: String,
805    label: String,
806    keys: Vec<SecretKeyDefinition>,
807    models: Vec<ModelInfo>,
808}
809
810#[derive(Debug, Clone, Serialize, Deserialize)]
811struct DefaultModelsFile {
812    providers: Vec<DefaultProviderEntry>,
813}
814
815fn load_default_providers() -> &'static [DefaultProviderEntry] {
816    use std::sync::OnceLock;
817    static PROVIDERS: OnceLock<Vec<DefaultProviderEntry>> = OnceLock::new();
818    PROVIDERS.get_or_init(|| {
819        let json = include_str!("default_models.json");
820        let file: DefaultModelsFile =
821            serde_json::from_str(json).expect("Failed to parse default_models.json");
822        file.providers
823    })
824}
825
826/// Models grouped by provider, with configuration status
827#[derive(Debug, Clone, Serialize, Deserialize)]
828pub struct ProviderModels {
829    /// Provider identifier
830    pub provider_id: String,
831    /// Human-readable provider name
832    pub provider_label: String,
833    /// Available models for this provider
834    pub models: Vec<ModelInfo>,
835}
836
837/// Provider models with configuration status (returned by API)
838#[derive(Debug, Clone, Serialize, Deserialize)]
839pub struct ProviderModelsStatus {
840    /// Provider identifier
841    pub provider_id: String,
842    /// Human-readable provider name
843    pub provider_label: String,
844    /// Whether the provider's API key is configured
845    pub configured: bool,
846    /// Available models for this provider
847    pub models: Vec<ModelInfo>,
848}
849
850impl Default for ModelProvider {
851    fn default() -> Self {
852        ModelProvider::OpenAI {}
853    }
854}
855
856impl ModelProvider {
857    pub fn openai_base_url() -> String {
858        "https://api.openai.com/v1".to_string()
859    }
860
861    pub fn anthropic_base_url() -> Option<String> {
862        None // Uses default https://api.anthropic.com
863    }
864
865    pub fn vllora_url() -> String {
866        "http://localhost:9090/v1".to_string()
867    }
868
869    pub fn azure_api_version() -> String {
870        "2024-06-01".to_string()
871    }
872
873    /// Returns the provider ID for secret lookup
874    pub fn provider_id(&self) -> &'static str {
875        match self {
876            ModelProvider::OpenAI {} => "openai",
877            ModelProvider::OpenAICompatible { .. } => "openai_compat",
878            ModelProvider::AzureOpenAI { .. } => "azure_openai",
879            ModelProvider::Anthropic { .. } => "anthropic",
880            ModelProvider::Vllora { .. } => "vllora",
881        }
882    }
883
884    /// Returns the required secret keys for this provider
885    pub fn required_secret_keys(&self) -> Vec<&'static str> {
886        match self {
887            ModelProvider::OpenAI {} => vec!["OPENAI_API_KEY"],
888            ModelProvider::OpenAICompatible { api_key, .. } => {
889                if api_key.is_some() {
890                    vec![]
891                } else {
892                    vec!["OPENAI_API_KEY"]
893                }
894            }
895            ModelProvider::AzureOpenAI { api_key, .. } => {
896                if api_key.is_some() {
897                    vec![]
898                } else {
899                    vec!["AZURE_OPENAI_API_KEY"]
900                }
901            }
902            ModelProvider::Anthropic { api_key, .. } => {
903                if api_key.is_some() {
904                    vec![]
905                } else {
906                    vec!["ANTHROPIC_API_KEY"]
907                }
908            }
909            ModelProvider::Vllora { .. } => vec![],
910        }
911    }
912
913    /// Returns all provider secret definitions, loaded from default_models.json.
914    pub fn all_provider_definitions() -> Vec<ProviderSecretDefinition> {
915        load_default_providers()
916            .iter()
917            .map(|p| ProviderSecretDefinition {
918                id: p.id.clone(),
919                label: p.label.clone(),
920                keys: p.keys.clone(),
921            })
922            .collect()
923    }
924
925    /// Returns the well-known models grouped by provider, loaded from default_models.json.
926    pub fn well_known_models() -> Vec<ProviderModels> {
927        load_default_providers()
928            .iter()
929            .filter(|p| !p.models.is_empty())
930            .map(|p| ProviderModels {
931                provider_id: p.id.clone(),
932                provider_label: p.label.clone(),
933                models: p.models.clone(),
934            })
935            .collect()
936    }
937
938    /// Get the human-readable name for a provider
939    pub fn display_name(&self) -> &'static str {
940        match self {
941            ModelProvider::OpenAI {} => "OpenAI",
942            ModelProvider::OpenAICompatible { .. } => "OpenAI Compatible",
943            ModelProvider::AzureOpenAI { .. } => "Azure OpenAI",
944            ModelProvider::Anthropic { .. } => "Anthropic",
945            ModelProvider::Vllora { .. } => "vLLORA",
946        }
947    }
948}
949
950/// Model settings configuration.
951/// A `ModelSettings` always has a valid model string.
952/// Use `Option<ModelSettings>` when no model is configured yet.
953#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
954pub struct ModelSettings {
955    pub model: String,
956    #[serde(flatten)]
957    pub inner: ModelSettingsInner,
958}
959
960/// Optional/defaultable model parameters. Split from `ModelSettings` so callers
961/// can construct `ModelSettings { model: "...", ..Default::default() }` easily
962/// via the `inner` field having `Default`.
963#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, Default)]
964pub struct ModelSettingsInner {
965    #[serde(default, skip_serializing_if = "Option::is_none")]
966    pub temperature: Option<f32>,
967    #[serde(default, skip_serializing_if = "Option::is_none")]
968    pub max_tokens: Option<u32>,
969    #[serde(default = "default_context_size")]
970    pub context_size: u32,
971    #[serde(default, skip_serializing_if = "Option::is_none")]
972    pub top_p: Option<f32>,
973    #[serde(default, skip_serializing_if = "Option::is_none")]
974    pub frequency_penalty: Option<f32>,
975    #[serde(default, skip_serializing_if = "Option::is_none")]
976    pub presence_penalty: Option<f32>,
977    #[serde(default = "default_model_provider")]
978    pub provider: ModelProvider,
979    /// Additional parameters for the agent, if any.
980    #[serde(default)]
981    pub parameters: Option<serde_json::Value>,
982    /// The format of the response, if specified.
983    #[serde(default)]
984    pub response_format: Option<serde_json::Value>,
985}
986
987impl ModelSettings {
988    /// Create a new `ModelSettings` with the given model and default inner settings.
989    pub fn new(model: impl Into<String>) -> Self {
990        Self {
991            model: model.into(),
992            inner: ModelSettingsInner::default(),
993        }
994    }
995
996    /// Parse a "provider/model" string (e.g. "anthropic/claude-sonnet-4") into ModelSettings.
997    /// Returns None if the format is invalid.
998    /// For custom providers (prefixed with "custom_"), returns an OpenAICompatible provider
999    /// with empty base_url/api_key — the caller must fill these from secrets/config.
1000    /// Parse "provider/model" string into ModelSettings.
1001    /// Returns Err with a descriptive message if the provider is not recognized.
1002    /// Returns Ok(None) if the input is empty or has no slash.
1003    pub fn from_provider_model_str(s: &str) -> Result<Option<Self>, String> {
1004        let Some((provider_str, model_id)) = s.split_once('/') else {
1005            return Ok(None);
1006        };
1007        if model_id.is_empty() {
1008            return Ok(None);
1009        }
1010        let provider = match provider_str {
1011            "openai" => ModelProvider::OpenAI {},
1012            "anthropic" => ModelProvider::Anthropic {
1013                base_url: None,
1014                api_key: None,
1015            },
1016            "azure_openai" => ModelProvider::AzureOpenAI {
1017                base_url: String::new(),
1018                api_key: None,
1019                deployment: model_id.to_string(),
1020                api_version: ModelProvider::azure_api_version(),
1021            },
1022            "gemini" => ModelProvider::OpenAICompatible {
1023                base_url: "https://generativelanguage.googleapis.com/v1beta/openai".to_string(),
1024                api_key: None,
1025                project_id: None,
1026            },
1027            _ if provider_str.starts_with("custom_") => ModelProvider::OpenAICompatible {
1028                base_url: String::new(),
1029                api_key: None,
1030                project_id: None,
1031            },
1032            unknown => {
1033                return Err(format!(
1034                    "Provider '{}' is not recognized. Supported providers: openai, anthropic, azure_openai, gemini, or custom_* prefixed providers.",
1035                    unknown
1036                ));
1037            }
1038        };
1039        Ok(Some(Self {
1040            model: model_id.to_string(),
1041            inner: ModelSettingsInner {
1042                provider,
1043                ..Default::default()
1044            },
1045        }))
1046    }
1047}
1048
1049// Default functions
1050pub fn default_agent_version() -> Option<String> {
1051    Some("0.2.2".to_string())
1052}
1053
1054fn default_model_provider() -> ModelProvider {
1055    ModelProvider::OpenAI {}
1056}
1057
1058fn default_context_size() -> u32 {
1059    20000 // Default limit for general use - agents can override with higher values as needed
1060}
1061
1062fn default_history_size() -> Option<usize> {
1063    Some(5)
1064}
1065
1066impl StandardDefinition {
1067    pub fn validate(&self) -> anyhow::Result<()> {
1068        // Basic validation - can be expanded
1069        if self.name.is_empty() {
1070            return Err(anyhow::anyhow!("Agent name cannot be empty"));
1071        }
1072
1073        // Validate reflection configuration
1074        if let Some(ref reflection) = self.reflection
1075            && reflection.enabled
1076        {
1077            // If a custom reflection_agent is specified, validate the name
1078            if let Some(ref agent_name) = reflection.reflection_agent
1079                && agent_name.is_empty()
1080            {
1081                return Err(anyhow::anyhow!(
1082                    "Reflection agent name cannot be empty when specified"
1083                ));
1084            }
1085        }
1086
1087        Ok(())
1088    }
1089
1090    /// Validate that a reflection agent definition has the "reflect" tool configured.
1091    /// This is called at registration time when we have access to the full agent config.
1092    pub fn validate_reflection_agent(agent_def: &StandardDefinition) -> anyhow::Result<()> {
1093        let has_reflect_tool = agent_def
1094            .tools
1095            .as_ref()
1096            .map(|t| t.builtin.iter().any(|name| name == "reflect"))
1097            .unwrap_or(false);
1098
1099        if !has_reflect_tool {
1100            // The built-in reflection_agent gets the reflect tool automatically,
1101            // but custom reflection agents must explicitly list it
1102            anyhow::bail!(
1103                "Reflection agent '{}' must have the 'reflect' tool in its tools.builtin configuration",
1104                agent_def.name
1105            );
1106        }
1107
1108        Ok(())
1109    }
1110}
1111
1112impl From<StandardDefinition> for LlmDefinition {
1113    fn from(definition: StandardDefinition) -> Self {
1114        let model_settings = match (definition.model_settings, definition.context_size) {
1115            (Some(mut ms), Some(ctx)) => {
1116                ms.inner.context_size = ctx;
1117                Some(ms)
1118            }
1119            (ms, _) => ms,
1120        };
1121
1122        Self {
1123            name: definition.name,
1124            model_settings,
1125            tool_format: definition.tool_format,
1126            tool_delivery_mode: definition.tool_delivery_mode,
1127        }
1128    }
1129}
1130
1131impl ToolsConfig {
1132    /// Create a simple configuration with just built-in tools
1133    pub fn builtin_only(tools: Vec<&str>) -> Self {
1134        Self {
1135            builtin: tools.into_iter().map(|s| s.to_string()).collect(),
1136            dynamic: vec![],
1137            mcp: vec![],
1138            external: None,
1139        }
1140    }
1141
1142    /// Create a configuration that includes all tools from an MCP server
1143    pub fn mcp_all(server: &str) -> Self {
1144        Self {
1145            builtin: vec![],
1146            dynamic: vec![],
1147            mcp: vec![McpToolConfig {
1148                server: server.to_string(),
1149                include: vec!["*".to_string()],
1150                exclude: vec![],
1151            }],
1152            external: None,
1153        }
1154    }
1155
1156    /// Create a configuration with specific MCP tool patterns
1157    pub fn mcp_filtered(server: &str, include: Vec<&str>, exclude: Vec<&str>) -> Self {
1158        Self {
1159            builtin: vec![],
1160            dynamic: vec![],
1161            mcp: vec![McpToolConfig {
1162                server: server.to_string(),
1163                include: include.into_iter().map(|s| s.to_string()).collect(),
1164                exclude: exclude.into_iter().map(|s| s.to_string()).collect(),
1165            }],
1166            external: None,
1167        }
1168    }
1169}
1170
1171pub async fn parse_agent_markdown_content(content: &str) -> Result<StandardDefinition, AgentError> {
1172    // Split by --- to separate TOML frontmatter from markdown content
1173    let parts: Vec<&str> = content.split("---").collect();
1174
1175    if parts.len() < 3 {
1176        return Err(AgentError::Validation(
1177            "Invalid agent markdown format. Expected TOML frontmatter between --- markers"
1178                .to_string(),
1179        ));
1180    }
1181
1182    // Parse TOML frontmatter (parts[1] is between the first two --- markers)
1183    let toml_content = parts[1].trim();
1184    let mut agent_def: crate::StandardDefinition =
1185        toml::from_str(toml_content).map_err(|e| AgentError::Validation(e.to_string()))?;
1186
1187    // Validate agent name format using centralized validation
1188    if let Err(validation_error) = validate_plugin_name(&agent_def.name) {
1189        return Err(AgentError::Validation(format!(
1190            "Invalid agent name '{}': {}",
1191            agent_def.name, validation_error
1192        )));
1193    }
1194
1195    // Validate that agent name is a valid JavaScript identifier
1196    if !agent_def
1197        .name
1198        .chars()
1199        .all(|c| c.is_alphanumeric() || c == '_')
1200        || agent_def
1201            .name
1202            .chars()
1203            .next()
1204            .is_some_and(|c| c.is_numeric())
1205    {
1206        return Err(AgentError::Validation(format!(
1207            "Invalid agent name '{}': Agent names must be valid JavaScript identifiers (alphanumeric + underscores, cannot start with number). \
1208                Reason: Agent names become function names in TypeScript runtime.",
1209            agent_def.name
1210        )));
1211    }
1212
1213    // Extract markdown instructions (everything after the second ---)
1214    let instructions = parts[2..].join("---").trim().to_string();
1215
1216    // Set the instructions in the agent definition
1217    agent_def.instructions = instructions;
1218
1219    Ok(agent_def)
1220}
1221
1222/// Validate plugin name follows naming conventions
1223/// Plugin names must be valid JavaScript identifiers (no hyphens)
1224pub fn validate_plugin_name(name: &str) -> Result<(), String> {
1225    if name.contains('-') {
1226        return Err(format!(
1227            "Plugin name '{}' cannot contain hyphens. Use underscores instead.",
1228            name
1229        ));
1230    }
1231
1232    if name.is_empty() {
1233        return Err("Plugin name cannot be empty".to_string());
1234    }
1235
1236    // Check if first character is valid for JavaScript identifier
1237    if let Some(first_char) = name.chars().next()
1238        && !first_char.is_ascii_alphabetic()
1239        && first_char != '_'
1240    {
1241        return Err(format!(
1242            "Plugin name '{}' must start with a letter or underscore",
1243            name
1244        ));
1245    }
1246
1247    // Check if all characters are valid for JavaScript identifier
1248    for ch in name.chars() {
1249        if !ch.is_ascii_alphanumeric() && ch != '_' {
1250            return Err(format!(
1251                "Plugin name '{}' can only contain letters, numbers, and underscores",
1252                name
1253            ));
1254        }
1255    }
1256
1257    Ok(())
1258}
1259
1260#[cfg(test)]
1261mod tests {
1262    use super::*;
1263
1264    #[test]
1265    fn test_compaction_enabled_defaults_to_true_via_serde() {
1266        // serde default uses default_compaction_enabled() -> true
1267        let json = r#"{"name": "test"}"#;
1268        let def: StandardDefinition = serde_json::from_str(json).unwrap();
1269        assert!(def.compaction_enabled);
1270    }
1271
1272    #[test]
1273    fn test_compaction_enabled_deserializes_true_when_absent() {
1274        let json = r#"{"name": "test", "description": "test agent"}"#;
1275        let def: StandardDefinition = serde_json::from_str(json).unwrap();
1276        assert!(def.compaction_enabled);
1277    }
1278
1279    #[test]
1280    fn test_compaction_enabled_deserializes_false() {
1281        let json = r#"{"name": "test", "description": "test agent", "compaction_enabled": false}"#;
1282        let def: StandardDefinition = serde_json::from_str(json).unwrap();
1283        assert!(!def.compaction_enabled);
1284    }
1285
1286    #[test]
1287    fn test_compaction_enabled_true_skipped_in_serialization() {
1288        let def = StandardDefinition {
1289            name: "test".to_string(),
1290            compaction_enabled: true,
1291            ..Default::default()
1292        };
1293        let json = serde_json::to_string(&def).unwrap();
1294        assert!(!json.contains("compaction_enabled"));
1295    }
1296
1297    #[test]
1298    fn test_compaction_enabled_false_serialized() {
1299        let def = StandardDefinition {
1300            name: "test".to_string(),
1301            compaction_enabled: false,
1302            ..Default::default()
1303        };
1304        let json = serde_json::to_string(&def).unwrap();
1305        assert!(json.contains("\"compaction_enabled\":false"));
1306    }
1307
1308    #[test]
1309    fn test_max_tokens_optional_defaults_to_none() {
1310        let def = StandardDefinition::default();
1311        assert!(def.model_settings().is_none());
1312    }
1313
1314    #[test]
1315    fn test_max_tokens_deserializes_when_present() {
1316        let json =
1317            r#"{"name": "test", "model_settings": {"model": "gpt-4.1", "max_tokens": 4096}}"#;
1318        let def: StandardDefinition = serde_json::from_str(json).unwrap();
1319        assert_eq!(def.model_settings().unwrap().inner.max_tokens, Some(4096));
1320    }
1321
1322    #[test]
1323    fn test_max_tokens_none_when_absent() {
1324        let json = r#"{"name": "test", "model_settings": {"model": "gpt-4.1"}}"#;
1325        let def: StandardDefinition = serde_json::from_str(json).unwrap();
1326        assert!(def.model_settings().unwrap().inner.max_tokens.is_none());
1327    }
1328
1329    #[test]
1330    fn test_max_tokens_none_skipped_in_serialization() {
1331        let settings = ModelSettings {
1332            model: "test-model".to_string(),
1333            inner: ModelSettingsInner {
1334                max_tokens: None,
1335                provider: ModelProvider::OpenAI {},
1336                ..Default::default()
1337            },
1338        };
1339        let json = serde_json::to_string(&settings).unwrap();
1340        assert!(!json.contains("max_tokens"));
1341    }
1342
1343    #[test]
1344    fn test_max_tokens_some_serialized() {
1345        let settings = ModelSettings {
1346            model: "test-model".to_string(),
1347            inner: ModelSettingsInner {
1348                max_tokens: Some(2048),
1349                provider: ModelProvider::OpenAI {},
1350                ..Default::default()
1351            },
1352        };
1353        let json = serde_json::to_string(&settings).unwrap();
1354        assert!(json.contains("\"max_tokens\":2048"));
1355    }
1356}