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