distri_types/
agent.rs

1use crate::AgentError;
2use crate::a2a::AgentSkill;
3use crate::browser::{BrowserAgentConfig, DistriBrowserConfig};
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/// Unified Agent Strategy Configuration
13#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
14#[serde(deny_unknown_fields, rename_all = "snake_case")]
15pub struct AgentStrategy {
16    /// Depth of reasoning (shallow, standard, deep)
17    #[serde(default, skip_serializing_if = "Option::is_none")]
18    pub reasoning_depth: Option<ReasoningDepth>,
19
20    /// Execution mode - tools vs code
21    #[serde(default, skip_serializing_if = "Option::is_none")]
22    pub execution_mode: Option<ExecutionMode>,
23    /// When to replan
24    #[serde(default, skip_serializing_if = "Option::is_none")]
25    pub replanning: Option<ReplanningConfig>,
26
27    /// Timeout in seconds for external tool execution (default: 120)
28    /// External tools are tools that delegate execution to the frontend/client.
29    #[serde(default, skip_serializing_if = "Option::is_none")]
30    pub external_tool_timeout_secs: Option<u64>,
31}
32
33impl Default for AgentStrategy {
34    fn default() -> Self {
35        Self {
36            reasoning_depth: None,
37            execution_mode: None,
38            replanning: None,
39            external_tool_timeout_secs: None,
40        }
41    }
42}
43
44impl AgentStrategy {
45    /// Get reasoning depth with default fallback
46    pub fn get_reasoning_depth(&self) -> ReasoningDepth {
47        self.reasoning_depth.clone().unwrap_or_default()
48    }
49
50    /// Get execution mode with default fallback
51    pub fn get_execution_mode(&self) -> ExecutionMode {
52        self.execution_mode.clone().unwrap_or_default()
53    }
54
55    /// Get replanning config with default fallback
56    pub fn get_replanning(&self) -> ReplanningConfig {
57        self.replanning.clone().unwrap_or_default()
58    }
59
60    /// Get external tool timeout with default fallback
61    pub fn get_external_tool_timeout_secs(&self) -> u64 {
62        self.external_tool_timeout_secs
63            .unwrap_or(DEFAULT_EXTERNAL_TOOL_TIMEOUT_SECS)
64    }
65}
66
67#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, Default, PartialEq)]
68#[serde(rename_all = "snake_case")]
69pub enum CodeLanguage {
70    #[default]
71    Typescript,
72}
73
74impl std::fmt::Display for CodeLanguage {
75    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
76        write!(f, "{}", self.to_string())
77    }
78}
79
80impl CodeLanguage {
81    pub fn to_string(&self) -> String {
82        match self {
83            CodeLanguage::Typescript => "typescript".to_string(),
84        }
85    }
86}
87
88/// Reflection configuration
89#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, Default)]
90pub struct ReflectionConfig {
91    /// Whether to enable reflection
92    #[serde(default)]
93    pub enabled: bool,
94    /// When to trigger reflection
95    #[serde(default)]
96    pub trigger: ReflectionTrigger,
97    /// Depth of reflection
98    #[serde(default)]
99    pub depth: ReflectionDepth,
100}
101
102/// When to trigger reflection
103#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, Default)]
104#[serde(rename_all = "snake_case")]
105pub enum ReflectionTrigger {
106    /// At the end of execution
107    #[default]
108    EndOfExecution,
109    /// After each step
110    AfterEachStep,
111    /// After failures only
112    AfterFailures,
113    /// After N steps
114    AfterNSteps(usize),
115}
116
117/// Depth of reflection
118#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, Default)]
119#[serde(rename_all = "snake_case")]
120pub enum ReflectionDepth {
121    /// Light reflection
122    #[default]
123    Light,
124    /// Standard reflection
125    Standard,
126    /// Deep reflection
127    Deep,
128}
129
130/// Configuration for planning operations
131#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
132pub struct PlanConfig {
133    /// The model settings for the planning agent
134    #[serde(default)]
135    pub model_settings: ModelSettings,
136    /// The maximum number of iterations allowed during planning
137    #[serde(default = "default_plan_max_iterations")]
138    pub max_iterations: usize,
139}
140
141impl Default for PlanConfig {
142    fn default() -> Self {
143        Self {
144            model_settings: ModelSettings::default(),
145            max_iterations: default_plan_max_iterations(),
146        }
147    }
148}
149
150fn default_plan_max_iterations() -> usize {
151    10
152}
153
154/// Depth of reasoning for planning
155#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, Default, PartialEq)]
156#[serde(rename_all = "snake_case")]
157pub enum ReasoningDepth {
158    /// Shallow reasoning - direct action with minimal thought, skip reasoning sections
159    Shallow,
160    /// Standard reasoning - moderate planning and thought
161    #[default]
162    Standard,
163    /// Deep reasoning - extensive planning, multi-step analysis, and comprehensive thinking
164    Deep,
165}
166
167/// Execution mode - tools vs code
168#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, Default, PartialEq)]
169#[serde(rename_all = "snake_case", tag = "type")]
170pub enum ExecutionMode {
171    /// Use tools for execution
172    #[default]
173    Tools,
174    /// Use code execution
175    Code { language: CodeLanguage },
176}
177
178/// Replanning configuration
179#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, Default)]
180#[serde(rename_all = "snake_case")]
181pub struct ReplanningConfig {
182    /// When to trigger replanning
183    #[serde(default, skip_serializing_if = "Option::is_none")]
184    pub trigger: Option<ReplanningTrigger>,
185    /// Whether to replan at all
186    #[serde(default, skip_serializing_if = "Option::is_none")]
187    pub enabled: Option<bool>,
188}
189
190/// When to trigger replanning
191#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, Default)]
192#[serde(rename_all = "snake_case")]
193pub enum ReplanningTrigger {
194    /// Never replan (default)
195    #[default]
196    Never,
197    /// Replan after execution reflection
198    AfterReflection,
199    /// Replan after N iterations
200    AfterNIterations(usize),
201    /// Replan after failures
202    AfterFailures,
203}
204
205impl ReplanningConfig {
206    /// Get trigger with default fallback
207    pub fn get_trigger(&self) -> ReplanningTrigger {
208        self.trigger.clone().unwrap_or_default()
209    }
210
211    /// Get enabled with default fallback
212    pub fn is_enabled(&self) -> bool {
213        self.enabled.unwrap_or(false)
214    }
215}
216
217#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, Default)]
218#[serde(rename_all = "snake_case")]
219pub enum ExecutionKind {
220    #[default]
221    Retriable,
222    Interleaved,
223    Sequential,
224}
225
226#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, Default)]
227#[serde(rename_all = "snake_case")]
228pub enum MemoryKind {
229    #[default]
230    None,
231    ShortTerm,
232    LongTerm,
233}
234
235/// Supported tool call formats
236#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, Default, PartialEq)]
237#[serde(rename_all = "snake_case")]
238pub enum ToolCallFormat {
239    /// New XML format: Streaming-capable XML tool calls
240    /// Example: <search><query>test</query></search>
241    #[default]
242    Xml,
243    /// New JSON format: JSONL with tool_calls blocks
244    /// Example: ```tool_calls\n{"name":"search","arguments":{"query":"test"}}```
245    JsonL,
246
247    /// Code execution format: TypeScript/JavaScript code blocks
248    /// Example: ```typescript ... ```
249    Code,
250    #[serde(rename = "provider")]
251    Provider,
252    None,
253}
254
255#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, Default)]
256#[serde(deny_unknown_fields)]
257pub struct LlmDefinition {
258    /// The name of the agent.
259    pub name: String,
260    /// Settings related to the model used by the agent.
261    #[serde(default)]
262    pub model_settings: ModelSettings,
263    /// Tool calling configuration
264    #[serde(default)]
265    pub tool_format: ToolCallFormat,
266}
267
268/// Hooks configuration to control how browser hooks interact with Browsr.
269#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
270#[serde(tag = "mode", rename_all = "snake_case")]
271pub enum BrowserHooksConfig {
272    /// Disable browser hooks entirely.
273    Disabled,
274    /// Send hook events to Browsr via HTTP/stdout.
275    Webhook {
276        /// Optional override base URL for the Browsr HTTP API.
277        #[serde(default, skip_serializing_if = "Option::is_none")]
278        api_base_url: Option<String>,
279    },
280    /// Await an inline hook completion (e.g., via POST /event/hooks) before continuing.
281    Inline {
282        /// Optional timeout in milliseconds to await the inline hook.
283        #[serde(default)]
284        timeout_ms: Option<u64>,
285    },
286}
287
288impl Default for BrowserHooksConfig {
289    fn default() -> Self {
290        BrowserHooksConfig::Disabled
291    }
292}
293
294/// Agent definition - complete configuration for an agent
295#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, Default)]
296#[serde(deny_unknown_fields)]
297pub struct StandardDefinition {
298    /// The name of the agent.
299    pub name: String,
300    /// Optional package name that registered this agent (workspace/plugin metadata)
301    #[serde(default, skip_serializing_if = "Option::is_none")]
302    pub package_name: Option<String>,
303    /// A brief description of the agent's purpose.
304    #[serde(default)]
305    pub description: String,
306
307    /// The version of the agent.
308    #[serde(default = "default_agent_version")]
309    pub version: Option<String>,
310
311    /// Instructions for the agent - serves as an introduction defining what the agent is and does.
312    #[serde(default)]
313    pub instructions: String,
314
315    /// A list of MCP server definitions associated with the agent.
316    #[serde(default)]
317    pub mcp_servers: Option<Vec<McpDefinition>>,
318    /// Settings related to the model used by the agent.
319    #[serde(default)]
320    pub model_settings: ModelSettings,
321    /// Optional lower-level model settings for lightweight analysis helpers
322    #[serde(default, skip_serializing_if = "Option::is_none")]
323    pub analysis_model_settings: Option<ModelSettings>,
324
325    /// The size of the history to maintain for the agent.
326    #[serde(default = "default_history_size")]
327    pub history_size: Option<usize>,
328    /// The new strategy configuration for the agent.
329    #[serde(default, skip_serializing_if = "Option::is_none")]
330    pub strategy: Option<AgentStrategy>,
331    /// A2A-specific fields
332    #[serde(default)]
333    pub icon_url: Option<String>,
334
335    #[serde(default, skip_serializing_if = "Option::is_none")]
336    pub max_iterations: Option<usize>,
337
338    #[serde(default, skip_serializing_if = "Vec::is_empty")]
339    pub skills: Vec<AgentSkill>,
340
341    /// List of sub-agents that this agent can transfer control to
342    #[serde(default)]
343    pub sub_agents: Vec<String>,
344
345    /// Tool calling configuration
346    #[serde(default)]
347    pub tool_format: ToolCallFormat,
348
349    /// Tools configuration for this agent
350    #[serde(default, skip_serializing_if = "Option::is_none")]
351    pub tools: Option<ToolsConfig>,
352
353    /// Where filesystem and artifact tools should run (server or local)
354    #[serde(default)]
355    pub file_system: FileSystemMode,
356
357    /// Custom handlebars partials (name -> template path) for use in custom prompts
358    #[serde(default, skip_serializing_if = "std::collections::HashMap::is_empty")]
359    pub partials: std::collections::HashMap<String, String>,
360
361    /// Whether to write large tool responses to filesystem as artifacts (default: false)
362    #[serde(default, skip_serializing_if = "Option::is_none")]
363    pub write_large_tool_responses_to_fs: Option<bool>,
364
365    /// Whether to enable reflection using a subagent (default: false)
366    #[serde(default, skip_serializing_if = "Option::is_none")]
367    pub enable_reflection: Option<bool>,
368    /// Whether to enable TODO management functionality
369    #[serde(default, skip_serializing_if = "Option::is_none")]
370    pub enable_todos: Option<bool>,
371
372    /// Browser configuration for this agent (enables shared Chromium automation)
373    #[serde(default, skip_serializing_if = "Option::is_none")]
374    pub browser_config: Option<BrowserAgentConfig>,
375    /// Browser hook configuration (API vs local)
376    #[serde(default, skip_serializing_if = "Option::is_none")]
377    pub browser_hooks: Option<BrowserHooksConfig>,
378
379    /// Context size override for this agent (overrides model_settings.context_size)
380    #[serde(default, skip_serializing_if = "Option::is_none")]
381    pub context_size: Option<u32>,
382
383    /// Strategy for prompt construction (append default template vs fully custom)
384    #[serde(
385        skip_serializing_if = "Option::is_none",
386        default = "default_append_default_instructions"
387    )]
388    pub append_default_instructions: Option<bool>,
389    /// Whether to include the built-in scratchpad/history in prompts (default: true)
390    #[serde(
391        skip_serializing_if = "Option::is_none",
392        default = "default_include_scratchpad"
393    )]
394    pub include_scratchpad: Option<bool>,
395
396    /// Optional hook names to attach to this agent
397    #[serde(default, skip_serializing_if = "Vec::is_empty")]
398    pub hooks: Vec<String>,
399}
400fn default_append_default_instructions() -> Option<bool> {
401    Some(true)
402}
403fn default_include_scratchpad() -> Option<bool> {
404    Some(true)
405}
406impl StandardDefinition {
407    /// Check if large tool responses should be written to filesystem (default: false)
408    pub fn should_write_large_tool_responses_to_fs(&self) -> bool {
409        self.write_large_tool_responses_to_fs.unwrap_or(false)
410    }
411
412    /// Check if browser should be initialized automatically in orchestrator (default: false)
413    pub fn should_use_browser(&self) -> bool {
414        self.browser_config
415            .as_ref()
416            .map(|cfg| cfg.is_enabled())
417            .unwrap_or(false)
418    }
419
420    /// Returns browser config if defined
421    pub fn browser_settings(&self) -> Option<&BrowserAgentConfig> {
422        self.browser_config.as_ref()
423    }
424
425    /// Returns the runtime Chromium driver configuration if enabled
426    pub fn browser_runtime_config(&self) -> Option<DistriBrowserConfig> {
427        self.browser_config.as_ref().map(|cfg| cfg.runtime_config())
428    }
429
430    /// Should browser session state be serialized after tool runs
431    pub fn should_persist_browser_session(&self) -> bool {
432        self.browser_config
433            .as_ref()
434            .map(|cfg| cfg.should_persist_session())
435            .unwrap_or(false)
436    }
437
438    /// Check if reflection is enabled (default: false)
439    pub fn is_reflection_enabled(&self) -> bool {
440        self.enable_reflection.unwrap_or(false)
441    }
442    /// Check if TODO management functionality is enabled (default: false)
443    pub fn is_todos_enabled(&self) -> bool {
444        self.enable_todos.unwrap_or(false)
445    }
446
447    /// Get the effective context size (agent-level override or model settings)
448    pub fn get_effective_context_size(&self) -> u32 {
449        self.context_size
450            .unwrap_or(self.model_settings.context_size)
451    }
452
453    /// Model settings to use for lightweight browser analysis helpers (e.g., observe_summary commands)
454    pub fn analysis_model_settings_config(&self) -> ModelSettings {
455        self.analysis_model_settings
456            .clone()
457            .unwrap_or_else(|| self.model_settings.clone())
458    }
459
460    /// Whether to include the persistent scratchpad/history in prompts
461    pub fn include_scratchpad(&self) -> bool {
462        self.include_scratchpad.unwrap_or(true)
463    }
464
465    /// Apply definition overrides to this agent definition
466    pub fn apply_overrides(&mut self, overrides: DefinitionOverrides) {
467        // Override model settings
468        if let Some(model) = overrides.model {
469            self.model_settings.model = model;
470        }
471
472        if let Some(temperature) = overrides.temperature {
473            self.model_settings.temperature = temperature;
474        }
475
476        if let Some(max_tokens) = overrides.max_tokens {
477            self.model_settings.max_tokens = max_tokens;
478        }
479
480        // Override max_iterations
481        if let Some(max_iterations) = overrides.max_iterations {
482            self.max_iterations = Some(max_iterations);
483        }
484
485        // Override instructions
486        if let Some(instructions) = overrides.instructions {
487            self.instructions = instructions;
488        }
489
490        if let Some(use_browser) = overrides.use_browser {
491            let mut config = self.browser_config.clone().unwrap_or_default();
492            config.enabled = use_browser;
493            self.browser_config = Some(config);
494        }
495    }
496}
497
498/// Tools configuration for agents
499#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, Default)]
500#[serde(deny_unknown_fields)]
501pub struct ToolsConfig {
502    /// Built-in tools to include (e.g., ["final", "transfer_to_agent"])
503    #[serde(default, skip_serializing_if = "Vec::is_empty")]
504    pub builtin: Vec<String>,
505
506    /// DAP package tools: package_name -> list of tool names
507    #[serde(default, skip_serializing_if = "std::collections::HashMap::is_empty")]
508    pub packages: std::collections::HashMap<String, Vec<String>>,
509
510    /// MCP server tool configurations
511    #[serde(default, skip_serializing_if = "Vec::is_empty")]
512    pub mcp: Vec<McpToolConfig>,
513
514    /// External tools to include from client  
515    #[serde(default, skip_serializing_if = "Option::is_none")]
516    pub external: Option<Vec<String>>,
517}
518
519/// Where filesystem and artifact tools should execute
520#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, Default, PartialEq, Eq)]
521#[serde(rename_all = "snake_case")]
522pub enum FileSystemMode {
523    /// Run filesystem/artifact tools on the server (default)
524    #[default]
525    Remote,
526    /// Handle filesystem/artifact tools locally via external tool callbacks
527    Local,
528}
529
530impl FileSystemMode {
531    pub fn include_server_tools(&self) -> bool {
532        !matches!(self, FileSystemMode::Local)
533    }
534}
535
536/// Configuration for tools from an MCP server
537#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
538#[serde(deny_unknown_fields)]
539pub struct McpToolConfig {
540    /// Name of the MCP server
541    pub server: String,
542
543    /// Include patterns (glob-style, e.g., ["fetch_*", "extract_text"])
544    /// Use ["*"] to include all tools from the server
545    #[serde(default, skip_serializing_if = "Vec::is_empty")]
546    pub include: Vec<String>,
547
548    /// Exclude patterns (glob-style, e.g., ["delete_*", "rm_*"])
549    #[serde(default, skip_serializing_if = "Vec::is_empty")]
550    pub exclude: Vec<String>,
551}
552
553#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
554#[serde(deny_unknown_fields)]
555pub struct McpDefinition {
556    /// The filter applied to the tools in this MCP definition.
557    #[serde(default)]
558    pub filter: Option<Vec<String>>,
559    /// The name of the MCP server.
560    pub name: String,
561    /// The type of the MCP server (Tool or Agent).
562    #[serde(default)]
563    pub r#type: McpServerType,
564    /// Authentication configuration for this MCP server.
565    #[serde(default)]
566    pub auth_config: Option<crate::a2a::SecurityScheme>,
567}
568
569#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema, PartialEq)]
570#[serde(rename_all = "lowercase")]
571pub enum McpServerType {
572    #[default]
573    Tool,
574    Agent,
575}
576
577#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
578#[serde(deny_unknown_fields, rename_all = "lowercase", tag = "name")]
579pub enum ModelProvider {
580    #[serde(rename = "openai")]
581    OpenAI {},
582    #[serde(rename = "openai_compat")]
583    OpenAICompatible {
584        base_url: String,
585        api_key: Option<String>,
586        project_id: Option<String>,
587    },
588    #[serde(rename = "vllora")]
589    Vllora {
590        #[serde(default = "ModelProvider::vllora_url")]
591        base_url: String,
592    },
593}
594impl ModelProvider {
595    pub fn openai_base_url() -> String {
596        "https://api.openai.com/v1".to_string()
597    }
598
599    pub fn vllora_url() -> String {
600        "http://localhost:9090/v1".to_string()
601    }
602}
603
604/// Model settings configuration
605#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
606#[serde(deny_unknown_fields)]
607pub struct ModelSettings {
608    #[serde(default = "default_model")]
609    pub model: String,
610    #[serde(default = "default_temperature")]
611    pub temperature: f32,
612    #[serde(default = "default_max_tokens")]
613    pub max_tokens: u32,
614    #[serde(default = "default_context_size")]
615    pub context_size: u32,
616    #[serde(default = "default_top_p")]
617    pub top_p: f32,
618    #[serde(default = "default_frequency_penalty")]
619    pub frequency_penalty: f32,
620    #[serde(default = "default_presence_penalty")]
621    pub presence_penalty: f32,
622    #[serde(default = "default_model_provider")]
623    pub provider: ModelProvider,
624    /// Additional parameters for the agent, if any.
625    #[serde(default)]
626    pub parameters: Option<serde_json::Value>,
627    /// The format of the response, if specified.
628    #[serde(default)]
629    pub response_format: Option<serde_json::Value>,
630}
631
632impl Default for ModelSettings {
633    fn default() -> Self {
634        Self {
635            model: "gpt-4.1-mini".to_string(),
636            temperature: 0.7,
637            max_tokens: 1000,
638            context_size: 20000,
639            top_p: 1.0,
640            frequency_penalty: 0.0,
641            presence_penalty: 0.0,
642            provider: default_model_provider(),
643            parameters: None,
644            response_format: None,
645        }
646    }
647}
648
649// Default functions
650pub fn default_agent_version() -> Option<String> {
651    Some("0.2.2".to_string())
652}
653
654fn default_model_provider() -> ModelProvider {
655    ModelProvider::OpenAI {}
656}
657
658fn default_model() -> String {
659    "gpt-4.1-mini".to_string()
660}
661
662fn default_temperature() -> f32 {
663    0.7
664}
665
666fn default_max_tokens() -> u32 {
667    1000
668}
669
670fn default_context_size() -> u32 {
671    20000 // Default limit for general use - agents can override with higher values as needed
672}
673
674fn default_top_p() -> f32 {
675    1.0
676}
677
678fn default_frequency_penalty() -> f32 {
679    0.0
680}
681
682fn default_presence_penalty() -> f32 {
683    0.0
684}
685
686fn default_history_size() -> Option<usize> {
687    Some(5)
688}
689
690impl StandardDefinition {
691    pub fn validate(&self) -> anyhow::Result<()> {
692        // Basic validation - can be expanded
693        if self.name.is_empty() {
694            return Err(anyhow::anyhow!("Agent name cannot be empty"));
695        }
696        Ok(())
697    }
698}
699
700impl From<StandardDefinition> for LlmDefinition {
701    fn from(definition: StandardDefinition) -> Self {
702        let mut model_settings = definition.model_settings.clone();
703        // Use agent-level context_size override if provided
704        if let Some(context_size) = definition.context_size {
705            model_settings.context_size = context_size;
706        }
707
708        Self {
709            name: definition.name,
710            model_settings,
711            tool_format: definition.tool_format,
712        }
713    }
714}
715
716impl ToolsConfig {
717    /// Create a simple configuration with just built-in tools
718    pub fn builtin_only(tools: Vec<&str>) -> Self {
719        Self {
720            builtin: tools.into_iter().map(|s| s.to_string()).collect(),
721            packages: std::collections::HashMap::new(),
722            mcp: vec![],
723            external: None,
724        }
725    }
726
727    /// Create a configuration that includes all tools from an MCP server
728    pub fn mcp_all(server: &str) -> Self {
729        Self {
730            builtin: vec![],
731            packages: std::collections::HashMap::new(),
732            mcp: vec![McpToolConfig {
733                server: server.to_string(),
734                include: vec!["*".to_string()],
735                exclude: vec![],
736            }],
737            external: None,
738        }
739    }
740
741    /// Create a configuration with specific MCP tool patterns
742    pub fn mcp_filtered(server: &str, include: Vec<&str>, exclude: Vec<&str>) -> Self {
743        Self {
744            builtin: vec![],
745            packages: std::collections::HashMap::new(),
746            mcp: vec![McpToolConfig {
747                server: server.to_string(),
748                include: include.into_iter().map(|s| s.to_string()).collect(),
749                exclude: exclude.into_iter().map(|s| s.to_string()).collect(),
750            }],
751            external: None,
752        }
753    }
754}
755
756pub async fn parse_agent_markdown_content(content: &str) -> Result<StandardDefinition, AgentError> {
757    // Split by --- to separate TOML frontmatter from markdown content
758    let parts: Vec<&str> = content.split("---").collect();
759
760    if parts.len() < 3 {
761        return Err(AgentError::Validation(
762            "Invalid agent markdown format. Expected TOML frontmatter between --- markers"
763                .to_string(),
764        ));
765    }
766
767    // Parse TOML frontmatter (parts[1] is between the first two --- markers)
768    let toml_content = parts[1].trim();
769    let mut agent_def: crate::StandardDefinition =
770        toml::from_str(toml_content).map_err(|e| AgentError::Validation(e.to_string()))?;
771
772    // Validate agent name format using centralized validation
773    if let Err(validation_error) = validate_plugin_name(&agent_def.name) {
774        return Err(AgentError::Validation(format!(
775            "Invalid agent name '{}': {}",
776            agent_def.name, validation_error
777        )));
778    }
779
780    // Validate that agent name is a valid JavaScript identifier
781    if !agent_def
782        .name
783        .chars()
784        .all(|c| c.is_alphanumeric() || c == '_')
785        || agent_def
786            .name
787            .chars()
788            .next()
789            .map_or(false, |c| c.is_numeric())
790    {
791        return Err(AgentError::Validation(format!(
792            "Invalid agent name '{}': Agent names must be valid JavaScript identifiers (alphanumeric + underscores, cannot start with number). \
793                Reason: Agent names become function names in TypeScript runtime.",
794            agent_def.name
795        )));
796    }
797
798    // Extract markdown instructions (everything after the second ---)
799    let instructions = parts[2..].join("---").trim().to_string();
800
801    // Set the instructions in the agent definition
802    agent_def.instructions = instructions;
803
804    Ok(agent_def)
805}
806
807/// Validate plugin name follows naming conventions
808/// Plugin names must be valid JavaScript identifiers (no hyphens)
809pub fn validate_plugin_name(name: &str) -> Result<(), String> {
810    if name.contains('-') {
811        return Err(format!(
812            "Plugin name '{}' cannot contain hyphens. Use underscores instead.",
813            name
814        ));
815    }
816
817    if name.is_empty() {
818        return Err("Plugin name cannot be empty".to_string());
819    }
820
821    // Check if first character is valid for JavaScript identifier
822    if let Some(first_char) = name.chars().next() {
823        if !first_char.is_ascii_alphabetic() && first_char != '_' {
824            return Err(format!(
825                "Plugin name '{}' must start with a letter or underscore",
826                name
827            ));
828        }
829    }
830
831    // Check if all characters are valid for JavaScript identifier
832    for ch in name.chars() {
833        if !ch.is_ascii_alphanumeric() && ch != '_' {
834            return Err(format!(
835                "Plugin name '{}' can only contain letters, numbers, and underscores",
836                name
837            ));
838        }
839    }
840
841    Ok(())
842}