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
9pub const DEFAULT_EXTERNAL_TOOL_TIMEOUT_SECS: u64 = 120;
11
12#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, Default)]
14pub struct AvailableSkill {
15 pub id: String,
17 pub name: String,
19 #[serde(default, skip_serializing_if = "Option::is_none")]
21 pub description: Option<String>,
22}
23
24#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
26#[serde(deny_unknown_fields, rename_all = "snake_case")]
27#[derive(Default)]
28pub struct AgentStrategy {
29 #[serde(default, skip_serializing_if = "Option::is_none")]
31 pub reasoning_depth: Option<ReasoningDepth>,
32
33 #[serde(default, skip_serializing_if = "Option::is_none")]
35 pub execution_mode: Option<ExecutionMode>,
36 #[serde(default, skip_serializing_if = "Option::is_none")]
38 pub replanning: Option<ReplanningConfig>,
39
40 #[serde(default, skip_serializing_if = "Option::is_none")]
43 pub external_tool_timeout_secs: Option<u64>,
44}
45
46impl AgentStrategy {
47 pub fn get_reasoning_depth(&self) -> ReasoningDepth {
49 self.reasoning_depth.clone().unwrap_or_default()
50 }
51
52 pub fn get_execution_mode(&self) -> ExecutionMode {
54 self.execution_mode.clone().unwrap_or_default()
55 }
56
57 pub fn get_replanning(&self) -> ReplanningConfig {
59 self.replanning.clone().unwrap_or_default()
60 }
61
62 pub fn get_external_tool_timeout_secs(&self) -> u64 {
64 self.external_tool_timeout_secs
65 .unwrap_or(DEFAULT_EXTERNAL_TOOL_TIMEOUT_SECS)
66 }
67}
68
69#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, Default, PartialEq)]
70#[serde(rename_all = "snake_case")]
71pub enum CodeLanguage {
72 #[default]
73 Typescript,
74}
75
76impl std::fmt::Display for CodeLanguage {
77 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
78 write!(f, "{}", self.to_string())
79 }
80}
81
82impl CodeLanguage {
83 pub fn to_string(&self) -> String {
84 match self {
85 CodeLanguage::Typescript => "typescript".to_string(),
86 }
87 }
88}
89
90#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, Default)]
92pub struct ReflectionConfig {
93 #[serde(default)]
95 pub enabled: bool,
96 #[serde(default, skip_serializing_if = "Option::is_none")]
100 pub reflection_agent: Option<String>,
101 #[serde(default)]
103 pub trigger: ReflectionTrigger,
104 #[serde(default)]
106 pub depth: ReflectionDepth,
107}
108
109#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, Default)]
111#[serde(rename_all = "snake_case")]
112pub enum ReflectionTrigger {
113 #[default]
115 EndOfExecution,
116 AfterEachStep,
118 AfterFailures,
120 AfterNSteps(usize),
122}
123
124#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, Default)]
126#[serde(rename_all = "snake_case")]
127pub enum ReflectionDepth {
128 #[default]
130 Light,
131 Standard,
133 Deep,
135}
136
137#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
139pub struct PlanConfig {
140 #[serde(default, skip_serializing_if = "Option::is_none")]
142 pub model_settings: Option<ModelSettings>,
143 #[serde(default = "default_plan_max_iterations")]
145 pub max_iterations: usize,
146}
147
148impl Default for PlanConfig {
149 fn default() -> Self {
150 Self {
151 model_settings: None,
152 max_iterations: default_plan_max_iterations(),
153 }
154 }
155}
156
157fn default_plan_max_iterations() -> usize {
158 10
159}
160
161#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, Default, PartialEq)]
163#[serde(rename_all = "snake_case")]
164pub enum ReasoningDepth {
165 Shallow,
167 #[default]
169 Standard,
170 Deep,
172}
173
174#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, Default, PartialEq)]
176#[serde(rename_all = "snake_case", tag = "type")]
177pub enum ExecutionMode {
178 #[default]
180 Tools,
181 Code { language: CodeLanguage },
183}
184
185#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, Default)]
187#[serde(rename_all = "snake_case")]
188pub struct ReplanningConfig {
189 #[serde(default, skip_serializing_if = "Option::is_none")]
191 pub trigger: Option<ReplanningTrigger>,
192 #[serde(default, skip_serializing_if = "Option::is_none")]
194 pub enabled: Option<bool>,
195}
196
197#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, Default)]
199#[serde(rename_all = "snake_case")]
200pub enum ReplanningTrigger {
201 #[default]
203 Never,
204 AfterReflection,
206 AfterNIterations(usize),
208 AfterFailures,
210}
211
212impl ReplanningConfig {
213 pub fn get_trigger(&self) -> ReplanningTrigger {
215 self.trigger.clone().unwrap_or_default()
216 }
217
218 pub fn is_enabled(&self) -> bool {
220 self.enabled.unwrap_or(false)
221 }
222}
223
224#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, Default)]
225#[serde(rename_all = "snake_case")]
226pub enum ExecutionKind {
227 #[default]
228 Retriable,
229 Interleaved,
230}
231
232#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, Default)]
233#[serde(rename_all = "snake_case")]
234pub enum MemoryKind {
235 #[default]
236 None,
237 ShortTerm,
238 LongTerm,
239}
240
241#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, Default, PartialEq)]
243#[serde(rename_all = "snake_case")]
244pub enum ToolDeliveryMode {
245 #[default]
247 AllTools,
248 ToolSearch,
250}
251
252#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, Default, PartialEq)]
254#[serde(rename_all = "snake_case")]
255pub enum ToolCallFormat {
256 #[default]
259 Xml,
260 JsonL,
263
264 Code,
267 #[serde(rename = "provider")]
268 Provider,
269 None,
270}
271
272#[derive(Debug, Serialize, Deserialize, Clone, JsonSchema, Default)]
273pub struct UserMessageOverrides {
274 pub parts: Vec<PartDefinition>,
276 #[serde(default)]
278 pub include_artifacts: bool,
279 #[serde(default = "default_include_step_count")]
281 pub include_step_count: Option<bool>,
282}
283
284fn default_include_step_count() -> Option<bool> {
285 Some(true)
286}
287
288#[derive(Debug, Serialize, Deserialize, Clone, JsonSchema)]
289#[serde(tag = "type", content = "source", rename_all = "snake_case")]
290pub enum PartDefinition {
291 Template(String), SessionKey(String), }
294
295#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
296#[serde(deny_unknown_fields)]
297pub struct LlmDefinition {
298 pub name: String,
300 #[serde(default, skip_serializing_if = "Option::is_none")]
302 pub model_settings: Option<ModelSettings>,
303 #[serde(default)]
305 pub tool_format: ToolCallFormat,
306 #[serde(default)]
308 pub tool_delivery_mode: ToolDeliveryMode,
309}
310
311impl LlmDefinition {
312 pub fn ms(&self) -> Result<&ModelSettings, String> {
315 self.model_settings.as_ref().ok_or_else(|| {
316 "No model configured. Please set a default model in Agent Settings → Default Model."
317 .to_string()
318 })
319 }
320
321 pub fn ms_mut(&mut self) -> Result<&mut ModelSettings, String> {
324 self.model_settings.as_mut().ok_or_else(|| {
325 "No model configured. Please set a default model in Agent Settings → Default Model."
326 .to_string()
327 })
328 }
329}
330
331#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, Default)]
333pub struct StandardDefinition {
334 pub name: String,
336 #[serde(default, skip_serializing_if = "Option::is_none")]
338 pub package_name: Option<String>,
339 #[serde(default)]
341 pub description: String,
342
343 #[serde(default = "default_agent_version")]
345 pub version: Option<String>,
346
347 #[serde(default)]
349 pub instructions: String,
350
351 #[serde(default)]
353 pub mcp_servers: Option<Vec<McpDefinition>>,
354 #[serde(default, skip_serializing_if = "Option::is_none")]
357 pub model_settings: Option<ModelSettings>,
358 #[serde(default, skip_serializing_if = "Option::is_none")]
360 pub analysis_model_settings: Option<ModelSettings>,
361
362 #[serde(default = "default_history_size")]
364 pub history_size: Option<usize>,
365 #[serde(default, skip_serializing_if = "Option::is_none")]
367 pub strategy: Option<AgentStrategy>,
368 #[serde(default)]
370 pub icon_url: Option<String>,
371
372 #[serde(default, skip_serializing_if = "Option::is_none")]
373 pub max_iterations: Option<usize>,
374
375 #[serde(default, skip_serializing_if = "Vec::is_empty")]
377 pub skills_description: Vec<AgentSkill>,
378
379 #[serde(default, skip_serializing_if = "Vec::is_empty")]
381 pub available_skills: Vec<AvailableSkill>,
382
383 #[serde(default)]
385 pub sub_agents: Vec<String>,
386
387 #[serde(default)]
389 pub tool_format: ToolCallFormat,
390
391 #[serde(default)]
393 pub tool_delivery_mode: ToolDeliveryMode,
394
395 #[serde(default, skip_serializing_if = "Option::is_none")]
397 pub tools: Option<ToolsConfig>,
398
399 #[serde(default)]
401 pub file_system: FileSystemMode,
402
403 #[serde(default, skip_serializing_if = "std::collections::HashMap::is_empty")]
405 pub partials: std::collections::HashMap<String, String>,
406
407 #[serde(default, skip_serializing_if = "Option::is_none")]
409 pub write_large_tool_responses_to_fs: Option<bool>,
410
411 #[serde(default, skip_serializing_if = "Option::is_none")]
413 pub reflection: Option<ReflectionConfig>,
414 #[serde(default, skip_serializing_if = "Option::is_none")]
416 pub enable_todos: Option<bool>,
417
418 #[serde(default, skip_serializing_if = "Option::is_none")]
420 pub browser_config: Option<BrowserAgentConfig>,
421
422 #[serde(default, skip_serializing_if = "Option::is_none")]
424 pub include_shell: Option<bool>,
425
426 #[serde(default, skip_serializing_if = "Option::is_none")]
428 pub context_size: Option<u32>,
429
430 #[serde(
432 skip_serializing_if = "Option::is_none",
433 default = "default_append_default_instructions"
434 )]
435 pub append_default_instructions: Option<bool>,
436 #[serde(
438 skip_serializing_if = "Option::is_none",
439 default = "default_include_scratchpad"
440 )]
441 pub include_scratchpad: Option<bool>,
442
443 #[serde(default, skip_serializing_if = "Vec::is_empty")]
445 pub hooks: Vec<String>,
446
447 #[serde(default, skip_serializing_if = "Option::is_none")]
449 pub user_message_overrides: Option<UserMessageOverrides>,
450
451 #[serde(
453 default = "default_compaction_enabled",
454 skip_serializing_if = "is_true"
455 )]
456 pub compaction_enabled: bool,
457}
458fn default_append_default_instructions() -> Option<bool> {
459 Some(true)
460}
461fn default_include_scratchpad() -> Option<bool> {
462 Some(true)
463}
464fn default_compaction_enabled() -> bool {
465 true
466}
467fn is_true(v: &bool) -> bool {
468 *v
469}
470impl StandardDefinition {
471 pub fn should_write_large_tool_responses_to_fs(&self) -> bool {
473 self.write_large_tool_responses_to_fs.unwrap_or(false)
474 }
475
476 pub fn should_use_browser(&self) -> bool {
478 self.browser_config
479 .as_ref()
480 .map(|cfg| cfg.is_enabled())
481 .unwrap_or(false)
482 }
483
484 pub fn browser_settings(&self) -> Option<&BrowserAgentConfig> {
486 self.browser_config.as_ref()
487 }
488
489 pub fn browser_runtime_config(&self) -> Option<BrowsrClientConfig> {
491 self.browser_config.as_ref().map(|cfg| cfg.runtime_config())
492 }
493
494 pub fn should_persist_browser_session(&self) -> bool {
496 self.browser_config
497 .as_ref()
498 .map(|cfg| cfg.should_persist_session())
499 .unwrap_or(false)
500 }
501
502 pub fn is_reflection_enabled(&self) -> bool {
504 self.reflection.as_ref().map(|r| r.enabled).unwrap_or(false)
505 }
506
507 pub fn reflection_config(&self) -> Option<&ReflectionConfig> {
509 self.reflection.as_ref().filter(|r| r.enabled)
510 }
511 pub fn is_todos_enabled(&self) -> bool {
513 self.enable_todos.unwrap_or(false)
514 }
515
516 pub fn should_include_shell(&self) -> bool {
518 self.include_shell.unwrap_or(false)
519 }
520
521 pub fn model_settings(&self) -> Option<&ModelSettings> {
523 self.model_settings.as_ref()
524 }
525
526 pub fn model_settings_mut(&mut self) -> Option<&mut ModelSettings> {
528 self.model_settings.as_mut()
529 }
530
531 pub fn get_effective_context_size(&self) -> u32 {
533 self.context_size
534 .or_else(|| self.model_settings().map(|ms| ms.inner.context_size))
535 .unwrap_or_else(default_context_size)
536 }
537
538 pub fn analysis_model_settings_config(&self) -> Option<&ModelSettings> {
540 self.analysis_model_settings
541 .as_ref()
542 .or_else(|| self.model_settings())
543 }
544
545 pub fn include_scratchpad(&self) -> bool {
547 self.include_scratchpad.unwrap_or(true)
548 }
549
550 pub fn apply_overrides(&mut self, overrides: DefinitionOverrides) {
552 if let Some(ref mut ms) = self.model_settings {
554 if let Some(model) = overrides.model {
555 ms.model = model
557 .split_once('/')
558 .map(|(_, m)| m.to_string())
559 .unwrap_or(model);
560 }
561 if let Some(temperature) = overrides.temperature {
562 ms.inner.temperature = Some(temperature);
563 }
564 if let Some(max_tokens) = overrides.max_tokens {
565 ms.inner.max_tokens = Some(max_tokens);
566 }
567 }
568
569 if let Some(max_iterations) = overrides.max_iterations {
571 self.max_iterations = Some(max_iterations);
572 }
573
574 if let Some(instructions) = overrides.instructions {
576 self.instructions = instructions;
577 }
578
579 if let Some(use_browser) = overrides.use_browser {
580 let mut config = self.browser_config.clone().unwrap_or_default();
581 config.enabled = use_browser;
582 self.browser_config = Some(config);
583 }
584
585 if let Some(dynamic_tools) = overrides.dynamic_tools {
587 let tools = self.tools.get_or_insert_with(ToolsConfig::default);
588 tools.dynamic.extend(dynamic_tools);
589 }
590 }
591}
592
593pub const VALID_BUILTIN_TOOLS: &[&str] = &[
600 "final",
602 "reflect",
603 "transfer_to_agent",
604 "browsr_scrape",
606 "browsr_browser",
607 "browsr_crawl",
608 "browser_step",
609 "search",
610 "start_shell",
612 "execute_shell",
613 "stop_shell",
614 "distri_execute_code",
616 "tool_search",
618 "load_skill",
619 "inject_connection_env",
621 "console_log",
623 "artifact_tool",
625 "todos",
627];
628
629#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, Default)]
630#[serde(deny_unknown_fields)]
631pub struct ToolsConfig {
632 #[serde(default, skip_serializing_if = "Vec::is_empty")]
634 pub builtin: Vec<String>,
635
636 #[serde(default, skip_serializing_if = "Vec::is_empty")]
638 pub dynamic: Vec<crate::dynamic_tool::DynamicToolFactory>,
639
640 #[serde(default, skip_serializing_if = "Vec::is_empty")]
642 pub mcp: Vec<McpToolConfig>,
643
644 #[serde(default, skip_serializing_if = "Option::is_none")]
646 pub external: Option<Vec<String>>,
647}
648
649impl ToolsConfig {
650 pub fn invalid_builtin_tools(&self) -> Vec<String> {
653 self.builtin
654 .iter()
655 .filter(|name| !VALID_BUILTIN_TOOLS.contains(&name.as_str()))
656 .cloned()
657 .collect()
658 }
659}
660
661#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, Default, PartialEq, Eq)]
663#[serde(rename_all = "snake_case")]
664pub enum FileSystemMode {
665 #[default]
667 Remote,
668 Local,
670}
671
672impl FileSystemMode {
673 pub fn include_server_tools(&self) -> bool {
674 !matches!(self, FileSystemMode::Local)
675 }
676}
677
678#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
680#[serde(deny_unknown_fields)]
681pub struct McpToolConfig {
682 pub server: String,
684
685 #[serde(default, skip_serializing_if = "Vec::is_empty")]
688 pub include: Vec<String>,
689
690 #[serde(default, skip_serializing_if = "Vec::is_empty")]
692 pub exclude: Vec<String>,
693}
694
695#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
696#[serde(deny_unknown_fields)]
697pub struct McpDefinition {
698 #[serde(default)]
700 pub filter: Option<Vec<String>>,
701 pub name: String,
703 #[serde(default)]
705 pub r#type: McpServerType,
706 #[serde(default)]
708 pub auth_config: Option<crate::a2a::SecurityScheme>,
709}
710
711#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema, PartialEq)]
712#[serde(rename_all = "lowercase")]
713pub enum McpServerType {
714 #[default]
715 Tool,
716 Agent,
717}
718
719#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
720#[serde(deny_unknown_fields, rename_all = "lowercase", tag = "name")]
721pub enum ModelProvider {
722 #[serde(rename = "openai")]
723 OpenAI {},
724 #[serde(rename = "openai_compat")]
725 OpenAICompatible {
726 base_url: String,
727 api_key: Option<String>,
728 project_id: Option<String>,
729 },
730 #[serde(rename = "azure_openai")]
731 AzureOpenAI {
732 base_url: String,
734 api_key: Option<String>,
736 deployment: String,
738 #[serde(default = "ModelProvider::azure_api_version")]
740 api_version: String,
741 },
742 #[serde(rename = "anthropic")]
743 Anthropic {
744 #[serde(default = "ModelProvider::anthropic_base_url")]
745 base_url: Option<String>,
746 api_key: Option<String>,
747 },
748 #[serde(rename = "vllora")]
749 Vllora {
750 #[serde(default = "ModelProvider::vllora_url")]
751 base_url: String,
752 },
753}
754#[derive(Debug, Clone, Serialize, Deserialize)]
756pub struct ProviderSecretDefinition {
757 pub id: String,
759 pub label: String,
761 pub keys: Vec<SecretKeyDefinition>,
763}
764
765#[derive(Debug, Clone, Serialize, Deserialize)]
767pub struct SecretKeyDefinition {
768 pub key: String,
770 pub label: String,
772 pub placeholder: String,
774 #[serde(default = "default_required")]
776 pub required: bool,
777 #[serde(default = "default_sensitive")]
780 pub sensitive: bool,
781}
782
783fn default_required() -> bool {
784 true
785}
786
787fn default_sensitive() -> bool {
788 true
789}
790
791#[derive(Debug, Clone, Serialize, Deserialize)]
793pub struct ModelInfo {
794 pub id: String,
796 pub name: String,
798}
799
800#[derive(Debug, Clone, Serialize, Deserialize)]
803struct DefaultProviderEntry {
804 id: String,
805 label: String,
806 keys: Vec<SecretKeyDefinition>,
807 models: Vec<ModelInfo>,
808}
809
810#[derive(Debug, Clone, Serialize, Deserialize)]
811struct DefaultModelsFile {
812 providers: Vec<DefaultProviderEntry>,
813}
814
815fn load_default_providers() -> &'static [DefaultProviderEntry] {
816 use std::sync::OnceLock;
817 static PROVIDERS: OnceLock<Vec<DefaultProviderEntry>> = OnceLock::new();
818 PROVIDERS.get_or_init(|| {
819 let json = include_str!("default_models.json");
820 let file: DefaultModelsFile =
821 serde_json::from_str(json).expect("Failed to parse default_models.json");
822 file.providers
823 })
824}
825
826#[derive(Debug, Clone, Serialize, Deserialize)]
828pub struct ProviderModels {
829 pub provider_id: String,
831 pub provider_label: String,
833 pub models: Vec<ModelInfo>,
835}
836
837#[derive(Debug, Clone, Serialize, Deserialize)]
839pub struct ProviderModelsStatus {
840 pub provider_id: String,
842 pub provider_label: String,
844 pub configured: bool,
846 pub models: Vec<ModelInfo>,
848}
849
850impl Default for ModelProvider {
851 fn default() -> Self {
852 ModelProvider::OpenAI {}
853 }
854}
855
856impl ModelProvider {
857 pub fn openai_base_url() -> String {
858 "https://api.openai.com/v1".to_string()
859 }
860
861 pub fn anthropic_base_url() -> Option<String> {
862 None }
864
865 pub fn vllora_url() -> String {
866 "http://localhost:9090/v1".to_string()
867 }
868
869 pub fn azure_api_version() -> String {
870 "2024-06-01".to_string()
871 }
872
873 pub fn provider_id(&self) -> &'static str {
875 match self {
876 ModelProvider::OpenAI {} => "openai",
877 ModelProvider::OpenAICompatible { .. } => "openai_compat",
878 ModelProvider::AzureOpenAI { .. } => "azure_openai",
879 ModelProvider::Anthropic { .. } => "anthropic",
880 ModelProvider::Vllora { .. } => "vllora",
881 }
882 }
883
884 pub fn required_secret_keys(&self) -> Vec<&'static str> {
886 match self {
887 ModelProvider::OpenAI {} => vec!["OPENAI_API_KEY"],
888 ModelProvider::OpenAICompatible { api_key, .. } => {
889 if api_key.is_some() {
890 vec![]
891 } else {
892 vec!["OPENAI_API_KEY"]
893 }
894 }
895 ModelProvider::AzureOpenAI { api_key, .. } => {
896 if api_key.is_some() {
897 vec![]
898 } else {
899 vec!["AZURE_OPENAI_API_KEY"]
900 }
901 }
902 ModelProvider::Anthropic { api_key, .. } => {
903 if api_key.is_some() {
904 vec![]
905 } else {
906 vec!["ANTHROPIC_API_KEY"]
907 }
908 }
909 ModelProvider::Vllora { .. } => vec![],
910 }
911 }
912
913 pub fn all_provider_definitions() -> Vec<ProviderSecretDefinition> {
915 load_default_providers()
916 .iter()
917 .map(|p| ProviderSecretDefinition {
918 id: p.id.clone(),
919 label: p.label.clone(),
920 keys: p.keys.clone(),
921 })
922 .collect()
923 }
924
925 pub fn well_known_models() -> Vec<ProviderModels> {
927 load_default_providers()
928 .iter()
929 .filter(|p| !p.models.is_empty())
930 .map(|p| ProviderModels {
931 provider_id: p.id.clone(),
932 provider_label: p.label.clone(),
933 models: p.models.clone(),
934 })
935 .collect()
936 }
937
938 pub fn display_name(&self) -> &'static str {
940 match self {
941 ModelProvider::OpenAI {} => "OpenAI",
942 ModelProvider::OpenAICompatible { .. } => "OpenAI Compatible",
943 ModelProvider::AzureOpenAI { .. } => "Azure OpenAI",
944 ModelProvider::Anthropic { .. } => "Anthropic",
945 ModelProvider::Vllora { .. } => "vLLORA",
946 }
947 }
948}
949
950#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
954pub struct ModelSettings {
955 pub model: String,
956 #[serde(flatten)]
957 pub inner: ModelSettingsInner,
958}
959
960#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, Default)]
964pub struct ModelSettingsInner {
965 #[serde(default, skip_serializing_if = "Option::is_none")]
966 pub temperature: Option<f32>,
967 #[serde(default, skip_serializing_if = "Option::is_none")]
968 pub max_tokens: Option<u32>,
969 #[serde(default = "default_context_size")]
970 pub context_size: u32,
971 #[serde(default, skip_serializing_if = "Option::is_none")]
972 pub top_p: Option<f32>,
973 #[serde(default, skip_serializing_if = "Option::is_none")]
974 pub frequency_penalty: Option<f32>,
975 #[serde(default, skip_serializing_if = "Option::is_none")]
976 pub presence_penalty: Option<f32>,
977 #[serde(default = "default_model_provider")]
978 pub provider: ModelProvider,
979 #[serde(default)]
981 pub parameters: Option<serde_json::Value>,
982 #[serde(default)]
984 pub response_format: Option<serde_json::Value>,
985}
986
987impl ModelSettings {
988 pub fn new(model: impl Into<String>) -> Self {
990 Self {
991 model: model.into(),
992 inner: ModelSettingsInner::default(),
993 }
994 }
995
996 pub fn from_provider_model_str(s: &str) -> Result<Option<Self>, String> {
1004 let Some((provider_str, model_id)) = s.split_once('/') else {
1005 return Ok(None);
1006 };
1007 if model_id.is_empty() {
1008 return Ok(None);
1009 }
1010 let provider = match provider_str {
1011 "openai" => ModelProvider::OpenAI {},
1012 "anthropic" => ModelProvider::Anthropic {
1013 base_url: None,
1014 api_key: None,
1015 },
1016 "azure_openai" => ModelProvider::AzureOpenAI {
1017 base_url: String::new(),
1018 api_key: None,
1019 deployment: model_id.to_string(),
1020 api_version: ModelProvider::azure_api_version(),
1021 },
1022 "gemini" => ModelProvider::OpenAICompatible {
1023 base_url: "https://generativelanguage.googleapis.com/v1beta/openai".to_string(),
1024 api_key: None,
1025 project_id: None,
1026 },
1027 _ if provider_str.starts_with("custom_") => ModelProvider::OpenAICompatible {
1028 base_url: String::new(),
1029 api_key: None,
1030 project_id: None,
1031 },
1032 unknown => {
1033 return Err(format!(
1034 "Provider '{}' is not recognized. Supported providers: openai, anthropic, azure_openai, gemini, or custom_* prefixed providers.",
1035 unknown
1036 ));
1037 }
1038 };
1039 Ok(Some(Self {
1040 model: model_id.to_string(),
1041 inner: ModelSettingsInner {
1042 provider,
1043 ..Default::default()
1044 },
1045 }))
1046 }
1047}
1048
1049pub fn default_agent_version() -> Option<String> {
1051 Some("0.2.2".to_string())
1052}
1053
1054fn default_model_provider() -> ModelProvider {
1055 ModelProvider::OpenAI {}
1056}
1057
1058fn default_context_size() -> u32 {
1059 20000 }
1061
1062fn default_history_size() -> Option<usize> {
1063 Some(5)
1064}
1065
1066impl StandardDefinition {
1067 pub fn validate(&self) -> anyhow::Result<()> {
1068 if self.name.is_empty() {
1070 return Err(anyhow::anyhow!("Agent name cannot be empty"));
1071 }
1072
1073 if let Some(ref reflection) = self.reflection
1075 && reflection.enabled
1076 {
1077 if let Some(ref agent_name) = reflection.reflection_agent
1079 && agent_name.is_empty()
1080 {
1081 return Err(anyhow::anyhow!(
1082 "Reflection agent name cannot be empty when specified"
1083 ));
1084 }
1085 }
1086
1087 Ok(())
1088 }
1089
1090 pub fn validate_reflection_agent(agent_def: &StandardDefinition) -> anyhow::Result<()> {
1093 let has_reflect_tool = agent_def
1094 .tools
1095 .as_ref()
1096 .map(|t| t.builtin.iter().any(|name| name == "reflect"))
1097 .unwrap_or(false);
1098
1099 if !has_reflect_tool {
1100 anyhow::bail!(
1103 "Reflection agent '{}' must have the 'reflect' tool in its tools.builtin configuration",
1104 agent_def.name
1105 );
1106 }
1107
1108 Ok(())
1109 }
1110}
1111
1112impl From<StandardDefinition> for LlmDefinition {
1113 fn from(definition: StandardDefinition) -> Self {
1114 let model_settings = match (definition.model_settings, definition.context_size) {
1115 (Some(mut ms), Some(ctx)) => {
1116 ms.inner.context_size = ctx;
1117 Some(ms)
1118 }
1119 (ms, _) => ms,
1120 };
1121
1122 Self {
1123 name: definition.name,
1124 model_settings,
1125 tool_format: definition.tool_format,
1126 tool_delivery_mode: definition.tool_delivery_mode,
1127 }
1128 }
1129}
1130
1131impl ToolsConfig {
1132 pub fn builtin_only(tools: Vec<&str>) -> Self {
1134 Self {
1135 builtin: tools.into_iter().map(|s| s.to_string()).collect(),
1136 dynamic: vec![],
1137 mcp: vec![],
1138 external: None,
1139 }
1140 }
1141
1142 pub fn mcp_all(server: &str) -> Self {
1144 Self {
1145 builtin: vec![],
1146 dynamic: vec![],
1147 mcp: vec![McpToolConfig {
1148 server: server.to_string(),
1149 include: vec!["*".to_string()],
1150 exclude: vec![],
1151 }],
1152 external: None,
1153 }
1154 }
1155
1156 pub fn mcp_filtered(server: &str, include: Vec<&str>, exclude: Vec<&str>) -> Self {
1158 Self {
1159 builtin: vec![],
1160 dynamic: vec![],
1161 mcp: vec![McpToolConfig {
1162 server: server.to_string(),
1163 include: include.into_iter().map(|s| s.to_string()).collect(),
1164 exclude: exclude.into_iter().map(|s| s.to_string()).collect(),
1165 }],
1166 external: None,
1167 }
1168 }
1169}
1170
1171pub async fn parse_agent_markdown_content(content: &str) -> Result<StandardDefinition, AgentError> {
1172 let parts: Vec<&str> = content.split("---").collect();
1174
1175 if parts.len() < 3 {
1176 return Err(AgentError::Validation(
1177 "Invalid agent markdown format. Expected TOML frontmatter between --- markers"
1178 .to_string(),
1179 ));
1180 }
1181
1182 let toml_content = parts[1].trim();
1184 let mut agent_def: crate::StandardDefinition =
1185 toml::from_str(toml_content).map_err(|e| AgentError::Validation(e.to_string()))?;
1186
1187 if let Err(validation_error) = validate_plugin_name(&agent_def.name) {
1189 return Err(AgentError::Validation(format!(
1190 "Invalid agent name '{}': {}",
1191 agent_def.name, validation_error
1192 )));
1193 }
1194
1195 if !agent_def
1197 .name
1198 .chars()
1199 .all(|c| c.is_alphanumeric() || c == '_')
1200 || agent_def
1201 .name
1202 .chars()
1203 .next()
1204 .is_some_and(|c| c.is_numeric())
1205 {
1206 return Err(AgentError::Validation(format!(
1207 "Invalid agent name '{}': Agent names must be valid JavaScript identifiers (alphanumeric + underscores, cannot start with number). \
1208 Reason: Agent names become function names in TypeScript runtime.",
1209 agent_def.name
1210 )));
1211 }
1212
1213 let instructions = parts[2..].join("---").trim().to_string();
1215
1216 agent_def.instructions = instructions;
1218
1219 Ok(agent_def)
1220}
1221
1222pub fn validate_plugin_name(name: &str) -> Result<(), String> {
1225 if name.contains('-') {
1226 return Err(format!(
1227 "Plugin name '{}' cannot contain hyphens. Use underscores instead.",
1228 name
1229 ));
1230 }
1231
1232 if name.is_empty() {
1233 return Err("Plugin name cannot be empty".to_string());
1234 }
1235
1236 if let Some(first_char) = name.chars().next()
1238 && !first_char.is_ascii_alphabetic()
1239 && first_char != '_'
1240 {
1241 return Err(format!(
1242 "Plugin name '{}' must start with a letter or underscore",
1243 name
1244 ));
1245 }
1246
1247 for ch in name.chars() {
1249 if !ch.is_ascii_alphanumeric() && ch != '_' {
1250 return Err(format!(
1251 "Plugin name '{}' can only contain letters, numbers, and underscores",
1252 name
1253 ));
1254 }
1255 }
1256
1257 Ok(())
1258}
1259
1260#[cfg(test)]
1261mod tests {
1262 use super::*;
1263
1264 #[test]
1265 fn test_compaction_enabled_defaults_to_true_via_serde() {
1266 let json = r#"{"name": "test"}"#;
1268 let def: StandardDefinition = serde_json::from_str(json).unwrap();
1269 assert!(def.compaction_enabled);
1270 }
1271
1272 #[test]
1273 fn test_compaction_enabled_deserializes_true_when_absent() {
1274 let json = r#"{"name": "test", "description": "test agent"}"#;
1275 let def: StandardDefinition = serde_json::from_str(json).unwrap();
1276 assert!(def.compaction_enabled);
1277 }
1278
1279 #[test]
1280 fn test_compaction_enabled_deserializes_false() {
1281 let json = r#"{"name": "test", "description": "test agent", "compaction_enabled": false}"#;
1282 let def: StandardDefinition = serde_json::from_str(json).unwrap();
1283 assert!(!def.compaction_enabled);
1284 }
1285
1286 #[test]
1287 fn test_compaction_enabled_true_skipped_in_serialization() {
1288 let def = StandardDefinition {
1289 name: "test".to_string(),
1290 compaction_enabled: true,
1291 ..Default::default()
1292 };
1293 let json = serde_json::to_string(&def).unwrap();
1294 assert!(!json.contains("compaction_enabled"));
1295 }
1296
1297 #[test]
1298 fn test_compaction_enabled_false_serialized() {
1299 let def = StandardDefinition {
1300 name: "test".to_string(),
1301 compaction_enabled: false,
1302 ..Default::default()
1303 };
1304 let json = serde_json::to_string(&def).unwrap();
1305 assert!(json.contains("\"compaction_enabled\":false"));
1306 }
1307
1308 #[test]
1309 fn test_max_tokens_optional_defaults_to_none() {
1310 let def = StandardDefinition::default();
1311 assert!(def.model_settings().is_none());
1312 }
1313
1314 #[test]
1315 fn test_max_tokens_deserializes_when_present() {
1316 let json =
1317 r#"{"name": "test", "model_settings": {"model": "gpt-4.1", "max_tokens": 4096}}"#;
1318 let def: StandardDefinition = serde_json::from_str(json).unwrap();
1319 assert_eq!(def.model_settings().unwrap().inner.max_tokens, Some(4096));
1320 }
1321
1322 #[test]
1323 fn test_max_tokens_none_when_absent() {
1324 let json = r#"{"name": "test", "model_settings": {"model": "gpt-4.1"}}"#;
1325 let def: StandardDefinition = serde_json::from_str(json).unwrap();
1326 assert!(def.model_settings().unwrap().inner.max_tokens.is_none());
1327 }
1328
1329 #[test]
1330 fn test_max_tokens_none_skipped_in_serialization() {
1331 let settings = ModelSettings {
1332 model: "test-model".to_string(),
1333 inner: ModelSettingsInner {
1334 max_tokens: None,
1335 provider: ModelProvider::OpenAI {},
1336 ..Default::default()
1337 },
1338 };
1339 let json = serde_json::to_string(&settings).unwrap();
1340 assert!(!json.contains("max_tokens"));
1341 }
1342
1343 #[test]
1344 fn test_max_tokens_some_serialized() {
1345 let settings = ModelSettings {
1346 model: "test-model".to_string(),
1347 inner: ModelSettingsInner {
1348 max_tokens: Some(2048),
1349 provider: ModelProvider::OpenAI {},
1350 ..Default::default()
1351 },
1352 };
1353 let json = serde_json::to_string(&settings).unwrap();
1354 assert!(json.contains("\"max_tokens\":2048"));
1355 }
1356}