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, Serialize, Deserialize, Clone, JsonSchema, Default)]
256pub struct UserMessageOverrides {
257    /// The parts to include in the user message
258    pub parts: Vec<PartDefinition>,
259    /// If true, artifacts will be expanded to their actual content (e.g., image artifacts become Part::Image)
260    #[serde(default)]
261    pub include_artifacts: bool,
262    /// If true (default), step count information will be included at the end of the user message
263    #[serde(default = "default_include_step_count")]
264    pub include_step_count: Option<bool>,
265}
266
267fn default_include_step_count() -> Option<bool> {
268    Some(true)
269}
270
271#[derive(Debug, Serialize, Deserialize, Clone, JsonSchema)]
272#[serde(tag = "type", content = "source", rename_all = "snake_case")]
273pub enum PartDefinition {
274    Template(String),   // Prompt Template Key
275    SessionKey(String), // Session key reference
276}
277
278#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, Default)]
279#[serde(deny_unknown_fields)]
280pub struct LlmDefinition {
281    /// The name of the agent.
282    pub name: String,
283    /// Settings related to the model used by the agent.
284    #[serde(default)]
285    pub model_settings: ModelSettings,
286    /// Tool calling configuration
287    #[serde(default)]
288    pub tool_format: ToolCallFormat,
289}
290
291/// Hooks configuration to control how browser hooks interact with Browsr.
292#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
293#[serde(tag = "mode", rename_all = "snake_case")]
294pub enum BrowserHooksConfig {
295    /// Disable browser hooks entirely.
296    Disabled,
297    /// Send hook events to Browsr via HTTP/stdout.
298    Webhook {
299        /// Optional override base URL for the Browsr HTTP API.
300        #[serde(default, skip_serializing_if = "Option::is_none")]
301        api_base_url: Option<String>,
302    },
303    /// Await an inline hook completion (e.g., via POST /event/hooks) before continuing.
304    Inline {
305        /// Optional timeout in milliseconds to await the inline hook.
306        #[serde(default)]
307        timeout_ms: Option<u64>,
308    },
309}
310
311impl Default for BrowserHooksConfig {
312    fn default() -> Self {
313        BrowserHooksConfig::Disabled
314    }
315}
316
317/// Agent definition - complete configuration for an agent
318#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, Default)]
319#[serde(deny_unknown_fields)]
320pub struct StandardDefinition {
321    /// The name of the agent.
322    pub name: String,
323    /// Optional package name that registered this agent (workspace/plugin metadata)
324    #[serde(default, skip_serializing_if = "Option::is_none")]
325    pub package_name: Option<String>,
326    /// A brief description of the agent's purpose.
327    #[serde(default)]
328    pub description: String,
329
330    /// The version of the agent.
331    #[serde(default = "default_agent_version")]
332    pub version: Option<String>,
333
334    /// Instructions for the agent - serves as an introduction defining what the agent is and does.
335    #[serde(default)]
336    pub instructions: String,
337
338    /// A list of MCP server definitions associated with the agent.
339    #[serde(default)]
340    pub mcp_servers: Option<Vec<McpDefinition>>,
341    /// Settings related to the model used by the agent.
342    #[serde(default)]
343    pub model_settings: ModelSettings,
344    /// Optional lower-level model settings for lightweight analysis helpers
345    #[serde(default, skip_serializing_if = "Option::is_none")]
346    pub analysis_model_settings: Option<ModelSettings>,
347
348    /// The size of the history to maintain for the agent.
349    #[serde(default = "default_history_size")]
350    pub history_size: Option<usize>,
351    /// The new strategy configuration for the agent.
352    #[serde(default, skip_serializing_if = "Option::is_none")]
353    pub strategy: Option<AgentStrategy>,
354    /// A2A-specific fields
355    #[serde(default)]
356    pub icon_url: Option<String>,
357
358    #[serde(default, skip_serializing_if = "Option::is_none")]
359    pub max_iterations: Option<usize>,
360
361    #[serde(default, skip_serializing_if = "Vec::is_empty")]
362    pub skills: Vec<AgentSkill>,
363
364    /// List of sub-agents that this agent can transfer control to
365    #[serde(default)]
366    pub sub_agents: Vec<String>,
367
368    /// Tool calling configuration
369    #[serde(default)]
370    pub tool_format: ToolCallFormat,
371
372    /// Tools configuration for this agent
373    #[serde(default, skip_serializing_if = "Option::is_none")]
374    pub tools: Option<ToolsConfig>,
375
376    /// Where filesystem and artifact tools should run (server or local)
377    #[serde(default)]
378    pub file_system: FileSystemMode,
379
380    /// Custom handlebars partials (name -> template path) for use in custom prompts
381    #[serde(default, skip_serializing_if = "std::collections::HashMap::is_empty")]
382    pub partials: std::collections::HashMap<String, String>,
383
384    /// Whether to write large tool responses to filesystem as artifacts (default: false)
385    #[serde(default, skip_serializing_if = "Option::is_none")]
386    pub write_large_tool_responses_to_fs: Option<bool>,
387
388    /// Whether to enable reflection using a subagent (default: false)
389    #[serde(default, skip_serializing_if = "Option::is_none")]
390    pub enable_reflection: Option<bool>,
391    /// Whether to enable TODO management functionality
392    #[serde(default, skip_serializing_if = "Option::is_none")]
393    pub enable_todos: Option<bool>,
394
395    /// Browser configuration for this agent (enables shared Chromium automation)
396    #[serde(default, skip_serializing_if = "Option::is_none")]
397    pub browser_config: Option<BrowserAgentConfig>,
398    /// Browser hook configuration (API vs local)
399    #[serde(default, skip_serializing_if = "Option::is_none")]
400    pub browser_hooks: Option<BrowserHooksConfig>,
401
402    /// Context size override for this agent (overrides model_settings.context_size)
403    #[serde(default, skip_serializing_if = "Option::is_none")]
404    pub context_size: Option<u32>,
405
406    /// Strategy for prompt construction (append default template vs fully custom)
407    #[serde(
408        skip_serializing_if = "Option::is_none",
409        default = "default_append_default_instructions"
410    )]
411    pub append_default_instructions: Option<bool>,
412    /// Whether to include the built-in scratchpad/history in prompts (default: true)
413    #[serde(
414        skip_serializing_if = "Option::is_none",
415        default = "default_include_scratchpad"
416    )]
417    pub include_scratchpad: Option<bool>,
418
419    /// Optional hook names to attach to this agent
420    #[serde(default, skip_serializing_if = "Vec::is_empty")]
421    pub hooks: Vec<String>,
422 
423    /// Custom user message construction (dynamic prompting)
424    #[serde(default, skip_serializing_if = "Option::is_none")]
425    pub user_message_overrides: Option<UserMessageOverrides>,
426}
427fn default_append_default_instructions() -> Option<bool> {
428    Some(true)
429}
430fn default_include_scratchpad() -> Option<bool> {
431    Some(true)
432}
433impl StandardDefinition {
434    /// Check if large tool responses should be written to filesystem (default: false)
435    pub fn should_write_large_tool_responses_to_fs(&self) -> bool {
436        self.write_large_tool_responses_to_fs.unwrap_or(false)
437    }
438
439    /// Check if browser should be initialized automatically in orchestrator (default: false)
440    pub fn should_use_browser(&self) -> bool {
441        self.browser_config
442            .as_ref()
443            .map(|cfg| cfg.is_enabled())
444            .unwrap_or(false)
445    }
446
447    /// Returns browser config if defined
448    pub fn browser_settings(&self) -> Option<&BrowserAgentConfig> {
449        self.browser_config.as_ref()
450    }
451
452    /// Returns the runtime Chromium driver configuration if enabled
453    pub fn browser_runtime_config(&self) -> Option<DistriBrowserConfig> {
454        self.browser_config.as_ref().map(|cfg| cfg.runtime_config())
455    }
456
457    /// Should browser session state be serialized after tool runs
458    pub fn should_persist_browser_session(&self) -> bool {
459        self.browser_config
460            .as_ref()
461            .map(|cfg| cfg.should_persist_session())
462            .unwrap_or(false)
463    }
464
465    /// Check if reflection is enabled (default: false)
466    pub fn is_reflection_enabled(&self) -> bool {
467        self.enable_reflection.unwrap_or(false)
468    }
469    /// Check if TODO management functionality is enabled (default: false)
470    pub fn is_todos_enabled(&self) -> bool {
471        self.enable_todos.unwrap_or(false)
472    }
473
474    /// Get the effective context size (agent-level override or model settings)
475    pub fn get_effective_context_size(&self) -> u32 {
476        self.context_size
477            .unwrap_or(self.model_settings.context_size)
478    }
479
480    /// Model settings to use for lightweight browser analysis helpers (e.g., observe_summary commands)
481    pub fn analysis_model_settings_config(&self) -> ModelSettings {
482        self.analysis_model_settings
483            .clone()
484            .unwrap_or_else(|| self.model_settings.clone())
485    }
486
487    /// Whether to include the persistent scratchpad/history in prompts
488    pub fn include_scratchpad(&self) -> bool {
489        self.include_scratchpad.unwrap_or(true)
490    }
491
492    /// Apply definition overrides to this agent definition
493    pub fn apply_overrides(&mut self, overrides: DefinitionOverrides) {
494        // Override model settings
495        if let Some(model) = overrides.model {
496            self.model_settings.model = model;
497        }
498
499        if let Some(temperature) = overrides.temperature {
500            self.model_settings.temperature = temperature;
501        }
502
503        if let Some(max_tokens) = overrides.max_tokens {
504            self.model_settings.max_tokens = max_tokens;
505        }
506
507        // Override max_iterations
508        if let Some(max_iterations) = overrides.max_iterations {
509            self.max_iterations = Some(max_iterations);
510        }
511
512        // Override instructions
513        if let Some(instructions) = overrides.instructions {
514            self.instructions = instructions;
515        }
516
517        if let Some(use_browser) = overrides.use_browser {
518            let mut config = self.browser_config.clone().unwrap_or_default();
519            config.enabled = use_browser;
520            self.browser_config = Some(config);
521        }
522    }
523}
524
525/// Tools configuration for agents
526#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, Default)]
527#[serde(deny_unknown_fields)]
528pub struct ToolsConfig {
529    /// Built-in tools to include (e.g., ["final", "transfer_to_agent"])
530    #[serde(default, skip_serializing_if = "Vec::is_empty")]
531    pub builtin: Vec<String>,
532
533    /// DAP package tools: package_name -> list of tool names
534    #[serde(default, skip_serializing_if = "std::collections::HashMap::is_empty")]
535    pub packages: std::collections::HashMap<String, Vec<String>>,
536
537    /// MCP server tool configurations
538    #[serde(default, skip_serializing_if = "Vec::is_empty")]
539    pub mcp: Vec<McpToolConfig>,
540
541    /// External tools to include from client  
542    #[serde(default, skip_serializing_if = "Option::is_none")]
543    pub external: Option<Vec<String>>,
544}
545
546/// Where filesystem and artifact tools should execute
547#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, Default, PartialEq, Eq)]
548#[serde(rename_all = "snake_case")]
549pub enum FileSystemMode {
550    /// Run filesystem/artifact tools on the server (default)
551    #[default]
552    Remote,
553    /// Handle filesystem/artifact tools locally via external tool callbacks
554    Local,
555}
556
557impl FileSystemMode {
558    pub fn include_server_tools(&self) -> bool {
559        !matches!(self, FileSystemMode::Local)
560    }
561}
562
563/// Configuration for tools from an MCP server
564#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
565#[serde(deny_unknown_fields)]
566pub struct McpToolConfig {
567    /// Name of the MCP server
568    pub server: String,
569
570    /// Include patterns (glob-style, e.g., ["fetch_*", "extract_text"])
571    /// Use ["*"] to include all tools from the server
572    #[serde(default, skip_serializing_if = "Vec::is_empty")]
573    pub include: Vec<String>,
574
575    /// Exclude patterns (glob-style, e.g., ["delete_*", "rm_*"])
576    #[serde(default, skip_serializing_if = "Vec::is_empty")]
577    pub exclude: Vec<String>,
578}
579
580#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
581#[serde(deny_unknown_fields)]
582pub struct McpDefinition {
583    /// The filter applied to the tools in this MCP definition.
584    #[serde(default)]
585    pub filter: Option<Vec<String>>,
586    /// The name of the MCP server.
587    pub name: String,
588    /// The type of the MCP server (Tool or Agent).
589    #[serde(default)]
590    pub r#type: McpServerType,
591    /// Authentication configuration for this MCP server.
592    #[serde(default)]
593    pub auth_config: Option<crate::a2a::SecurityScheme>,
594}
595
596#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema, PartialEq)]
597#[serde(rename_all = "lowercase")]
598pub enum McpServerType {
599    #[default]
600    Tool,
601    Agent,
602}
603
604#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
605#[serde(deny_unknown_fields, rename_all = "lowercase", tag = "name")]
606pub enum ModelProvider {
607    #[serde(rename = "openai")]
608    OpenAI {},
609    #[serde(rename = "openai_compat")]
610    OpenAICompatible {
611        base_url: String,
612        api_key: Option<String>,
613        project_id: Option<String>,
614    },
615    #[serde(rename = "vllora")]
616    Vllora {
617        #[serde(default = "ModelProvider::vllora_url")]
618        base_url: String,
619    },
620}
621impl ModelProvider {
622    pub fn openai_base_url() -> String {
623        "https://api.openai.com/v1".to_string()
624    }
625
626    pub fn vllora_url() -> String {
627        "http://localhost:9090/v1".to_string()
628    }
629}
630
631/// Model settings configuration
632#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
633#[serde(deny_unknown_fields)]
634pub struct ModelSettings {
635    #[serde(default = "default_model")]
636    pub model: String,
637    #[serde(default = "default_temperature")]
638    pub temperature: f32,
639    #[serde(default = "default_max_tokens")]
640    pub max_tokens: u32,
641    #[serde(default = "default_context_size")]
642    pub context_size: u32,
643    #[serde(default = "default_top_p")]
644    pub top_p: f32,
645    #[serde(default = "default_frequency_penalty")]
646    pub frequency_penalty: f32,
647    #[serde(default = "default_presence_penalty")]
648    pub presence_penalty: f32,
649    #[serde(default = "default_model_provider")]
650    pub provider: ModelProvider,
651    /// Additional parameters for the agent, if any.
652    #[serde(default)]
653    pub parameters: Option<serde_json::Value>,
654    /// The format of the response, if specified.
655    #[serde(default)]
656    pub response_format: Option<serde_json::Value>,
657}
658
659impl Default for ModelSettings {
660    fn default() -> Self {
661        Self {
662            model: "gpt-4.1-mini".to_string(),
663            temperature: 0.7,
664            max_tokens: 1000,
665            context_size: 20000,
666            top_p: 1.0,
667            frequency_penalty: 0.0,
668            presence_penalty: 0.0,
669            provider: default_model_provider(),
670            parameters: None,
671            response_format: None,
672        }
673    }
674}
675
676// Default functions
677pub fn default_agent_version() -> Option<String> {
678    Some("0.2.2".to_string())
679}
680
681fn default_model_provider() -> ModelProvider {
682    ModelProvider::OpenAI {}
683}
684
685fn default_model() -> String {
686    "gpt-4.1-mini".to_string()
687}
688
689fn default_temperature() -> f32 {
690    0.7
691}
692
693fn default_max_tokens() -> u32 {
694    1000
695}
696
697fn default_context_size() -> u32 {
698    20000 // Default limit for general use - agents can override with higher values as needed
699}
700
701fn default_top_p() -> f32 {
702    1.0
703}
704
705fn default_frequency_penalty() -> f32 {
706    0.0
707}
708
709fn default_presence_penalty() -> f32 {
710    0.0
711}
712
713fn default_history_size() -> Option<usize> {
714    Some(5)
715}
716
717impl StandardDefinition {
718    pub fn validate(&self) -> anyhow::Result<()> {
719        // Basic validation - can be expanded
720        if self.name.is_empty() {
721            return Err(anyhow::anyhow!("Agent name cannot be empty"));
722        }
723        Ok(())
724    }
725}
726
727impl From<StandardDefinition> for LlmDefinition {
728    fn from(definition: StandardDefinition) -> Self {
729        let mut model_settings = definition.model_settings.clone();
730        // Use agent-level context_size override if provided
731        if let Some(context_size) = definition.context_size {
732            model_settings.context_size = context_size;
733        }
734
735        Self {
736            name: definition.name,
737            model_settings,
738            tool_format: definition.tool_format,
739        }
740    }
741}
742
743impl ToolsConfig {
744    /// Create a simple configuration with just built-in tools
745    pub fn builtin_only(tools: Vec<&str>) -> Self {
746        Self {
747            builtin: tools.into_iter().map(|s| s.to_string()).collect(),
748            packages: std::collections::HashMap::new(),
749            mcp: vec![],
750            external: None,
751        }
752    }
753
754    /// Create a configuration that includes all tools from an MCP server
755    pub fn mcp_all(server: &str) -> Self {
756        Self {
757            builtin: vec![],
758            packages: std::collections::HashMap::new(),
759            mcp: vec![McpToolConfig {
760                server: server.to_string(),
761                include: vec!["*".to_string()],
762                exclude: vec![],
763            }],
764            external: None,
765        }
766    }
767
768    /// Create a configuration with specific MCP tool patterns
769    pub fn mcp_filtered(server: &str, include: Vec<&str>, exclude: Vec<&str>) -> Self {
770        Self {
771            builtin: vec![],
772            packages: std::collections::HashMap::new(),
773            mcp: vec![McpToolConfig {
774                server: server.to_string(),
775                include: include.into_iter().map(|s| s.to_string()).collect(),
776                exclude: exclude.into_iter().map(|s| s.to_string()).collect(),
777            }],
778            external: None,
779        }
780    }
781}
782
783pub async fn parse_agent_markdown_content(content: &str) -> Result<StandardDefinition, AgentError> {
784    // Split by --- to separate TOML frontmatter from markdown content
785    let parts: Vec<&str> = content.split("---").collect();
786
787    if parts.len() < 3 {
788        return Err(AgentError::Validation(
789            "Invalid agent markdown format. Expected TOML frontmatter between --- markers"
790                .to_string(),
791        ));
792    }
793
794    // Parse TOML frontmatter (parts[1] is between the first two --- markers)
795    let toml_content = parts[1].trim();
796    let mut agent_def: crate::StandardDefinition =
797        toml::from_str(toml_content).map_err(|e| AgentError::Validation(e.to_string()))?;
798
799    // Validate agent name format using centralized validation
800    if let Err(validation_error) = validate_plugin_name(&agent_def.name) {
801        return Err(AgentError::Validation(format!(
802            "Invalid agent name '{}': {}",
803            agent_def.name, validation_error
804        )));
805    }
806
807    // Validate that agent name is a valid JavaScript identifier
808    if !agent_def
809        .name
810        .chars()
811        .all(|c| c.is_alphanumeric() || c == '_')
812        || agent_def
813            .name
814            .chars()
815            .next()
816            .map_or(false, |c| c.is_numeric())
817    {
818        return Err(AgentError::Validation(format!(
819            "Invalid agent name '{}': Agent names must be valid JavaScript identifiers (alphanumeric + underscores, cannot start with number). \
820                Reason: Agent names become function names in TypeScript runtime.",
821            agent_def.name
822        )));
823    }
824
825    // Extract markdown instructions (everything after the second ---)
826    let instructions = parts[2..].join("---").trim().to_string();
827
828    // Set the instructions in the agent definition
829    agent_def.instructions = instructions;
830
831    Ok(agent_def)
832}
833
834/// Validate plugin name follows naming conventions
835/// Plugin names must be valid JavaScript identifiers (no hyphens)
836pub fn validate_plugin_name(name: &str) -> Result<(), String> {
837    if name.contains('-') {
838        return Err(format!(
839            "Plugin name '{}' cannot contain hyphens. Use underscores instead.",
840            name
841        ));
842    }
843
844    if name.is_empty() {
845        return Err("Plugin name cannot be empty".to_string());
846    }
847
848    // Check if first character is valid for JavaScript identifier
849    if let Some(first_char) = name.chars().next() {
850        if !first_char.is_ascii_alphabetic() && first_char != '_' {
851            return Err(format!(
852                "Plugin name '{}' must start with a letter or underscore",
853                name
854            ));
855        }
856    }
857
858    // Check if all characters are valid for JavaScript identifier
859    for ch in name.chars() {
860        if !ch.is_ascii_alphanumeric() && ch != '_' {
861            return Err(format!(
862                "Plugin name '{}' can only contain letters, numbers, and underscores",
863                name
864            ));
865        }
866    }
867
868    Ok(())
869}