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)]
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<BrowsrClientConfig> {
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}
621#[derive(Debug, Clone, Serialize, Deserialize)]
623pub struct ProviderSecretDefinition {
624 pub id: String,
626 pub label: String,
628 pub keys: Vec<SecretKeyDefinition>,
630}
631
632#[derive(Debug, Clone, Serialize, Deserialize)]
634pub struct SecretKeyDefinition {
635 pub key: String,
637 pub label: String,
639 pub placeholder: String,
641 #[serde(default = "default_required")]
643 pub required: bool,
644}
645
646fn default_required() -> bool {
647 true
648}
649
650impl ModelProvider {
651 pub fn openai_base_url() -> String {
652 "https://api.openai.com/v1".to_string()
653 }
654
655 pub fn vllora_url() -> String {
656 "http://localhost:9090/v1".to_string()
657 }
658
659 pub fn provider_id(&self) -> &'static str {
661 match self {
662 ModelProvider::OpenAI {} => "openai",
663 ModelProvider::OpenAICompatible { .. } => "openai_compat",
664 ModelProvider::Vllora { .. } => "vllora",
665 }
666 }
667
668 pub fn required_secret_keys(&self) -> Vec<&'static str> {
670 match self {
671 ModelProvider::OpenAI {} => vec!["OPENAI_API_KEY"],
672 ModelProvider::OpenAICompatible { api_key, .. } => {
673 if api_key.is_some() {
675 vec![]
676 } else {
677 vec!["OPENAI_API_KEY"]
678 }
679 }
680 ModelProvider::Vllora { .. } => vec![], }
682 }
683
684 pub fn all_provider_definitions() -> Vec<ProviderSecretDefinition> {
686 vec![
687 ProviderSecretDefinition {
688 id: "openai".to_string(),
689 label: "OpenAI".to_string(),
690 keys: vec![SecretKeyDefinition {
691 key: "OPENAI_API_KEY".to_string(),
692 label: "API key".to_string(),
693 placeholder: "sk-...".to_string(),
694 required: true,
695 }],
696 },
697 ProviderSecretDefinition {
698 id: "anthropic".to_string(),
699 label: "Anthropic".to_string(),
700 keys: vec![SecretKeyDefinition {
701 key: "ANTHROPIC_API_KEY".to_string(),
702 label: "API key".to_string(),
703 placeholder: "sk-ant-...".to_string(),
704 required: true,
705 }],
706 },
707 ProviderSecretDefinition {
708 id: "gemini".to_string(),
709 label: "Google Gemini".to_string(),
710 keys: vec![SecretKeyDefinition {
711 key: "GEMINI_API_KEY".to_string(),
712 label: "API key".to_string(),
713 placeholder: "AIza...".to_string(),
714 required: true,
715 }],
716 },
717 ProviderSecretDefinition {
718 id: "custom".to_string(),
719 label: "Custom".to_string(),
720 keys: vec![],
721 },
722 ]
723 }
724
725 pub fn display_name(&self) -> &'static str {
727 match self {
728 ModelProvider::OpenAI {} => "OpenAI",
729 ModelProvider::OpenAICompatible { .. } => "OpenAI Compatible",
730 ModelProvider::Vllora { .. } => "vLLORA",
731 }
732 }
733}
734
735#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
737#[serde(deny_unknown_fields)]
738pub struct ModelSettings {
739 #[serde(default = "default_model")]
740 pub model: String,
741 #[serde(default = "default_temperature")]
742 pub temperature: f32,
743 #[serde(default = "default_max_tokens")]
744 pub max_tokens: u32,
745 #[serde(default = "default_context_size")]
746 pub context_size: u32,
747 #[serde(default = "default_top_p")]
748 pub top_p: f32,
749 #[serde(default = "default_frequency_penalty")]
750 pub frequency_penalty: f32,
751 #[serde(default = "default_presence_penalty")]
752 pub presence_penalty: f32,
753 #[serde(default = "default_model_provider")]
754 pub provider: ModelProvider,
755 #[serde(default)]
757 pub parameters: Option<serde_json::Value>,
758 #[serde(default)]
760 pub response_format: Option<serde_json::Value>,
761}
762
763impl Default for ModelSettings {
764 fn default() -> Self {
765 Self {
766 model: "gpt-4.1-mini".to_string(),
767 temperature: 0.7,
768 max_tokens: 1000,
769 context_size: 20000,
770 top_p: 1.0,
771 frequency_penalty: 0.0,
772 presence_penalty: 0.0,
773 provider: default_model_provider(),
774 parameters: None,
775 response_format: None,
776 }
777 }
778}
779
780pub fn default_agent_version() -> Option<String> {
782 Some("0.2.2".to_string())
783}
784
785fn default_model_provider() -> ModelProvider {
786 ModelProvider::OpenAI {}
787}
788
789fn default_model() -> String {
790 "gpt-4.1-mini".to_string()
791}
792
793fn default_temperature() -> f32 {
794 0.7
795}
796
797fn default_max_tokens() -> u32 {
798 1000
799}
800
801fn default_context_size() -> u32 {
802 20000 }
804
805fn default_top_p() -> f32 {
806 1.0
807}
808
809fn default_frequency_penalty() -> f32 {
810 0.0
811}
812
813fn default_presence_penalty() -> f32 {
814 0.0
815}
816
817fn default_history_size() -> Option<usize> {
818 Some(5)
819}
820
821impl StandardDefinition {
822 pub fn validate(&self) -> anyhow::Result<()> {
823 if self.name.is_empty() {
825 return Err(anyhow::anyhow!("Agent name cannot be empty"));
826 }
827 Ok(())
828 }
829}
830
831impl From<StandardDefinition> for LlmDefinition {
832 fn from(definition: StandardDefinition) -> Self {
833 let mut model_settings = definition.model_settings.clone();
834 if let Some(context_size) = definition.context_size {
836 model_settings.context_size = context_size;
837 }
838
839 Self {
840 name: definition.name,
841 model_settings,
842 tool_format: definition.tool_format,
843 }
844 }
845}
846
847impl ToolsConfig {
848 pub fn builtin_only(tools: Vec<&str>) -> Self {
850 Self {
851 builtin: tools.into_iter().map(|s| s.to_string()).collect(),
852 packages: std::collections::HashMap::new(),
853 mcp: vec![],
854 external: None,
855 }
856 }
857
858 pub fn mcp_all(server: &str) -> Self {
860 Self {
861 builtin: vec![],
862 packages: std::collections::HashMap::new(),
863 mcp: vec![McpToolConfig {
864 server: server.to_string(),
865 include: vec!["*".to_string()],
866 exclude: vec![],
867 }],
868 external: None,
869 }
870 }
871
872 pub fn mcp_filtered(server: &str, include: Vec<&str>, exclude: Vec<&str>) -> Self {
874 Self {
875 builtin: vec![],
876 packages: std::collections::HashMap::new(),
877 mcp: vec![McpToolConfig {
878 server: server.to_string(),
879 include: include.into_iter().map(|s| s.to_string()).collect(),
880 exclude: exclude.into_iter().map(|s| s.to_string()).collect(),
881 }],
882 external: None,
883 }
884 }
885}
886
887pub async fn parse_agent_markdown_content(content: &str) -> Result<StandardDefinition, AgentError> {
888 let parts: Vec<&str> = content.split("---").collect();
890
891 if parts.len() < 3 {
892 return Err(AgentError::Validation(
893 "Invalid agent markdown format. Expected TOML frontmatter between --- markers"
894 .to_string(),
895 ));
896 }
897
898 let toml_content = parts[1].trim();
900 let mut agent_def: crate::StandardDefinition =
901 toml::from_str(toml_content).map_err(|e| AgentError::Validation(e.to_string()))?;
902
903 if let Err(validation_error) = validate_plugin_name(&agent_def.name) {
905 return Err(AgentError::Validation(format!(
906 "Invalid agent name '{}': {}",
907 agent_def.name, validation_error
908 )));
909 }
910
911 if !agent_def
913 .name
914 .chars()
915 .all(|c| c.is_alphanumeric() || c == '_')
916 || agent_def
917 .name
918 .chars()
919 .next()
920 .map_or(false, |c| c.is_numeric())
921 {
922 return Err(AgentError::Validation(format!(
923 "Invalid agent name '{}': Agent names must be valid JavaScript identifiers (alphanumeric + underscores, cannot start with number). \
924 Reason: Agent names become function names in TypeScript runtime.",
925 agent_def.name
926 )));
927 }
928
929 let instructions = parts[2..].join("---").trim().to_string();
931
932 agent_def.instructions = instructions;
934
935 Ok(agent_def)
936}
937
938pub fn validate_plugin_name(name: &str) -> Result<(), String> {
941 if name.contains('-') {
942 return Err(format!(
943 "Plugin name '{}' cannot contain hyphens. Use underscores instead.",
944 name
945 ));
946 }
947
948 if name.is_empty() {
949 return Err("Plugin name cannot be empty".to_string());
950 }
951
952 if let Some(first_char) = name.chars().next() {
954 if !first_char.is_ascii_alphabetic() && first_char != '_' {
955 return Err(format!(
956 "Plugin name '{}' must start with a letter or underscore",
957 name
958 ));
959 }
960 }
961
962 for ch in name.chars() {
964 if !ch.is_ascii_alphanumeric() && ch != '_' {
965 return Err(format!(
966 "Plugin name '{}' can only contain letters, numbers, and underscores",
967 name
968 ));
969 }
970 }
971
972 Ok(())
973}