Skip to main content

distri_types/
agent.rs

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