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)]
108 pub trigger: ReflectionTrigger,
109 #[serde(default)]
111 pub depth: ReflectionDepth,
112}
113
114#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, Default)]
116#[serde(rename_all = "snake_case")]
117pub enum ReflectionTrigger {
118 #[default]
120 EndOfExecution,
121 AfterEachStep,
123 AfterFailures,
125 AfterNSteps(usize),
127}
128
129#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, Default)]
131#[serde(rename_all = "snake_case")]
132pub enum ReflectionDepth {
133 #[default]
135 Light,
136 Standard,
138 Deep,
140}
141
142#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
144pub struct PlanConfig {
145 #[serde(default)]
147 pub model_settings: ModelSettings,
148 #[serde(default = "default_plan_max_iterations")]
150 pub max_iterations: usize,
151}
152
153impl Default for PlanConfig {
154 fn default() -> Self {
155 Self {
156 model_settings: ModelSettings::default(),
157 max_iterations: default_plan_max_iterations(),
158 }
159 }
160}
161
162fn default_plan_max_iterations() -> usize {
163 10
164}
165
166#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, Default, PartialEq)]
168#[serde(rename_all = "snake_case")]
169pub enum ReasoningDepth {
170 Shallow,
172 #[default]
174 Standard,
175 Deep,
177}
178
179#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, Default, PartialEq)]
181#[serde(rename_all = "snake_case", tag = "type")]
182pub enum ExecutionMode {
183 #[default]
185 Tools,
186 Code { language: CodeLanguage },
188}
189
190#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, Default)]
192#[serde(rename_all = "snake_case")]
193pub struct ReplanningConfig {
194 #[serde(default, skip_serializing_if = "Option::is_none")]
196 pub trigger: Option<ReplanningTrigger>,
197 #[serde(default, skip_serializing_if = "Option::is_none")]
199 pub enabled: Option<bool>,
200}
201
202#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, Default)]
204#[serde(rename_all = "snake_case")]
205pub enum ReplanningTrigger {
206 #[default]
208 Never,
209 AfterReflection,
211 AfterNIterations(usize),
213 AfterFailures,
215}
216
217impl ReplanningConfig {
218 pub fn get_trigger(&self) -> ReplanningTrigger {
220 self.trigger.clone().unwrap_or_default()
221 }
222
223 pub fn is_enabled(&self) -> bool {
225 self.enabled.unwrap_or(false)
226 }
227}
228
229#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, Default)]
230#[serde(rename_all = "snake_case")]
231pub enum ExecutionKind {
232 #[default]
233 Retriable,
234 Interleaved,
235 Sequential,
236}
237
238#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, Default)]
239#[serde(rename_all = "snake_case")]
240pub enum MemoryKind {
241 #[default]
242 None,
243 ShortTerm,
244 LongTerm,
245}
246
247#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, Default, PartialEq)]
249#[serde(rename_all = "snake_case")]
250pub enum ToolCallFormat {
251 #[default]
254 Xml,
255 JsonL,
258
259 Code,
262 #[serde(rename = "provider")]
263 Provider,
264 None,
265}
266
267#[derive(Debug, Serialize, Deserialize, Clone, JsonSchema, Default)]
268pub struct UserMessageOverrides {
269 pub parts: Vec<PartDefinition>,
271 #[serde(default)]
273 pub include_artifacts: bool,
274 #[serde(default = "default_include_step_count")]
276 pub include_step_count: Option<bool>,
277}
278
279fn default_include_step_count() -> Option<bool> {
280 Some(true)
281}
282
283#[derive(Debug, Serialize, Deserialize, Clone, JsonSchema)]
284#[serde(tag = "type", content = "source", rename_all = "snake_case")]
285pub enum PartDefinition {
286 Template(String), SessionKey(String), }
289
290#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, Default)]
291#[serde(deny_unknown_fields)]
292pub struct LlmDefinition {
293 pub name: String,
295 #[serde(default)]
297 pub model_settings: ModelSettings,
298 #[serde(default)]
300 pub tool_format: ToolCallFormat,
301}
302
303#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
305#[serde(tag = "mode", rename_all = "snake_case")]
306pub enum BrowserHooksConfig {
307 Disabled,
309 Webhook {
311 #[serde(default, skip_serializing_if = "Option::is_none")]
313 api_base_url: Option<String>,
314 },
315 Inline {
317 #[serde(default)]
319 timeout_ms: Option<u64>,
320 },
321}
322
323impl Default for BrowserHooksConfig {
324 fn default() -> Self {
325 BrowserHooksConfig::Disabled
326 }
327}
328
329#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, Default)]
331#[serde(deny_unknown_fields)]
332pub struct StandardDefinition {
333 pub name: String,
335 #[serde(default, skip_serializing_if = "Option::is_none")]
337 pub package_name: Option<String>,
338 #[serde(default)]
340 pub description: String,
341
342 #[serde(default = "default_agent_version")]
344 pub version: Option<String>,
345
346 #[serde(default)]
348 pub instructions: String,
349
350 #[serde(default)]
352 pub mcp_servers: Option<Vec<McpDefinition>>,
353 #[serde(default)]
355 pub model_settings: ModelSettings,
356 #[serde(default, skip_serializing_if = "Option::is_none")]
358 pub analysis_model_settings: Option<ModelSettings>,
359
360 #[serde(default = "default_history_size")]
362 pub history_size: Option<usize>,
363 #[serde(default, skip_serializing_if = "Option::is_none")]
365 pub strategy: Option<AgentStrategy>,
366 #[serde(default)]
368 pub icon_url: Option<String>,
369
370 #[serde(default, skip_serializing_if = "Option::is_none")]
371 pub max_iterations: Option<usize>,
372
373 #[serde(default, skip_serializing_if = "Vec::is_empty")]
375 pub skills_description: Vec<AgentSkill>,
376
377 #[serde(default, skip_serializing_if = "Vec::is_empty")]
379 pub available_skills: Vec<AvailableSkill>,
380
381 #[serde(default)]
383 pub sub_agents: Vec<String>,
384
385 #[serde(default)]
387 pub tool_format: ToolCallFormat,
388
389 #[serde(default, skip_serializing_if = "Option::is_none")]
391 pub tools: Option<ToolsConfig>,
392
393 #[serde(default)]
395 pub file_system: FileSystemMode,
396
397 #[serde(default, skip_serializing_if = "std::collections::HashMap::is_empty")]
399 pub partials: std::collections::HashMap<String, String>,
400
401 #[serde(default, skip_serializing_if = "Option::is_none")]
403 pub write_large_tool_responses_to_fs: Option<bool>,
404
405 #[serde(default, skip_serializing_if = "Option::is_none")]
407 pub enable_reflection: Option<bool>,
408 #[serde(default, skip_serializing_if = "Option::is_none")]
410 pub enable_todos: Option<bool>,
411
412 #[serde(default, skip_serializing_if = "Option::is_none")]
414 pub browser_config: Option<BrowserAgentConfig>,
415 #[serde(default, skip_serializing_if = "Option::is_none")]
417 pub browser_hooks: Option<BrowserHooksConfig>,
418
419 #[serde(default, skip_serializing_if = "Option::is_none")]
421 pub context_size: Option<u32>,
422
423 #[serde(
425 skip_serializing_if = "Option::is_none",
426 default = "default_append_default_instructions"
427 )]
428 pub append_default_instructions: Option<bool>,
429 #[serde(
431 skip_serializing_if = "Option::is_none",
432 default = "default_include_scratchpad"
433 )]
434 pub include_scratchpad: Option<bool>,
435
436 #[serde(default, skip_serializing_if = "Vec::is_empty")]
438 pub hooks: Vec<String>,
439
440 #[serde(default, skip_serializing_if = "Option::is_none")]
442 pub user_message_overrides: Option<UserMessageOverrides>,
443}
444fn default_append_default_instructions() -> Option<bool> {
445 Some(true)
446}
447fn default_include_scratchpad() -> Option<bool> {
448 Some(true)
449}
450impl StandardDefinition {
451 pub fn should_write_large_tool_responses_to_fs(&self) -> bool {
453 self.write_large_tool_responses_to_fs.unwrap_or(false)
454 }
455
456 pub fn should_use_browser(&self) -> bool {
458 self.browser_config
459 .as_ref()
460 .map(|cfg| cfg.is_enabled())
461 .unwrap_or(false)
462 }
463
464 pub fn browser_settings(&self) -> Option<&BrowserAgentConfig> {
466 self.browser_config.as_ref()
467 }
468
469 pub fn browser_runtime_config(&self) -> Option<BrowsrClientConfig> {
471 self.browser_config.as_ref().map(|cfg| cfg.runtime_config())
472 }
473
474 pub fn should_persist_browser_session(&self) -> bool {
476 self.browser_config
477 .as_ref()
478 .map(|cfg| cfg.should_persist_session())
479 .unwrap_or(false)
480 }
481
482 pub fn is_reflection_enabled(&self) -> bool {
484 self.enable_reflection.unwrap_or(false)
485 }
486 pub fn is_todos_enabled(&self) -> bool {
488 self.enable_todos.unwrap_or(false)
489 }
490
491 pub fn get_effective_context_size(&self) -> u32 {
493 self.context_size
494 .unwrap_or(self.model_settings.context_size)
495 }
496
497 pub fn analysis_model_settings_config(&self) -> ModelSettings {
499 self.analysis_model_settings
500 .clone()
501 .unwrap_or_else(|| self.model_settings.clone())
502 }
503
504 pub fn include_scratchpad(&self) -> bool {
506 self.include_scratchpad.unwrap_or(true)
507 }
508
509 pub fn apply_overrides(&mut self, overrides: DefinitionOverrides) {
511 if let Some(model) = overrides.model {
513 self.model_settings.model = model;
514 }
515
516 if let Some(temperature) = overrides.temperature {
517 self.model_settings.temperature = temperature;
518 }
519
520 if let Some(max_tokens) = overrides.max_tokens {
521 self.model_settings.max_tokens = max_tokens;
522 }
523
524 if let Some(max_iterations) = overrides.max_iterations {
526 self.max_iterations = Some(max_iterations);
527 }
528
529 if let Some(instructions) = overrides.instructions {
531 self.instructions = instructions;
532 }
533
534 if let Some(use_browser) = overrides.use_browser {
535 let mut config = self.browser_config.clone().unwrap_or_default();
536 config.enabled = use_browser;
537 self.browser_config = Some(config);
538 }
539 }
540}
541
542#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, Default)]
544#[serde(deny_unknown_fields)]
545pub struct ToolsConfig {
546 #[serde(default, skip_serializing_if = "Vec::is_empty")]
548 pub builtin: Vec<String>,
549
550 #[serde(default, skip_serializing_if = "std::collections::HashMap::is_empty")]
552 pub packages: std::collections::HashMap<String, Vec<String>>,
553
554 #[serde(default, skip_serializing_if = "Vec::is_empty")]
556 pub mcp: Vec<McpToolConfig>,
557
558 #[serde(default, skip_serializing_if = "Option::is_none")]
560 pub external: Option<Vec<String>>,
561}
562
563#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, Default, PartialEq, Eq)]
565#[serde(rename_all = "snake_case")]
566pub enum FileSystemMode {
567 #[default]
569 Remote,
570 Local,
572}
573
574impl FileSystemMode {
575 pub fn include_server_tools(&self) -> bool {
576 !matches!(self, FileSystemMode::Local)
577 }
578}
579
580#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
582#[serde(deny_unknown_fields)]
583pub struct McpToolConfig {
584 pub server: String,
586
587 #[serde(default, skip_serializing_if = "Vec::is_empty")]
590 pub include: Vec<String>,
591
592 #[serde(default, skip_serializing_if = "Vec::is_empty")]
594 pub exclude: Vec<String>,
595}
596
597#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
598#[serde(deny_unknown_fields)]
599pub struct McpDefinition {
600 #[serde(default)]
602 pub filter: Option<Vec<String>>,
603 pub name: String,
605 #[serde(default)]
607 pub r#type: McpServerType,
608 #[serde(default)]
610 pub auth_config: Option<crate::a2a::SecurityScheme>,
611}
612
613#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema, PartialEq)]
614#[serde(rename_all = "lowercase")]
615pub enum McpServerType {
616 #[default]
617 Tool,
618 Agent,
619}
620
621#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
622#[serde(deny_unknown_fields, rename_all = "lowercase", tag = "name")]
623pub enum ModelProvider {
624 #[serde(rename = "openai")]
625 OpenAI {},
626 #[serde(rename = "openai_compat")]
627 OpenAICompatible {
628 base_url: String,
629 api_key: Option<String>,
630 project_id: Option<String>,
631 },
632 #[serde(rename = "vllora")]
633 Vllora {
634 #[serde(default = "ModelProvider::vllora_url")]
635 base_url: String,
636 },
637}
638#[derive(Debug, Clone, Serialize, Deserialize)]
640pub struct ProviderSecretDefinition {
641 pub id: String,
643 pub label: String,
645 pub keys: Vec<SecretKeyDefinition>,
647}
648
649#[derive(Debug, Clone, Serialize, Deserialize)]
651pub struct SecretKeyDefinition {
652 pub key: String,
654 pub label: String,
656 pub placeholder: String,
658 #[serde(default = "default_required")]
660 pub required: bool,
661}
662
663fn default_required() -> bool {
664 true
665}
666
667impl ModelProvider {
668 pub fn openai_base_url() -> String {
669 "https://api.openai.com/v1".to_string()
670 }
671
672 pub fn vllora_url() -> String {
673 "http://localhost:9090/v1".to_string()
674 }
675
676 pub fn provider_id(&self) -> &'static str {
678 match self {
679 ModelProvider::OpenAI {} => "openai",
680 ModelProvider::OpenAICompatible { .. } => "openai_compat",
681 ModelProvider::Vllora { .. } => "vllora",
682 }
683 }
684
685 pub fn required_secret_keys(&self) -> Vec<&'static str> {
687 match self {
688 ModelProvider::OpenAI {} => vec!["OPENAI_API_KEY"],
689 ModelProvider::OpenAICompatible { api_key, .. } => {
690 if api_key.is_some() {
692 vec![]
693 } else {
694 vec!["OPENAI_API_KEY"]
695 }
696 }
697 ModelProvider::Vllora { .. } => vec![], }
699 }
700
701 pub fn all_provider_definitions() -> Vec<ProviderSecretDefinition> {
703 vec![
704 ProviderSecretDefinition {
705 id: "openai".to_string(),
706 label: "OpenAI".to_string(),
707 keys: vec![SecretKeyDefinition {
708 key: "OPENAI_API_KEY".to_string(),
709 label: "API key".to_string(),
710 placeholder: "sk-...".to_string(),
711 required: true,
712 }],
713 },
714 ProviderSecretDefinition {
715 id: "anthropic".to_string(),
716 label: "Anthropic".to_string(),
717 keys: vec![SecretKeyDefinition {
718 key: "ANTHROPIC_API_KEY".to_string(),
719 label: "API key".to_string(),
720 placeholder: "sk-ant-...".to_string(),
721 required: true,
722 }],
723 },
724 ProviderSecretDefinition {
725 id: "gemini".to_string(),
726 label: "Google Gemini".to_string(),
727 keys: vec![SecretKeyDefinition {
728 key: "GEMINI_API_KEY".to_string(),
729 label: "API key".to_string(),
730 placeholder: "AIza...".to_string(),
731 required: true,
732 }],
733 },
734 ProviderSecretDefinition {
735 id: "custom".to_string(),
736 label: "Custom".to_string(),
737 keys: vec![],
738 },
739 ]
740 }
741
742 pub fn display_name(&self) -> &'static str {
744 match self {
745 ModelProvider::OpenAI {} => "OpenAI",
746 ModelProvider::OpenAICompatible { .. } => "OpenAI Compatible",
747 ModelProvider::Vllora { .. } => "vLLORA",
748 }
749 }
750}
751
752#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
754#[serde(deny_unknown_fields)]
755pub struct ModelSettings {
756 #[serde(default = "default_model")]
757 pub model: String,
758 #[serde(default = "default_temperature")]
759 pub temperature: f32,
760 #[serde(default = "default_max_tokens")]
761 pub max_tokens: u32,
762 #[serde(default = "default_context_size")]
763 pub context_size: u32,
764 #[serde(default = "default_top_p")]
765 pub top_p: f32,
766 #[serde(default = "default_frequency_penalty")]
767 pub frequency_penalty: f32,
768 #[serde(default = "default_presence_penalty")]
769 pub presence_penalty: f32,
770 #[serde(default = "default_model_provider")]
771 pub provider: ModelProvider,
772 #[serde(default)]
774 pub parameters: Option<serde_json::Value>,
775 #[serde(default)]
777 pub response_format: Option<serde_json::Value>,
778}
779
780impl Default for ModelSettings {
781 fn default() -> Self {
782 Self {
783 model: "gpt-4.1-mini".to_string(),
784 temperature: 0.7,
785 max_tokens: 1000,
786 context_size: 20000,
787 top_p: 1.0,
788 frequency_penalty: 0.0,
789 presence_penalty: 0.0,
790 provider: default_model_provider(),
791 parameters: None,
792 response_format: None,
793 }
794 }
795}
796
797pub fn default_agent_version() -> Option<String> {
799 Some("0.2.2".to_string())
800}
801
802fn default_model_provider() -> ModelProvider {
803 ModelProvider::OpenAI {}
804}
805
806fn default_model() -> String {
807 "gpt-4.1-mini".to_string()
808}
809
810fn default_temperature() -> f32 {
811 0.7
812}
813
814fn default_max_tokens() -> u32 {
815 1000
816}
817
818fn default_context_size() -> u32 {
819 20000 }
821
822fn default_top_p() -> f32 {
823 1.0
824}
825
826fn default_frequency_penalty() -> f32 {
827 0.0
828}
829
830fn default_presence_penalty() -> f32 {
831 0.0
832}
833
834fn default_history_size() -> Option<usize> {
835 Some(5)
836}
837
838impl StandardDefinition {
839 pub fn validate(&self) -> anyhow::Result<()> {
840 if self.name.is_empty() {
842 return Err(anyhow::anyhow!("Agent name cannot be empty"));
843 }
844 Ok(())
845 }
846}
847
848impl From<StandardDefinition> for LlmDefinition {
849 fn from(definition: StandardDefinition) -> Self {
850 let mut model_settings = definition.model_settings.clone();
851 if let Some(context_size) = definition.context_size {
853 model_settings.context_size = context_size;
854 }
855
856 Self {
857 name: definition.name,
858 model_settings,
859 tool_format: definition.tool_format,
860 }
861 }
862}
863
864impl ToolsConfig {
865 pub fn builtin_only(tools: Vec<&str>) -> Self {
867 Self {
868 builtin: tools.into_iter().map(|s| s.to_string()).collect(),
869 packages: std::collections::HashMap::new(),
870 mcp: vec![],
871 external: None,
872 }
873 }
874
875 pub fn mcp_all(server: &str) -> Self {
877 Self {
878 builtin: vec![],
879 packages: std::collections::HashMap::new(),
880 mcp: vec![McpToolConfig {
881 server: server.to_string(),
882 include: vec!["*".to_string()],
883 exclude: vec![],
884 }],
885 external: None,
886 }
887 }
888
889 pub fn mcp_filtered(server: &str, include: Vec<&str>, exclude: Vec<&str>) -> Self {
891 Self {
892 builtin: vec![],
893 packages: std::collections::HashMap::new(),
894 mcp: vec![McpToolConfig {
895 server: server.to_string(),
896 include: include.into_iter().map(|s| s.to_string()).collect(),
897 exclude: exclude.into_iter().map(|s| s.to_string()).collect(),
898 }],
899 external: None,
900 }
901 }
902}
903
904pub async fn parse_agent_markdown_content(content: &str) -> Result<StandardDefinition, AgentError> {
905 let parts: Vec<&str> = content.split("---").collect();
907
908 if parts.len() < 3 {
909 return Err(AgentError::Validation(
910 "Invalid agent markdown format. Expected TOML frontmatter between --- markers"
911 .to_string(),
912 ));
913 }
914
915 let toml_content = parts[1].trim();
917 let mut agent_def: crate::StandardDefinition =
918 toml::from_str(toml_content).map_err(|e| AgentError::Validation(e.to_string()))?;
919
920 if let Err(validation_error) = validate_plugin_name(&agent_def.name) {
922 return Err(AgentError::Validation(format!(
923 "Invalid agent name '{}': {}",
924 agent_def.name, validation_error
925 )));
926 }
927
928 if !agent_def
930 .name
931 .chars()
932 .all(|c| c.is_alphanumeric() || c == '_')
933 || agent_def
934 .name
935 .chars()
936 .next()
937 .map_or(false, |c| c.is_numeric())
938 {
939 return Err(AgentError::Validation(format!(
940 "Invalid agent name '{}': Agent names must be valid JavaScript identifiers (alphanumeric + underscores, cannot start with number). \
941 Reason: Agent names become function names in TypeScript runtime.",
942 agent_def.name
943 )));
944 }
945
946 let instructions = parts[2..].join("---").trim().to_string();
948
949 agent_def.instructions = instructions;
951
952 Ok(agent_def)
953}
954
955pub fn validate_plugin_name(name: &str) -> Result<(), String> {
958 if name.contains('-') {
959 return Err(format!(
960 "Plugin name '{}' cannot contain hyphens. Use underscores instead.",
961 name
962 ));
963 }
964
965 if name.is_empty() {
966 return Err("Plugin name cannot be empty".to_string());
967 }
968
969 if let Some(first_char) = name.chars().next() {
971 if !first_char.is_ascii_alphabetic() && first_char != '_' {
972 return Err(format!(
973 "Plugin name '{}' must start with a letter or underscore",
974 name
975 ));
976 }
977 }
978
979 for ch in name.chars() {
981 if !ch.is_ascii_alphanumeric() && ch != '_' {
982 return Err(format!(
983 "Plugin name '{}' can only contain letters, numbers, and underscores",
984 name
985 ));
986 }
987 }
988
989 Ok(())
990}