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")]
27pub struct AgentStrategy {
28 #[serde(default, skip_serializing_if = "Option::is_none")]
30 pub reasoning_depth: Option<ReasoningDepth>,
31
32 #[serde(default, skip_serializing_if = "Option::is_none")]
34 pub execution_mode: Option<ExecutionMode>,
35 #[serde(default, skip_serializing_if = "Option::is_none")]
37 pub replanning: Option<ReplanningConfig>,
38
39 #[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 pub fn get_reasoning_depth(&self) -> ReasoningDepth {
59 self.reasoning_depth.clone().unwrap_or_default()
60 }
61
62 pub fn get_execution_mode(&self) -> ExecutionMode {
64 self.execution_mode.clone().unwrap_or_default()
65 }
66
67 pub fn get_replanning(&self) -> ReplanningConfig {
69 self.replanning.clone().unwrap_or_default()
70 }
71
72 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#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, Default)]
102pub struct ReflectionConfig {
103 #[serde(default)]
105 pub enabled: bool,
106 #[serde(default, skip_serializing_if = "Option::is_none")]
110 pub reflection_agent: Option<String>,
111 #[serde(default)]
113 pub trigger: ReflectionTrigger,
114 #[serde(default)]
116 pub depth: ReflectionDepth,
117}
118
119#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, Default)]
121#[serde(rename_all = "snake_case")]
122pub enum ReflectionTrigger {
123 #[default]
125 EndOfExecution,
126 AfterEachStep,
128 AfterFailures,
130 AfterNSteps(usize),
132}
133
134#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, Default)]
136#[serde(rename_all = "snake_case")]
137pub enum ReflectionDepth {
138 #[default]
140 Light,
141 Standard,
143 Deep,
145}
146
147#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
149pub struct PlanConfig {
150 #[serde(default, skip_serializing_if = "Option::is_none")]
152 pub model_settings: Option<ModelSettings>,
153 #[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#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, Default, PartialEq)]
173#[serde(rename_all = "snake_case")]
174pub enum ReasoningDepth {
175 Shallow,
177 #[default]
179 Standard,
180 Deep,
182}
183
184#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, Default, PartialEq)]
186#[serde(rename_all = "snake_case", tag = "type")]
187pub enum ExecutionMode {
188 #[default]
190 Tools,
191 Code { language: CodeLanguage },
193}
194
195#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, Default)]
197#[serde(rename_all = "snake_case")]
198pub struct ReplanningConfig {
199 #[serde(default, skip_serializing_if = "Option::is_none")]
201 pub trigger: Option<ReplanningTrigger>,
202 #[serde(default, skip_serializing_if = "Option::is_none")]
204 pub enabled: Option<bool>,
205}
206
207#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, Default)]
209#[serde(rename_all = "snake_case")]
210pub enum ReplanningTrigger {
211 #[default]
213 Never,
214 AfterReflection,
216 AfterNIterations(usize),
218 AfterFailures,
220}
221
222impl ReplanningConfig {
223 pub fn get_trigger(&self) -> ReplanningTrigger {
225 self.trigger.clone().unwrap_or_default()
226 }
227
228 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#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, Default, PartialEq)]
253#[serde(rename_all = "snake_case")]
254pub enum ToolDeliveryMode {
255 #[default]
257 AllTools,
258 ToolSearch,
260}
261
262#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, Default, PartialEq)]
264#[serde(rename_all = "snake_case")]
265pub enum ToolCallFormat {
266 #[default]
269 Xml,
270 JsonL,
273
274 Code,
277 #[serde(rename = "provider")]
278 Provider,
279 None,
280}
281
282#[derive(Debug, Serialize, Deserialize, Clone, JsonSchema, Default)]
283pub struct UserMessageOverrides {
284 pub parts: Vec<PartDefinition>,
286 #[serde(default)]
288 pub include_artifacts: bool,
289 #[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), SessionKey(String), }
304
305#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
306#[serde(deny_unknown_fields)]
307pub struct LlmDefinition {
308 pub name: String,
310 #[serde(default, skip_serializing_if = "Option::is_none")]
312 pub model_settings: Option<ModelSettings>,
313 #[serde(default)]
315 pub tool_format: ToolCallFormat,
316 #[serde(default)]
318 pub tool_delivery_mode: ToolDeliveryMode,
319}
320
321impl LlmDefinition {
322 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 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#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, Default)]
343pub struct StandardDefinition {
344 pub name: String,
346 #[serde(default, skip_serializing_if = "Option::is_none")]
348 pub package_name: Option<String>,
349 #[serde(default)]
351 pub description: String,
352
353 #[serde(default = "default_agent_version")]
355 pub version: Option<String>,
356
357 #[serde(default)]
359 pub instructions: String,
360
361 #[serde(default)]
363 pub mcp_servers: Option<Vec<McpDefinition>>,
364 #[serde(default, skip_serializing_if = "Option::is_none")]
367 pub model_settings: Option<ModelSettings>,
368 #[serde(default, skip_serializing_if = "Option::is_none")]
370 pub analysis_model_settings: Option<ModelSettings>,
371
372 #[serde(default = "default_history_size")]
374 pub history_size: Option<usize>,
375 #[serde(default, skip_serializing_if = "Option::is_none")]
377 pub strategy: Option<AgentStrategy>,
378 #[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 #[serde(default, skip_serializing_if = "Vec::is_empty")]
387 pub skills_description: Vec<AgentSkill>,
388
389 #[serde(default, skip_serializing_if = "Vec::is_empty")]
391 pub available_skills: Vec<AvailableSkill>,
392
393 #[serde(default)]
395 pub sub_agents: Vec<String>,
396
397 #[serde(default)]
399 pub tool_format: ToolCallFormat,
400
401 #[serde(default)]
403 pub tool_delivery_mode: ToolDeliveryMode,
404
405 #[serde(default, skip_serializing_if = "Option::is_none")]
407 pub tools: Option<ToolsConfig>,
408
409 #[serde(default)]
411 pub file_system: FileSystemMode,
412
413 #[serde(default, skip_serializing_if = "std::collections::HashMap::is_empty")]
415 pub partials: std::collections::HashMap<String, String>,
416
417 #[serde(default, skip_serializing_if = "Option::is_none")]
419 pub write_large_tool_responses_to_fs: Option<bool>,
420
421 #[serde(default, skip_serializing_if = "Option::is_none")]
423 pub reflection: Option<ReflectionConfig>,
424 #[serde(default, skip_serializing_if = "Option::is_none")]
426 pub enable_todos: Option<bool>,
427
428 #[serde(default, skip_serializing_if = "Option::is_none")]
430 pub browser_config: Option<BrowserAgentConfig>,
431
432 #[serde(default, skip_serializing_if = "Option::is_none")]
434 pub include_shell: Option<bool>,
435
436 #[serde(default, skip_serializing_if = "Option::is_none")]
438 pub context_size: Option<u32>,
439
440 #[serde(
442 skip_serializing_if = "Option::is_none",
443 default = "default_append_default_instructions"
444 )]
445 pub append_default_instructions: Option<bool>,
446 #[serde(
448 skip_serializing_if = "Option::is_none",
449 default = "default_include_scratchpad"
450 )]
451 pub include_scratchpad: Option<bool>,
452
453 #[serde(default, skip_serializing_if = "Vec::is_empty")]
455 pub hooks: Vec<String>,
456
457 #[serde(default, skip_serializing_if = "Option::is_none")]
459 pub user_message_overrides: Option<UserMessageOverrides>,
460
461 #[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 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 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 pub fn browser_settings(&self) -> Option<&BrowserAgentConfig> {
496 self.browser_config.as_ref()
497 }
498
499 pub fn browser_runtime_config(&self) -> Option<BrowsrClientConfig> {
501 self.browser_config.as_ref().map(|cfg| cfg.runtime_config())
502 }
503
504 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 pub fn is_reflection_enabled(&self) -> bool {
514 self.reflection.as_ref().map(|r| r.enabled).unwrap_or(false)
515 }
516
517 pub fn reflection_config(&self) -> Option<&ReflectionConfig> {
519 self.reflection.as_ref().filter(|r| r.enabled)
520 }
521 pub fn is_todos_enabled(&self) -> bool {
523 self.enable_todos.unwrap_or(false)
524 }
525
526 pub fn should_include_shell(&self) -> bool {
528 self.include_shell.unwrap_or(false)
529 }
530
531 pub fn model_settings(&self) -> Option<&ModelSettings> {
533 self.model_settings.as_ref()
534 }
535
536 pub fn model_settings_mut(&mut self) -> Option<&mut ModelSettings> {
538 self.model_settings.as_mut()
539 }
540
541 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 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 pub fn include_scratchpad(&self) -> bool {
557 self.include_scratchpad.unwrap_or(true)
558 }
559
560 pub fn apply_overrides(&mut self, overrides: DefinitionOverrides) {
562 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 if let Some(max_iterations) = overrides.max_iterations {
577 self.max_iterations = Some(max_iterations);
578 }
579
580 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#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, Default)]
595#[serde(deny_unknown_fields)]
596pub struct ToolsConfig {
597 #[serde(default, skip_serializing_if = "Vec::is_empty")]
599 pub builtin: Vec<String>,
600
601 #[serde(default, skip_serializing_if = "std::collections::HashMap::is_empty")]
603 pub packages: std::collections::HashMap<String, Vec<String>>,
604
605 #[serde(default, skip_serializing_if = "Vec::is_empty")]
607 pub mcp: Vec<McpToolConfig>,
608
609 #[serde(default, skip_serializing_if = "Option::is_none")]
611 pub external: Option<Vec<String>>,
612}
613
614#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, Default, PartialEq, Eq)]
616#[serde(rename_all = "snake_case")]
617pub enum FileSystemMode {
618 #[default]
620 Remote,
621 Local,
623}
624
625impl FileSystemMode {
626 pub fn include_server_tools(&self) -> bool {
627 !matches!(self, FileSystemMode::Local)
628 }
629}
630
631#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
633#[serde(deny_unknown_fields)]
634pub struct McpToolConfig {
635 pub server: String,
637
638 #[serde(default, skip_serializing_if = "Vec::is_empty")]
641 pub include: Vec<String>,
642
643 #[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 #[serde(default)]
653 pub filter: Option<Vec<String>>,
654 pub name: String,
656 #[serde(default)]
658 pub r#type: McpServerType,
659 #[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 base_url: String,
687 api_key: Option<String>,
689 deployment: String,
691 #[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#[derive(Debug, Clone, Serialize, Deserialize)]
709pub struct ProviderSecretDefinition {
710 pub id: String,
712 pub label: String,
714 pub keys: Vec<SecretKeyDefinition>,
716}
717
718#[derive(Debug, Clone, Serialize, Deserialize)]
720pub struct SecretKeyDefinition {
721 pub key: String,
723 pub label: String,
725 pub placeholder: String,
727 #[serde(default = "default_required")]
729 pub required: bool,
730}
731
732fn default_required() -> bool {
733 true
734}
735
736#[derive(Debug, Clone, Serialize, Deserialize)]
738pub struct ModelInfo {
739 pub id: String,
741 pub name: String,
743}
744
745#[derive(Debug, Clone, Serialize, Deserialize)]
747pub struct ProviderModels {
748 pub provider_id: String,
750 pub provider_label: String,
752 pub models: Vec<ModelInfo>,
754}
755
756#[derive(Debug, Clone, Serialize, Deserialize)]
758pub struct ProviderModelsStatus {
759 pub provider_id: String,
761 pub provider_label: String,
763 pub configured: bool,
765 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 }
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 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 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 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 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 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#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
942pub struct ModelSettings {
943 pub model: String,
944 #[serde(flatten)]
945 pub inner: ModelSettingsInner,
946}
947
948#[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 #[serde(default)]
969 pub parameters: Option<serde_json::Value>,
970 #[serde(default)]
972 pub response_format: Option<serde_json::Value>,
973}
974
975impl ModelSettings {
976 pub fn new(model: impl Into<String>) -> Self {
978 Self {
979 model: model.into(),
980 inner: ModelSettingsInner::default(),
981 }
982 }
983
984 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
1007pub 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 }
1019
1020fn default_history_size() -> Option<usize> {
1021 Some(5)
1022}
1023
1024impl StandardDefinition {
1025 pub fn validate(&self) -> anyhow::Result<()> {
1026 if self.name.is_empty() {
1028 return Err(anyhow::anyhow!("Agent name cannot be empty"));
1029 }
1030
1031 if let Some(ref reflection) = self.reflection {
1033 if reflection.enabled {
1034 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 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 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 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 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 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 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 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 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 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 let instructions = parts[2..].join("---").trim().to_string();
1173
1174 agent_def.instructions = instructions;
1176
1177 Ok(agent_def)
1178}
1179
1180pub 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 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 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 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}