1use crate::AgentError;
2use crate::a2a::AgentSkill;
3use crate::browser::{BrowserAgentConfig, DistriBrowserConfig};
4use crate::configuration::DefinitionOverrides;
5use schemars::JsonSchema;
6use serde::{Deserialize, Serialize};
7use std::default::Default;
8
9pub const DEFAULT_EXTERNAL_TOOL_TIMEOUT_SECS: u64 = 120;
11
12#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
14#[serde(deny_unknown_fields, rename_all = "snake_case")]
15pub struct AgentStrategy {
16 #[serde(default, skip_serializing_if = "Option::is_none")]
18 pub reasoning_depth: Option<ReasoningDepth>,
19
20 #[serde(default, skip_serializing_if = "Option::is_none")]
22 pub execution_mode: Option<ExecutionMode>,
23 #[serde(default, skip_serializing_if = "Option::is_none")]
25 pub replanning: Option<ReplanningConfig>,
26
27 #[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 pub fn get_reasoning_depth(&self) -> ReasoningDepth {
47 self.reasoning_depth.clone().unwrap_or_default()
48 }
49
50 pub fn get_execution_mode(&self) -> ExecutionMode {
52 self.execution_mode.clone().unwrap_or_default()
53 }
54
55 pub fn get_replanning(&self) -> ReplanningConfig {
57 self.replanning.clone().unwrap_or_default()
58 }
59
60 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#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, Default)]
90pub struct ReflectionConfig {
91 #[serde(default)]
93 pub enabled: bool,
94 #[serde(default)]
96 pub trigger: ReflectionTrigger,
97 #[serde(default)]
99 pub depth: ReflectionDepth,
100}
101
102#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, Default)]
104#[serde(rename_all = "snake_case")]
105pub enum ReflectionTrigger {
106 #[default]
108 EndOfExecution,
109 AfterEachStep,
111 AfterFailures,
113 AfterNSteps(usize),
115}
116
117#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, Default)]
119#[serde(rename_all = "snake_case")]
120pub enum ReflectionDepth {
121 #[default]
123 Light,
124 Standard,
126 Deep,
128}
129
130#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
132pub struct PlanConfig {
133 #[serde(default)]
135 pub model_settings: ModelSettings,
136 #[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#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, Default, PartialEq)]
156#[serde(rename_all = "snake_case")]
157pub enum ReasoningDepth {
158 Shallow,
160 #[default]
162 Standard,
163 Deep,
165}
166
167#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, Default, PartialEq)]
169#[serde(rename_all = "snake_case", tag = "type")]
170pub enum ExecutionMode {
171 #[default]
173 Tools,
174 Code { language: CodeLanguage },
176}
177
178#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, Default)]
180#[serde(rename_all = "snake_case")]
181pub struct ReplanningConfig {
182 #[serde(default, skip_serializing_if = "Option::is_none")]
184 pub trigger: Option<ReplanningTrigger>,
185 #[serde(default, skip_serializing_if = "Option::is_none")]
187 pub enabled: Option<bool>,
188}
189
190#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, Default)]
192#[serde(rename_all = "snake_case")]
193pub enum ReplanningTrigger {
194 #[default]
196 Never,
197 AfterReflection,
199 AfterNIterations(usize),
201 AfterFailures,
203}
204
205impl ReplanningConfig {
206 pub fn get_trigger(&self) -> ReplanningTrigger {
208 self.trigger.clone().unwrap_or_default()
209 }
210
211 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#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, Default, PartialEq)]
237#[serde(rename_all = "snake_case")]
238pub enum ToolCallFormat {
239 #[default]
242 Xml,
243 JsonL,
246
247 Code,
250 #[serde(rename = "provider")]
251 Provider,
252 None,
253}
254
255#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, Default)]
256#[serde(deny_unknown_fields)]
257pub struct LlmDefinition {
258 pub name: String,
260 #[serde(default)]
262 pub model_settings: ModelSettings,
263 #[serde(default)]
265 pub tool_format: ToolCallFormat,
266}
267
268#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
270#[serde(tag = "mode", rename_all = "snake_case")]
271pub enum BrowserHooksConfig {
272 Disabled,
274 Webhook {
276 #[serde(default, skip_serializing_if = "Option::is_none")]
278 api_base_url: Option<String>,
279 },
280 Inline {
282 #[serde(default)]
284 timeout_ms: Option<u64>,
285 },
286}
287
288impl Default for BrowserHooksConfig {
289 fn default() -> Self {
290 BrowserHooksConfig::Disabled
291 }
292}
293
294#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, Default)]
296#[serde(deny_unknown_fields)]
297pub struct StandardDefinition {
298 pub name: String,
300 #[serde(default, skip_serializing_if = "Option::is_none")]
302 pub package_name: Option<String>,
303 #[serde(default)]
305 pub description: String,
306
307 #[serde(default = "default_agent_version")]
309 pub version: Option<String>,
310
311 #[serde(default)]
313 pub instructions: String,
314
315 #[serde(default)]
317 pub mcp_servers: Option<Vec<McpDefinition>>,
318 #[serde(default)]
320 pub model_settings: ModelSettings,
321 #[serde(default, skip_serializing_if = "Option::is_none")]
323 pub analysis_model_settings: Option<ModelSettings>,
324
325 #[serde(default = "default_history_size")]
327 pub history_size: Option<usize>,
328 #[serde(default, skip_serializing_if = "Option::is_none")]
330 pub strategy: Option<AgentStrategy>,
331 #[serde(default)]
333 pub icon_url: Option<String>,
334
335 #[serde(default, skip_serializing_if = "Option::is_none")]
336 pub max_iterations: Option<usize>,
337
338 #[serde(default, skip_serializing_if = "Vec::is_empty")]
339 pub skills: Vec<AgentSkill>,
340
341 #[serde(default)]
343 pub sub_agents: Vec<String>,
344
345 #[serde(default)]
347 pub tool_format: ToolCallFormat,
348
349 #[serde(default, skip_serializing_if = "Option::is_none")]
351 pub tools: Option<ToolsConfig>,
352
353 #[serde(default)]
355 pub file_system: FileSystemMode,
356
357 #[serde(default, skip_serializing_if = "std::collections::HashMap::is_empty")]
359 pub partials: std::collections::HashMap<String, String>,
360
361 #[serde(default, skip_serializing_if = "Option::is_none")]
363 pub write_large_tool_responses_to_fs: Option<bool>,
364
365 #[serde(default, skip_serializing_if = "Option::is_none")]
367 pub enable_reflection: Option<bool>,
368 #[serde(default, skip_serializing_if = "Option::is_none")]
370 pub enable_todos: Option<bool>,
371
372 #[serde(default, skip_serializing_if = "Option::is_none")]
374 pub browser_config: Option<BrowserAgentConfig>,
375 #[serde(default, skip_serializing_if = "Option::is_none")]
377 pub browser_hooks: Option<BrowserHooksConfig>,
378
379 #[serde(default, skip_serializing_if = "Option::is_none")]
381 pub context_size: Option<u32>,
382
383 #[serde(
385 skip_serializing_if = "Option::is_none",
386 default = "default_append_default_instructions"
387 )]
388 pub append_default_instructions: Option<bool>,
389 #[serde(
391 skip_serializing_if = "Option::is_none",
392 default = "default_include_scratchpad"
393 )]
394 pub include_scratchpad: Option<bool>,
395
396 #[serde(default, skip_serializing_if = "Vec::is_empty")]
398 pub hooks: Vec<String>,
399}
400fn default_append_default_instructions() -> Option<bool> {
401 Some(true)
402}
403fn default_include_scratchpad() -> Option<bool> {
404 Some(true)
405}
406impl StandardDefinition {
407 pub fn should_write_large_tool_responses_to_fs(&self) -> bool {
409 self.write_large_tool_responses_to_fs.unwrap_or(false)
410 }
411
412 pub fn should_use_browser(&self) -> bool {
414 self.browser_config
415 .as_ref()
416 .map(|cfg| cfg.is_enabled())
417 .unwrap_or(false)
418 }
419
420 pub fn browser_settings(&self) -> Option<&BrowserAgentConfig> {
422 self.browser_config.as_ref()
423 }
424
425 pub fn browser_runtime_config(&self) -> Option<DistriBrowserConfig> {
427 self.browser_config.as_ref().map(|cfg| cfg.runtime_config())
428 }
429
430 pub fn should_persist_browser_session(&self) -> bool {
432 self.browser_config
433 .as_ref()
434 .map(|cfg| cfg.should_persist_session())
435 .unwrap_or(false)
436 }
437
438 pub fn is_reflection_enabled(&self) -> bool {
440 self.enable_reflection.unwrap_or(false)
441 }
442 pub fn is_todos_enabled(&self) -> bool {
444 self.enable_todos.unwrap_or(false)
445 }
446
447 pub fn get_effective_context_size(&self) -> u32 {
449 self.context_size
450 .unwrap_or(self.model_settings.context_size)
451 }
452
453 pub fn analysis_model_settings_config(&self) -> ModelSettings {
455 self.analysis_model_settings
456 .clone()
457 .unwrap_or_else(|| self.model_settings.clone())
458 }
459
460 pub fn include_scratchpad(&self) -> bool {
462 self.include_scratchpad.unwrap_or(true)
463 }
464
465 pub fn apply_overrides(&mut self, overrides: DefinitionOverrides) {
467 if let Some(model) = overrides.model {
469 self.model_settings.model = model;
470 }
471
472 if let Some(temperature) = overrides.temperature {
473 self.model_settings.temperature = temperature;
474 }
475
476 if let Some(max_tokens) = overrides.max_tokens {
477 self.model_settings.max_tokens = max_tokens;
478 }
479
480 if let Some(max_iterations) = overrides.max_iterations {
482 self.max_iterations = Some(max_iterations);
483 }
484
485 if let Some(instructions) = overrides.instructions {
487 self.instructions = instructions;
488 }
489
490 if let Some(use_browser) = overrides.use_browser {
491 let mut config = self.browser_config.clone().unwrap_or_default();
492 config.enabled = use_browser;
493 self.browser_config = Some(config);
494 }
495 }
496}
497
498#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, Default)]
500#[serde(deny_unknown_fields)]
501pub struct ToolsConfig {
502 #[serde(default, skip_serializing_if = "Vec::is_empty")]
504 pub builtin: Vec<String>,
505
506 #[serde(default, skip_serializing_if = "std::collections::HashMap::is_empty")]
508 pub packages: std::collections::HashMap<String, Vec<String>>,
509
510 #[serde(default, skip_serializing_if = "Vec::is_empty")]
512 pub mcp: Vec<McpToolConfig>,
513
514 #[serde(default, skip_serializing_if = "Option::is_none")]
516 pub external: Option<Vec<String>>,
517}
518
519#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, Default, PartialEq, Eq)]
521#[serde(rename_all = "snake_case")]
522pub enum FileSystemMode {
523 #[default]
525 Remote,
526 Local,
528}
529
530impl FileSystemMode {
531 pub fn include_server_tools(&self) -> bool {
532 !matches!(self, FileSystemMode::Local)
533 }
534}
535
536#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
538#[serde(deny_unknown_fields)]
539pub struct McpToolConfig {
540 pub server: String,
542
543 #[serde(default, skip_serializing_if = "Vec::is_empty")]
546 pub include: Vec<String>,
547
548 #[serde(default, skip_serializing_if = "Vec::is_empty")]
550 pub exclude: Vec<String>,
551}
552
553#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
554#[serde(deny_unknown_fields)]
555pub struct McpDefinition {
556 #[serde(default)]
558 pub filter: Option<Vec<String>>,
559 pub name: String,
561 #[serde(default)]
563 pub r#type: McpServerType,
564 #[serde(default)]
566 pub auth_config: Option<crate::a2a::SecurityScheme>,
567}
568
569#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema, PartialEq)]
570#[serde(rename_all = "lowercase")]
571pub enum McpServerType {
572 #[default]
573 Tool,
574 Agent,
575}
576
577#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
578#[serde(deny_unknown_fields, rename_all = "lowercase", tag = "name")]
579pub enum ModelProvider {
580 #[serde(rename = "openai")]
581 OpenAI {},
582 #[serde(rename = "openai_compat")]
583 OpenAICompatible {
584 base_url: String,
585 api_key: Option<String>,
586 project_id: Option<String>,
587 },
588 #[serde(rename = "vllora")]
589 Vllora {
590 #[serde(default = "ModelProvider::vllora_url")]
591 base_url: String,
592 },
593}
594impl ModelProvider {
595 pub fn openai_base_url() -> String {
596 "https://api.openai.com/v1".to_string()
597 }
598
599 pub fn vllora_url() -> String {
600 "http://localhost:9090/v1".to_string()
601 }
602}
603
604#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
606#[serde(deny_unknown_fields)]
607pub struct ModelSettings {
608 #[serde(default = "default_model")]
609 pub model: String,
610 #[serde(default = "default_temperature")]
611 pub temperature: f32,
612 #[serde(default = "default_max_tokens")]
613 pub max_tokens: u32,
614 #[serde(default = "default_context_size")]
615 pub context_size: u32,
616 #[serde(default = "default_top_p")]
617 pub top_p: f32,
618 #[serde(default = "default_frequency_penalty")]
619 pub frequency_penalty: f32,
620 #[serde(default = "default_presence_penalty")]
621 pub presence_penalty: f32,
622 #[serde(default = "default_model_provider")]
623 pub provider: ModelProvider,
624 #[serde(default)]
626 pub parameters: Option<serde_json::Value>,
627 #[serde(default)]
629 pub response_format: Option<serde_json::Value>,
630}
631
632impl Default for ModelSettings {
633 fn default() -> Self {
634 Self {
635 model: "gpt-4.1-mini".to_string(),
636 temperature: 0.7,
637 max_tokens: 1000,
638 context_size: 20000,
639 top_p: 1.0,
640 frequency_penalty: 0.0,
641 presence_penalty: 0.0,
642 provider: default_model_provider(),
643 parameters: None,
644 response_format: None,
645 }
646 }
647}
648
649pub fn default_agent_version() -> Option<String> {
651 Some("0.2.2".to_string())
652}
653
654fn default_model_provider() -> ModelProvider {
655 ModelProvider::OpenAI {}
656}
657
658fn default_model() -> String {
659 "gpt-4.1-mini".to_string()
660}
661
662fn default_temperature() -> f32 {
663 0.7
664}
665
666fn default_max_tokens() -> u32 {
667 1000
668}
669
670fn default_context_size() -> u32 {
671 20000 }
673
674fn default_top_p() -> f32 {
675 1.0
676}
677
678fn default_frequency_penalty() -> f32 {
679 0.0
680}
681
682fn default_presence_penalty() -> f32 {
683 0.0
684}
685
686fn default_history_size() -> Option<usize> {
687 Some(5)
688}
689
690impl StandardDefinition {
691 pub fn validate(&self) -> anyhow::Result<()> {
692 if self.name.is_empty() {
694 return Err(anyhow::anyhow!("Agent name cannot be empty"));
695 }
696 Ok(())
697 }
698}
699
700impl From<StandardDefinition> for LlmDefinition {
701 fn from(definition: StandardDefinition) -> Self {
702 let mut model_settings = definition.model_settings.clone();
703 if let Some(context_size) = definition.context_size {
705 model_settings.context_size = context_size;
706 }
707
708 Self {
709 name: definition.name,
710 model_settings,
711 tool_format: definition.tool_format,
712 }
713 }
714}
715
716impl ToolsConfig {
717 pub fn builtin_only(tools: Vec<&str>) -> Self {
719 Self {
720 builtin: tools.into_iter().map(|s| s.to_string()).collect(),
721 packages: std::collections::HashMap::new(),
722 mcp: vec![],
723 external: None,
724 }
725 }
726
727 pub fn mcp_all(server: &str) -> Self {
729 Self {
730 builtin: vec![],
731 packages: std::collections::HashMap::new(),
732 mcp: vec![McpToolConfig {
733 server: server.to_string(),
734 include: vec!["*".to_string()],
735 exclude: vec![],
736 }],
737 external: None,
738 }
739 }
740
741 pub fn mcp_filtered(server: &str, include: Vec<&str>, exclude: Vec<&str>) -> Self {
743 Self {
744 builtin: vec![],
745 packages: std::collections::HashMap::new(),
746 mcp: vec![McpToolConfig {
747 server: server.to_string(),
748 include: include.into_iter().map(|s| s.to_string()).collect(),
749 exclude: exclude.into_iter().map(|s| s.to_string()).collect(),
750 }],
751 external: None,
752 }
753 }
754}
755
756pub async fn parse_agent_markdown_content(content: &str) -> Result<StandardDefinition, AgentError> {
757 let parts: Vec<&str> = content.split("---").collect();
759
760 if parts.len() < 3 {
761 return Err(AgentError::Validation(
762 "Invalid agent markdown format. Expected TOML frontmatter between --- markers"
763 .to_string(),
764 ));
765 }
766
767 let toml_content = parts[1].trim();
769 let mut agent_def: crate::StandardDefinition =
770 toml::from_str(toml_content).map_err(|e| AgentError::Validation(e.to_string()))?;
771
772 if let Err(validation_error) = validate_plugin_name(&agent_def.name) {
774 return Err(AgentError::Validation(format!(
775 "Invalid agent name '{}': {}",
776 agent_def.name, validation_error
777 )));
778 }
779
780 if !agent_def
782 .name
783 .chars()
784 .all(|c| c.is_alphanumeric() || c == '_')
785 || agent_def
786 .name
787 .chars()
788 .next()
789 .map_or(false, |c| c.is_numeric())
790 {
791 return Err(AgentError::Validation(format!(
792 "Invalid agent name '{}': Agent names must be valid JavaScript identifiers (alphanumeric + underscores, cannot start with number). \
793 Reason: Agent names become function names in TypeScript runtime.",
794 agent_def.name
795 )));
796 }
797
798 let instructions = parts[2..].join("---").trim().to_string();
800
801 agent_def.instructions = instructions;
803
804 Ok(agent_def)
805}
806
807pub fn validate_plugin_name(name: &str) -> Result<(), String> {
810 if name.contains('-') {
811 return Err(format!(
812 "Plugin name '{}' cannot contain hyphens. Use underscores instead.",
813 name
814 ));
815 }
816
817 if name.is_empty() {
818 return Err("Plugin name cannot be empty".to_string());
819 }
820
821 if let Some(first_char) = name.chars().next() {
823 if !first_char.is_ascii_alphabetic() && first_char != '_' {
824 return Err(format!(
825 "Plugin name '{}' must start with a letter or underscore",
826 name
827 ));
828 }
829 }
830
831 for ch in name.chars() {
833 if !ch.is_ascii_alphanumeric() && ch != '_' {
834 return Err(format!(
835 "Plugin name '{}' can only contain letters, numbers, and underscores",
836 name
837 ));
838 }
839 }
840
841 Ok(())
842}