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/// 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<BrowsrClientConfig> {
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}
621/// Defines the secret requirements for a provider
622#[derive(Debug, Clone, Serialize, Deserialize)]
623pub struct ProviderSecretDefinition {
624    /// Provider identifier (e.g., "openai", "anthropic")
625    pub id: String,
626    /// Human-readable label
627    pub label: String,
628    /// List of required secret keys with metadata
629    pub keys: Vec<SecretKeyDefinition>,
630}
631
632/// Defines a single secret key requirement
633#[derive(Debug, Clone, Serialize, Deserialize)]
634pub struct SecretKeyDefinition {
635    /// The environment variable / secret store key (e.g., "OPENAI_API_KEY")
636    pub key: String,
637    /// Human-readable label
638    pub label: String,
639    /// Placeholder for UI input
640    pub placeholder: String,
641    /// Whether this secret is required (vs optional)
642    #[serde(default = "default_required")]
643    pub required: bool,
644}
645
646fn default_required() -> bool {
647    true
648}
649
650impl ModelProvider {
651    pub fn openai_base_url() -> String {
652        "https://api.openai.com/v1".to_string()
653    }
654
655    pub fn vllora_url() -> String {
656        "http://localhost:9090/v1".to_string()
657    }
658
659    /// Returns the provider ID for secret lookup
660    pub fn provider_id(&self) -> &'static str {
661        match self {
662            ModelProvider::OpenAI {} => "openai",
663            ModelProvider::OpenAICompatible { .. } => "openai_compat",
664            ModelProvider::Vllora { .. } => "vllora",
665        }
666    }
667
668    /// Returns the required secret keys for this provider
669    pub fn required_secret_keys(&self) -> Vec<&'static str> {
670        match self {
671            ModelProvider::OpenAI {} => vec!["OPENAI_API_KEY"],
672            ModelProvider::OpenAICompatible { api_key, .. } => {
673                // If api_key is already provided in config, no secret needed
674                if api_key.is_some() {
675                    vec![]
676                } else {
677                    vec!["OPENAI_API_KEY"]
678                }
679            }
680            ModelProvider::Vllora { .. } => vec![], // Local server, no API key needed
681        }
682    }
683
684    /// Returns all provider secret definitions (static registry)
685    pub fn all_provider_definitions() -> Vec<ProviderSecretDefinition> {
686        vec![
687            ProviderSecretDefinition {
688                id: "openai".to_string(),
689                label: "OpenAI".to_string(),
690                keys: vec![SecretKeyDefinition {
691                    key: "OPENAI_API_KEY".to_string(),
692                    label: "API key".to_string(),
693                    placeholder: "sk-...".to_string(),
694                    required: true,
695                }],
696            },
697            ProviderSecretDefinition {
698                id: "anthropic".to_string(),
699                label: "Anthropic".to_string(),
700                keys: vec![SecretKeyDefinition {
701                    key: "ANTHROPIC_API_KEY".to_string(),
702                    label: "API key".to_string(),
703                    placeholder: "sk-ant-...".to_string(),
704                    required: true,
705                }],
706            },
707            ProviderSecretDefinition {
708                id: "gemini".to_string(),
709                label: "Google Gemini".to_string(),
710                keys: vec![SecretKeyDefinition {
711                    key: "GEMINI_API_KEY".to_string(),
712                    label: "API key".to_string(),
713                    placeholder: "AIza...".to_string(),
714                    required: true,
715                }],
716            },
717            ProviderSecretDefinition {
718                id: "custom".to_string(),
719                label: "Custom".to_string(),
720                keys: vec![],
721            },
722        ]
723    }
724
725    /// Get the human-readable name for a provider
726    pub fn display_name(&self) -> &'static str {
727        match self {
728            ModelProvider::OpenAI {} => "OpenAI",
729            ModelProvider::OpenAICompatible { .. } => "OpenAI Compatible",
730            ModelProvider::Vllora { .. } => "vLLORA",
731        }
732    }
733}
734
735/// Model settings configuration
736#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
737#[serde(deny_unknown_fields)]
738pub struct ModelSettings {
739    #[serde(default = "default_model")]
740    pub model: String,
741    #[serde(default = "default_temperature")]
742    pub temperature: f32,
743    #[serde(default = "default_max_tokens")]
744    pub max_tokens: u32,
745    #[serde(default = "default_context_size")]
746    pub context_size: u32,
747    #[serde(default = "default_top_p")]
748    pub top_p: f32,
749    #[serde(default = "default_frequency_penalty")]
750    pub frequency_penalty: f32,
751    #[serde(default = "default_presence_penalty")]
752    pub presence_penalty: f32,
753    #[serde(default = "default_model_provider")]
754    pub provider: ModelProvider,
755    /// Additional parameters for the agent, if any.
756    #[serde(default)]
757    pub parameters: Option<serde_json::Value>,
758    /// The format of the response, if specified.
759    #[serde(default)]
760    pub response_format: Option<serde_json::Value>,
761}
762
763impl Default for ModelSettings {
764    fn default() -> Self {
765        Self {
766            model: "gpt-4.1-mini".to_string(),
767            temperature: 0.7,
768            max_tokens: 1000,
769            context_size: 20000,
770            top_p: 1.0,
771            frequency_penalty: 0.0,
772            presence_penalty: 0.0,
773            provider: default_model_provider(),
774            parameters: None,
775            response_format: None,
776        }
777    }
778}
779
780// Default functions
781pub fn default_agent_version() -> Option<String> {
782    Some("0.2.2".to_string())
783}
784
785fn default_model_provider() -> ModelProvider {
786    ModelProvider::OpenAI {}
787}
788
789fn default_model() -> String {
790    "gpt-4.1-mini".to_string()
791}
792
793fn default_temperature() -> f32 {
794    0.7
795}
796
797fn default_max_tokens() -> u32 {
798    1000
799}
800
801fn default_context_size() -> u32 {
802    20000 // Default limit for general use - agents can override with higher values as needed
803}
804
805fn default_top_p() -> f32 {
806    1.0
807}
808
809fn default_frequency_penalty() -> f32 {
810    0.0
811}
812
813fn default_presence_penalty() -> f32 {
814    0.0
815}
816
817fn default_history_size() -> Option<usize> {
818    Some(5)
819}
820
821impl StandardDefinition {
822    pub fn validate(&self) -> anyhow::Result<()> {
823        // Basic validation - can be expanded
824        if self.name.is_empty() {
825            return Err(anyhow::anyhow!("Agent name cannot be empty"));
826        }
827        Ok(())
828    }
829}
830
831impl From<StandardDefinition> for LlmDefinition {
832    fn from(definition: StandardDefinition) -> Self {
833        let mut model_settings = definition.model_settings.clone();
834        // Use agent-level context_size override if provided
835        if let Some(context_size) = definition.context_size {
836            model_settings.context_size = context_size;
837        }
838
839        Self {
840            name: definition.name,
841            model_settings,
842            tool_format: definition.tool_format,
843        }
844    }
845}
846
847impl ToolsConfig {
848    /// Create a simple configuration with just built-in tools
849    pub fn builtin_only(tools: Vec<&str>) -> Self {
850        Self {
851            builtin: tools.into_iter().map(|s| s.to_string()).collect(),
852            packages: std::collections::HashMap::new(),
853            mcp: vec![],
854            external: None,
855        }
856    }
857
858    /// Create a configuration that includes all tools from an MCP server
859    pub fn mcp_all(server: &str) -> Self {
860        Self {
861            builtin: vec![],
862            packages: std::collections::HashMap::new(),
863            mcp: vec![McpToolConfig {
864                server: server.to_string(),
865                include: vec!["*".to_string()],
866                exclude: vec![],
867            }],
868            external: None,
869        }
870    }
871
872    /// Create a configuration with specific MCP tool patterns
873    pub fn mcp_filtered(server: &str, include: Vec<&str>, exclude: Vec<&str>) -> Self {
874        Self {
875            builtin: vec![],
876            packages: std::collections::HashMap::new(),
877            mcp: vec![McpToolConfig {
878                server: server.to_string(),
879                include: include.into_iter().map(|s| s.to_string()).collect(),
880                exclude: exclude.into_iter().map(|s| s.to_string()).collect(),
881            }],
882            external: None,
883        }
884    }
885}
886
887pub async fn parse_agent_markdown_content(content: &str) -> Result<StandardDefinition, AgentError> {
888    // Split by --- to separate TOML frontmatter from markdown content
889    let parts: Vec<&str> = content.split("---").collect();
890
891    if parts.len() < 3 {
892        return Err(AgentError::Validation(
893            "Invalid agent markdown format. Expected TOML frontmatter between --- markers"
894                .to_string(),
895        ));
896    }
897
898    // Parse TOML frontmatter (parts[1] is between the first two --- markers)
899    let toml_content = parts[1].trim();
900    let mut agent_def: crate::StandardDefinition =
901        toml::from_str(toml_content).map_err(|e| AgentError::Validation(e.to_string()))?;
902
903    // Validate agent name format using centralized validation
904    if let Err(validation_error) = validate_plugin_name(&agent_def.name) {
905        return Err(AgentError::Validation(format!(
906            "Invalid agent name '{}': {}",
907            agent_def.name, validation_error
908        )));
909    }
910
911    // Validate that agent name is a valid JavaScript identifier
912    if !agent_def
913        .name
914        .chars()
915        .all(|c| c.is_alphanumeric() || c == '_')
916        || agent_def
917            .name
918            .chars()
919            .next()
920            .map_or(false, |c| c.is_numeric())
921    {
922        return Err(AgentError::Validation(format!(
923            "Invalid agent name '{}': Agent names must be valid JavaScript identifiers (alphanumeric + underscores, cannot start with number). \
924                Reason: Agent names become function names in TypeScript runtime.",
925            agent_def.name
926        )));
927    }
928
929    // Extract markdown instructions (everything after the second ---)
930    let instructions = parts[2..].join("---").trim().to_string();
931
932    // Set the instructions in the agent definition
933    agent_def.instructions = instructions;
934
935    Ok(agent_def)
936}
937
938/// Validate plugin name follows naming conventions
939/// Plugin names must be valid JavaScript identifiers (no hyphens)
940pub fn validate_plugin_name(name: &str) -> Result<(), String> {
941    if name.contains('-') {
942        return Err(format!(
943            "Plugin name '{}' cannot contain hyphens. Use underscores instead.",
944            name
945        ));
946    }
947
948    if name.is_empty() {
949        return Err("Plugin name cannot be empty".to_string());
950    }
951
952    // Check if first character is valid for JavaScript identifier
953    if let Some(first_char) = name.chars().next() {
954        if !first_char.is_ascii_alphabetic() && first_char != '_' {
955            return Err(format!(
956                "Plugin name '{}' must start with a letter or underscore",
957                name
958            ));
959        }
960    }
961
962    // Check if all characters are valid for JavaScript identifier
963    for ch in name.chars() {
964        if !ch.is_ascii_alphanumeric() && ch != '_' {
965            return Err(format!(
966                "Plugin name '{}' can only contain letters, numbers, and underscores",
967                name
968            ));
969        }
970    }
971
972    Ok(())
973}