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, Serialize, Deserialize, Clone, JsonSchema, Default)]
256pub struct UserMessageOverrides {
257 pub parts: Vec<PartDefinition>,
259 #[serde(default)]
261 pub include_artifacts: bool,
262 #[serde(default = "default_include_step_count")]
264 pub include_step_count: Option<bool>,
265}
266
267fn default_include_step_count() -> Option<bool> {
268 Some(true)
269}
270
271#[derive(Debug, Serialize, Deserialize, Clone, JsonSchema)]
272#[serde(tag = "type", content = "source", rename_all = "snake_case")]
273pub enum PartDefinition {
274 Template(String), SessionKey(String), }
277
278#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, Default)]
279#[serde(deny_unknown_fields)]
280pub struct LlmDefinition {
281 pub name: String,
283 #[serde(default)]
285 pub model_settings: ModelSettings,
286 #[serde(default)]
288 pub tool_format: ToolCallFormat,
289}
290
291#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
293#[serde(tag = "mode", rename_all = "snake_case")]
294pub enum BrowserHooksConfig {
295 Disabled,
297 Webhook {
299 #[serde(default, skip_serializing_if = "Option::is_none")]
301 api_base_url: Option<String>,
302 },
303 Inline {
305 #[serde(default)]
307 timeout_ms: Option<u64>,
308 },
309}
310
311impl Default for BrowserHooksConfig {
312 fn default() -> Self {
313 BrowserHooksConfig::Disabled
314 }
315}
316
317#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, Default)]
319#[serde(deny_unknown_fields)]
320pub struct StandardDefinition {
321 pub name: String,
323 #[serde(default, skip_serializing_if = "Option::is_none")]
325 pub package_name: Option<String>,
326 #[serde(default)]
328 pub description: String,
329
330 #[serde(default = "default_agent_version")]
332 pub version: Option<String>,
333
334 #[serde(default)]
336 pub instructions: String,
337
338 #[serde(default)]
340 pub mcp_servers: Option<Vec<McpDefinition>>,
341 #[serde(default)]
343 pub model_settings: ModelSettings,
344 #[serde(default, skip_serializing_if = "Option::is_none")]
346 pub analysis_model_settings: Option<ModelSettings>,
347
348 #[serde(default = "default_history_size")]
350 pub history_size: Option<usize>,
351 #[serde(default, skip_serializing_if = "Option::is_none")]
353 pub strategy: Option<AgentStrategy>,
354 #[serde(default)]
356 pub icon_url: Option<String>,
357
358 #[serde(default, skip_serializing_if = "Option::is_none")]
359 pub max_iterations: Option<usize>,
360
361 #[serde(default, skip_serializing_if = "Vec::is_empty")]
362 pub skills: Vec<AgentSkill>,
363
364 #[serde(default)]
366 pub sub_agents: Vec<String>,
367
368 #[serde(default)]
370 pub tool_format: ToolCallFormat,
371
372 #[serde(default, skip_serializing_if = "Option::is_none")]
374 pub tools: Option<ToolsConfig>,
375
376 #[serde(default)]
378 pub file_system: FileSystemMode,
379
380 #[serde(default, skip_serializing_if = "std::collections::HashMap::is_empty")]
382 pub partials: std::collections::HashMap<String, String>,
383
384 #[serde(default, skip_serializing_if = "Option::is_none")]
386 pub write_large_tool_responses_to_fs: Option<bool>,
387
388 #[serde(default, skip_serializing_if = "Option::is_none")]
390 pub enable_reflection: Option<bool>,
391 #[serde(default, skip_serializing_if = "Option::is_none")]
393 pub enable_todos: Option<bool>,
394
395 #[serde(default, skip_serializing_if = "Option::is_none")]
397 pub browser_config: Option<BrowserAgentConfig>,
398 #[serde(default, skip_serializing_if = "Option::is_none")]
400 pub browser_hooks: Option<BrowserHooksConfig>,
401
402 #[serde(default, skip_serializing_if = "Option::is_none")]
404 pub context_size: Option<u32>,
405
406 #[serde(
408 skip_serializing_if = "Option::is_none",
409 default = "default_append_default_instructions"
410 )]
411 pub append_default_instructions: Option<bool>,
412 #[serde(
414 skip_serializing_if = "Option::is_none",
415 default = "default_include_scratchpad"
416 )]
417 pub include_scratchpad: Option<bool>,
418
419 #[serde(default, skip_serializing_if = "Vec::is_empty")]
421 pub hooks: Vec<String>,
422
423 #[serde(default, skip_serializing_if = "Option::is_none")]
425 pub user_message_overrides: Option<UserMessageOverrides>,
426}
427fn default_append_default_instructions() -> Option<bool> {
428 Some(true)
429}
430fn default_include_scratchpad() -> Option<bool> {
431 Some(true)
432}
433impl StandardDefinition {
434 pub fn should_write_large_tool_responses_to_fs(&self) -> bool {
436 self.write_large_tool_responses_to_fs.unwrap_or(false)
437 }
438
439 pub fn should_use_browser(&self) -> bool {
441 self.browser_config
442 .as_ref()
443 .map(|cfg| cfg.is_enabled())
444 .unwrap_or(false)
445 }
446
447 pub fn browser_settings(&self) -> Option<&BrowserAgentConfig> {
449 self.browser_config.as_ref()
450 }
451
452 pub fn browser_runtime_config(&self) -> Option<DistriBrowserConfig> {
454 self.browser_config.as_ref().map(|cfg| cfg.runtime_config())
455 }
456
457 pub fn should_persist_browser_session(&self) -> bool {
459 self.browser_config
460 .as_ref()
461 .map(|cfg| cfg.should_persist_session())
462 .unwrap_or(false)
463 }
464
465 pub fn is_reflection_enabled(&self) -> bool {
467 self.enable_reflection.unwrap_or(false)
468 }
469 pub fn is_todos_enabled(&self) -> bool {
471 self.enable_todos.unwrap_or(false)
472 }
473
474 pub fn get_effective_context_size(&self) -> u32 {
476 self.context_size
477 .unwrap_or(self.model_settings.context_size)
478 }
479
480 pub fn analysis_model_settings_config(&self) -> ModelSettings {
482 self.analysis_model_settings
483 .clone()
484 .unwrap_or_else(|| self.model_settings.clone())
485 }
486
487 pub fn include_scratchpad(&self) -> bool {
489 self.include_scratchpad.unwrap_or(true)
490 }
491
492 pub fn apply_overrides(&mut self, overrides: DefinitionOverrides) {
494 if let Some(model) = overrides.model {
496 self.model_settings.model = model;
497 }
498
499 if let Some(temperature) = overrides.temperature {
500 self.model_settings.temperature = temperature;
501 }
502
503 if let Some(max_tokens) = overrides.max_tokens {
504 self.model_settings.max_tokens = max_tokens;
505 }
506
507 if let Some(max_iterations) = overrides.max_iterations {
509 self.max_iterations = Some(max_iterations);
510 }
511
512 if let Some(instructions) = overrides.instructions {
514 self.instructions = instructions;
515 }
516
517 if let Some(use_browser) = overrides.use_browser {
518 let mut config = self.browser_config.clone().unwrap_or_default();
519 config.enabled = use_browser;
520 self.browser_config = Some(config);
521 }
522 }
523}
524
525#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, Default)]
527#[serde(deny_unknown_fields)]
528pub struct ToolsConfig {
529 #[serde(default, skip_serializing_if = "Vec::is_empty")]
531 pub builtin: Vec<String>,
532
533 #[serde(default, skip_serializing_if = "std::collections::HashMap::is_empty")]
535 pub packages: std::collections::HashMap<String, Vec<String>>,
536
537 #[serde(default, skip_serializing_if = "Vec::is_empty")]
539 pub mcp: Vec<McpToolConfig>,
540
541 #[serde(default, skip_serializing_if = "Option::is_none")]
543 pub external: Option<Vec<String>>,
544}
545
546#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, Default, PartialEq, Eq)]
548#[serde(rename_all = "snake_case")]
549pub enum FileSystemMode {
550 #[default]
552 Remote,
553 Local,
555}
556
557impl FileSystemMode {
558 pub fn include_server_tools(&self) -> bool {
559 !matches!(self, FileSystemMode::Local)
560 }
561}
562
563#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
565#[serde(deny_unknown_fields)]
566pub struct McpToolConfig {
567 pub server: String,
569
570 #[serde(default, skip_serializing_if = "Vec::is_empty")]
573 pub include: Vec<String>,
574
575 #[serde(default, skip_serializing_if = "Vec::is_empty")]
577 pub exclude: Vec<String>,
578}
579
580#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
581#[serde(deny_unknown_fields)]
582pub struct McpDefinition {
583 #[serde(default)]
585 pub filter: Option<Vec<String>>,
586 pub name: String,
588 #[serde(default)]
590 pub r#type: McpServerType,
591 #[serde(default)]
593 pub auth_config: Option<crate::a2a::SecurityScheme>,
594}
595
596#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema, PartialEq)]
597#[serde(rename_all = "lowercase")]
598pub enum McpServerType {
599 #[default]
600 Tool,
601 Agent,
602}
603
604#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
605#[serde(deny_unknown_fields, rename_all = "lowercase", tag = "name")]
606pub enum ModelProvider {
607 #[serde(rename = "openai")]
608 OpenAI {},
609 #[serde(rename = "openai_compat")]
610 OpenAICompatible {
611 base_url: String,
612 api_key: Option<String>,
613 project_id: Option<String>,
614 },
615 #[serde(rename = "vllora")]
616 Vllora {
617 #[serde(default = "ModelProvider::vllora_url")]
618 base_url: String,
619 },
620}
621impl ModelProvider {
622 pub fn openai_base_url() -> String {
623 "https://api.openai.com/v1".to_string()
624 }
625
626 pub fn vllora_url() -> String {
627 "http://localhost:9090/v1".to_string()
628 }
629}
630
631#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
633#[serde(deny_unknown_fields)]
634pub struct ModelSettings {
635 #[serde(default = "default_model")]
636 pub model: String,
637 #[serde(default = "default_temperature")]
638 pub temperature: f32,
639 #[serde(default = "default_max_tokens")]
640 pub max_tokens: u32,
641 #[serde(default = "default_context_size")]
642 pub context_size: u32,
643 #[serde(default = "default_top_p")]
644 pub top_p: f32,
645 #[serde(default = "default_frequency_penalty")]
646 pub frequency_penalty: f32,
647 #[serde(default = "default_presence_penalty")]
648 pub presence_penalty: f32,
649 #[serde(default = "default_model_provider")]
650 pub provider: ModelProvider,
651 #[serde(default)]
653 pub parameters: Option<serde_json::Value>,
654 #[serde(default)]
656 pub response_format: Option<serde_json::Value>,
657}
658
659impl Default for ModelSettings {
660 fn default() -> Self {
661 Self {
662 model: "gpt-4.1-mini".to_string(),
663 temperature: 0.7,
664 max_tokens: 1000,
665 context_size: 20000,
666 top_p: 1.0,
667 frequency_penalty: 0.0,
668 presence_penalty: 0.0,
669 provider: default_model_provider(),
670 parameters: None,
671 response_format: None,
672 }
673 }
674}
675
676pub fn default_agent_version() -> Option<String> {
678 Some("0.2.2".to_string())
679}
680
681fn default_model_provider() -> ModelProvider {
682 ModelProvider::OpenAI {}
683}
684
685fn default_model() -> String {
686 "gpt-4.1-mini".to_string()
687}
688
689fn default_temperature() -> f32 {
690 0.7
691}
692
693fn default_max_tokens() -> u32 {
694 1000
695}
696
697fn default_context_size() -> u32 {
698 20000 }
700
701fn default_top_p() -> f32 {
702 1.0
703}
704
705fn default_frequency_penalty() -> f32 {
706 0.0
707}
708
709fn default_presence_penalty() -> f32 {
710 0.0
711}
712
713fn default_history_size() -> Option<usize> {
714 Some(5)
715}
716
717impl StandardDefinition {
718 pub fn validate(&self) -> anyhow::Result<()> {
719 if self.name.is_empty() {
721 return Err(anyhow::anyhow!("Agent name cannot be empty"));
722 }
723 Ok(())
724 }
725}
726
727impl From<StandardDefinition> for LlmDefinition {
728 fn from(definition: StandardDefinition) -> Self {
729 let mut model_settings = definition.model_settings.clone();
730 if let Some(context_size) = definition.context_size {
732 model_settings.context_size = context_size;
733 }
734
735 Self {
736 name: definition.name,
737 model_settings,
738 tool_format: definition.tool_format,
739 }
740 }
741}
742
743impl ToolsConfig {
744 pub fn builtin_only(tools: Vec<&str>) -> Self {
746 Self {
747 builtin: tools.into_iter().map(|s| s.to_string()).collect(),
748 packages: std::collections::HashMap::new(),
749 mcp: vec![],
750 external: None,
751 }
752 }
753
754 pub fn mcp_all(server: &str) -> Self {
756 Self {
757 builtin: vec![],
758 packages: std::collections::HashMap::new(),
759 mcp: vec![McpToolConfig {
760 server: server.to_string(),
761 include: vec!["*".to_string()],
762 exclude: vec![],
763 }],
764 external: None,
765 }
766 }
767
768 pub fn mcp_filtered(server: &str, include: Vec<&str>, exclude: Vec<&str>) -> Self {
770 Self {
771 builtin: vec![],
772 packages: std::collections::HashMap::new(),
773 mcp: vec![McpToolConfig {
774 server: server.to_string(),
775 include: include.into_iter().map(|s| s.to_string()).collect(),
776 exclude: exclude.into_iter().map(|s| s.to_string()).collect(),
777 }],
778 external: None,
779 }
780 }
781}
782
783pub async fn parse_agent_markdown_content(content: &str) -> Result<StandardDefinition, AgentError> {
784 let parts: Vec<&str> = content.split("---").collect();
786
787 if parts.len() < 3 {
788 return Err(AgentError::Validation(
789 "Invalid agent markdown format. Expected TOML frontmatter between --- markers"
790 .to_string(),
791 ));
792 }
793
794 let toml_content = parts[1].trim();
796 let mut agent_def: crate::StandardDefinition =
797 toml::from_str(toml_content).map_err(|e| AgentError::Validation(e.to_string()))?;
798
799 if let Err(validation_error) = validate_plugin_name(&agent_def.name) {
801 return Err(AgentError::Validation(format!(
802 "Invalid agent name '{}': {}",
803 agent_def.name, validation_error
804 )));
805 }
806
807 if !agent_def
809 .name
810 .chars()
811 .all(|c| c.is_alphanumeric() || c == '_')
812 || agent_def
813 .name
814 .chars()
815 .next()
816 .map_or(false, |c| c.is_numeric())
817 {
818 return Err(AgentError::Validation(format!(
819 "Invalid agent name '{}': Agent names must be valid JavaScript identifiers (alphanumeric + underscores, cannot start with number). \
820 Reason: Agent names become function names in TypeScript runtime.",
821 agent_def.name
822 )));
823 }
824
825 let instructions = parts[2..].join("---").trim().to_string();
827
828 agent_def.instructions = instructions;
830
831 Ok(agent_def)
832}
833
834pub fn validate_plugin_name(name: &str) -> Result<(), String> {
837 if name.contains('-') {
838 return Err(format!(
839 "Plugin name '{}' cannot contain hyphens. Use underscores instead.",
840 name
841 ));
842 }
843
844 if name.is_empty() {
845 return Err("Plugin name cannot be empty".to_string());
846 }
847
848 if let Some(first_char) = name.chars().next() {
850 if !first_char.is_ascii_alphabetic() && first_char != '_' {
851 return Err(format!(
852 "Plugin name '{}' must start with a letter or underscore",
853 name
854 ));
855 }
856 }
857
858 for ch in name.chars() {
860 if !ch.is_ascii_alphanumeric() && ch != '_' {
861 return Err(format!(
862 "Plugin name '{}' can only contain letters, numbers, and underscores",
863 name
864 ));
865 }
866 }
867
868 Ok(())
869}