1use std::io;
7use std::sync::Arc;
8
9use tokio::runtime::Runtime;
10use tokio::sync::mpsc;
11use tokio_util::sync::CancellationToken;
12
13use crate::controller::{
14 ControllerEvent, ControllerInputPayload, Executable, LLMController, LLMSessionConfig,
15 LLMTool, ListSkillsTool, PermissionRegistry, ToolRegistry, UserInteractionRegistry,
16};
17use crate::skills::{SkillDiscovery, SkillDiscoveryError, SkillRegistry, SkillReloadResult};
18
19use super::config::{load_config, AgentConfig, LLMRegistry};
20use super::error::AgentError;
21use super::logger::Logger;
22use super::messages::channels::DEFAULT_CHANNEL_SIZE;
23use super::messages::UiMessage;
24use super::router::InputRouter;
25
26pub type ToControllerTx = mpsc::Sender<ControllerInputPayload>;
28pub type ToControllerRx = mpsc::Receiver<ControllerInputPayload>;
30pub type FromControllerTx = mpsc::Sender<UiMessage>;
32pub type FromControllerRx = mpsc::Receiver<UiMessage>;
34
35pub struct AgentCore {
78 #[allow(dead_code)]
81 logger: Logger,
82
83 name: String,
85
86 version: String,
88
89 runtime: Runtime,
91
92 controller: Arc<LLMController>,
94
95 llm_registry: Option<LLMRegistry>,
97
98 to_controller_tx: ToControllerTx,
100
101 to_controller_rx: Option<ToControllerRx>,
103
104 from_controller_tx: FromControllerTx,
106
107 from_controller_rx: Option<FromControllerRx>,
109
110 cancel_token: CancellationToken,
112
113 user_interaction_registry: Arc<UserInteractionRegistry>,
115
116 permission_registry: Arc<PermissionRegistry>,
118
119 tool_definitions: Vec<LLMTool>,
121
122 error_no_session: Option<String>,
124
125 skill_registry: Arc<SkillRegistry>,
127
128 skill_discovery: SkillDiscovery,
130}
131
132impl AgentCore {
133 pub fn new<C: AgentConfig>(config: &C) -> io::Result<Self> {
143 let logger = Logger::new(config.log_prefix())?;
144 tracing::info!("{} agent initialized", config.name());
145
146 let llm_registry = load_config(config);
148 if llm_registry.is_empty() {
149 tracing::warn!(
150 "No LLM providers configured. Set ANTHROPIC_API_KEY or create ~/{}",
151 config.config_path()
152 );
153 } else {
154 tracing::info!(
155 "Loaded {} LLM provider(s): {:?}",
156 llm_registry.providers().len(),
157 llm_registry.providers()
158 );
159 }
160
161 let runtime = Runtime::new().map_err(|e| {
163 io::Error::new(
164 io::ErrorKind::Other,
165 format!("Failed to create runtime: {}", e),
166 )
167 })?;
168
169 let channel_size = config.channel_buffer_size().unwrap_or(DEFAULT_CHANNEL_SIZE);
171 tracing::debug!("Using channel buffer size: {}", channel_size);
172
173 let (to_controller_tx, to_controller_rx) =
175 mpsc::channel::<ControllerInputPayload>(channel_size);
176 let (from_controller_tx, from_controller_rx) =
177 mpsc::channel::<UiMessage>(channel_size);
178
179 let (interaction_event_tx, mut interaction_event_rx) =
181 mpsc::channel::<ControllerEvent>(channel_size);
182
183 let user_interaction_registry =
185 Arc::new(UserInteractionRegistry::new(interaction_event_tx));
186
187 let ui_tx_for_interactions = from_controller_tx.clone();
190 runtime.spawn(async move {
191 while let Some(event) = interaction_event_rx.recv().await {
192 let msg = convert_controller_event_to_ui_message(event);
193 if let Err(e) = ui_tx_for_interactions.send(msg).await {
194 tracing::warn!("Failed to send user interaction event to UI: {}", e);
195 }
196 }
197 });
198
199 let (permission_event_tx, mut permission_event_rx) =
201 mpsc::channel::<ControllerEvent>(channel_size);
202
203 let permission_registry = Arc::new(PermissionRegistry::new(permission_event_tx));
205
206 let ui_tx_for_permissions = from_controller_tx.clone();
209 runtime.spawn(async move {
210 while let Some(event) = permission_event_rx.recv().await {
211 let msg = convert_controller_event_to_ui_message(event);
212 if let Err(e) = ui_tx_for_permissions.send(msg).await {
213 tracing::warn!("Failed to send permission event to UI: {}", e);
214 }
215 }
216 });
217
218 let controller = Arc::new(LLMController::new(
223 permission_registry.clone(),
224 Some(from_controller_tx.clone()),
225 Some(channel_size),
226 ));
227 let cancel_token = CancellationToken::new();
228
229 Ok(Self {
230 logger,
231 name: config.name().to_string(),
232 version: "0.1.0".to_string(),
233 runtime,
234 controller,
235 llm_registry: Some(llm_registry),
236 to_controller_tx,
237 to_controller_rx: Some(to_controller_rx),
238 from_controller_tx,
239 from_controller_rx: Some(from_controller_rx),
240 cancel_token,
241 user_interaction_registry,
242 permission_registry,
243 tool_definitions: Vec::new(),
244 error_no_session: None,
245 skill_registry: Arc::new(SkillRegistry::new()),
246 skill_discovery: SkillDiscovery::new(),
247 })
248 }
249
250 pub fn with_config(
271 name: impl Into<String>,
272 config_path: impl Into<String>,
273 system_prompt: impl Into<String>,
274 ) -> io::Result<Self> {
275 let config = super::config::SimpleConfig::new(name, config_path, system_prompt);
276 Self::new(&config)
277 }
278
279 pub fn set_error_no_session(&mut self, message: impl Into<String>) -> &mut Self {
289 self.error_no_session = Some(message.into());
290 self
291 }
292
293 pub fn error_no_session(&self) -> Option<&str> {
295 self.error_no_session.as_deref()
296 }
297
298 pub fn set_version(&mut self, version: impl Into<String>) {
300 self.version = version.into();
301 }
302
303 pub fn version(&self) -> &str {
305 &self.version
306 }
307
308 pub fn load_environment_context(&mut self) -> &mut Self {
326 if let Some(registry) = self.llm_registry.take() {
327 self.llm_registry = Some(registry.with_environment_context());
328 tracing::info!("Environment context loaded into system prompt");
329 }
330 self
331 }
332
333 pub fn register_tools<F>(&mut self, f: F) -> Result<(), AgentError>
346 where
347 F: FnOnce(
348 &Arc<ToolRegistry>,
349 &Arc<UserInteractionRegistry>,
350 &Arc<PermissionRegistry>,
351 ) -> Result<Vec<LLMTool>, String>,
352 {
353 let tool_defs = f(
354 self.controller.tool_registry(),
355 &self.user_interaction_registry,
356 &self.permission_registry,
357 )
358 .map_err(AgentError::ToolRegistration)?;
359 self.tool_definitions = tool_defs;
360 Ok(())
361 }
362
363 pub fn register_tools_async<F, Fut>(&mut self, f: F) -> Result<(), AgentError>
376 where
377 F: FnOnce(Arc<ToolRegistry>, Arc<UserInteractionRegistry>, Arc<PermissionRegistry>) -> Fut,
378 Fut: std::future::Future<Output = Result<Vec<LLMTool>, String>>,
379 {
380 let tool_defs = self.runtime.block_on(f(
381 self.controller.tool_registry().clone(),
382 self.user_interaction_registry.clone(),
383 self.permission_registry.clone(),
384 ))
385 .map_err(AgentError::ToolRegistration)?;
386 self.tool_definitions = tool_defs;
387 Ok(())
388 }
389
390 pub fn start_background_tasks(&mut self) {
395 tracing::info!("{} starting background tasks", self.name);
396
397 let controller = self.controller.clone();
399 self.runtime.spawn(async move {
400 controller.start().await;
401 });
402 tracing::info!("Controller started");
403
404 if let Some(to_controller_rx) = self.to_controller_rx.take() {
406 let router = InputRouter::new(
407 self.controller.clone(),
408 to_controller_rx,
409 self.cancel_token.clone(),
410 );
411 self.runtime.spawn(async move {
412 router.run().await;
413 });
414 tracing::info!("InputRouter started");
415 }
416 }
417
418 async fn create_session_internal(
420 controller: &Arc<LLMController>,
421 mut config: LLMSessionConfig,
422 tools: &[LLMTool],
423 skill_registry: &Arc<SkillRegistry>,
424 ) -> Result<i64, crate::client::error::LlmError> {
425 let skills_xml = skill_registry.to_prompt_xml();
427 if !skills_xml.is_empty() {
428 config.system_prompt = Some(match config.system_prompt {
429 Some(prompt) => format!("{}\n\n{}", prompt, skills_xml),
430 None => skills_xml,
431 });
432 }
433
434 let id = controller.create_session(config).await?;
435
436 if !tools.is_empty() {
438 if let Some(session) = controller.get_session(id).await {
439 session.set_tools(tools.to_vec()).await;
440 }
441 }
442
443 Ok(id)
444 }
445
446 pub fn create_initial_session(&mut self) -> Result<(i64, String, i32), AgentError> {
450 let registry = self.llm_registry.as_ref().ok_or_else(|| {
451 AgentError::NoConfiguration("No LLM registry available".to_string())
452 })?;
453
454 let config = registry.get_default().ok_or_else(|| {
455 AgentError::NoConfiguration("No default LLM provider configured".to_string())
456 })?;
457
458 let model = config.model.clone();
459 let context_limit = config.context_limit;
460
461 let controller = self.controller.clone();
462 let tool_definitions = self.tool_definitions.clone();
463 let skill_registry = self.skill_registry.clone();
464
465 let session_id = self.runtime.block_on(Self::create_session_internal(
466 &controller,
467 config.clone(),
468 &tool_definitions,
469 &skill_registry,
470 ))?;
471
472 tracing::info!(
473 session_id = session_id,
474 model = %model,
475 "Created initial session"
476 );
477
478 Ok((session_id, model, context_limit))
479 }
480
481 pub fn create_session(&self, config: LLMSessionConfig) -> Result<i64, AgentError> {
485 let controller = self.controller.clone();
486 let tool_definitions = self.tool_definitions.clone();
487 let skill_registry = self.skill_registry.clone();
488
489 self.runtime
490 .block_on(Self::create_session_internal(
491 &controller,
492 config,
493 &tool_definitions,
494 &skill_registry,
495 ))
496 .map_err(AgentError::from)
497 }
498
499 pub fn shutdown(&self) {
501 tracing::info!("{} shutting down", self.name);
502 self.cancel_token.cancel();
503
504 let controller = self.controller.clone();
505 self.runtime.block_on(async move {
506 controller.shutdown().await;
507 });
508
509 tracing::info!("{} shutdown complete", self.name);
510 }
511
512 pub fn run_with_frontend<E, I, P>(
553 &mut self,
554 event_sink: E,
555 mut input_source: I,
556 permission_policy: P,
557 ) -> io::Result<()>
558 where
559 E: super::interface::EventSink,
560 I: super::interface::InputSource,
561 P: super::interface::PermissionPolicy,
562 {
563 use std::sync::Arc;
564 use super::interface::PolicyDecision;
565 use crate::permissions::{BatchPermissionResponse, PermissionPanelResponse};
566
567 tracing::info!("{} starting with custom frontend", self.name);
568
569 let sink = Arc::new(event_sink);
571 let policy = Arc::new(permission_policy);
572
573 let controller = self.controller.clone();
576 self.runtime.spawn(async move {
577 controller.start().await;
578 });
579 tracing::info!("Controller started");
580
581 if let Some(mut from_controller_rx) = self.from_controller_rx.take() {
584 let sink_clone = sink.clone();
585 let policy_clone = policy.clone();
586 let permission_registry = self.permission_registry.clone();
587 let user_interaction_registry = self.user_interaction_registry.clone();
588
589 self.runtime.spawn(async move {
590 while let Some(event) = from_controller_rx.recv().await {
591 match &event {
593 UiMessage::PermissionRequired { tool_use_id, request, .. } => {
594 match policy_clone.decide(request) {
595 PolicyDecision::AskUser => {
596 }
598 decision => {
599 let response = match decision {
600 PolicyDecision::Allow => PermissionPanelResponse {
601 granted: true,
602 grant: None,
603 message: None,
604 },
605 PolicyDecision::AllowWithGrant(grant) => PermissionPanelResponse {
606 granted: true,
607 grant: Some(grant),
608 message: None,
609 },
610 PolicyDecision::Deny { reason } => PermissionPanelResponse {
611 granted: false,
612 grant: None,
613 message: reason,
614 },
615 PolicyDecision::AskUser => unreachable!(),
616 };
617 if let Err(e) = permission_registry
618 .respond_to_request(tool_use_id, response)
619 .await
620 {
621 tracing::warn!("Failed to respond to permission request: {}", e);
622 }
623 continue; }
625 }
626 }
627 UiMessage::BatchPermissionRequired { batch, .. } => {
628 let mut all_handled = true;
630 let mut approved_grants = Vec::new();
631 let mut denied_ids = Vec::new();
632
633 for request in &batch.requests {
634 match policy_clone.decide(request) {
635 PolicyDecision::Allow => {
636 }
638 PolicyDecision::AllowWithGrant(grant) => {
639 approved_grants.push(grant);
640 }
641 PolicyDecision::Deny { .. } => {
642 denied_ids.push(request.id.clone());
643 }
644 PolicyDecision::AskUser => {
645 all_handled = false;
646 break;
647 }
648 }
649 }
650
651 if all_handled {
652 let response = if denied_ids.is_empty() {
654 BatchPermissionResponse::all_granted(&batch.batch_id, approved_grants)
655 } else {
656 BatchPermissionResponse::all_denied(&batch.batch_id, denied_ids)
657 };
658 if let Err(e) = permission_registry
659 .respond_to_batch(&batch.batch_id, response)
660 .await
661 {
662 tracing::warn!("Failed to respond to batch permission request: {}", e);
663 }
664 continue; }
666 }
668 UiMessage::UserInteractionRequired { tool_use_id, .. } => {
669 if !policy_clone.supports_interaction() {
670 if let Err(e) = user_interaction_registry.cancel(tool_use_id).await {
672 tracing::warn!("Failed to cancel user interaction: {}", e);
673 }
674 tracing::debug!("Auto-cancelled user interaction in headless mode");
675 continue; }
677 }
679 _ => {}
680 }
681
682 if let Err(e) = sink_clone.send(event) {
684 tracing::warn!("Failed to send event to sink: {}", e);
685 }
686 }
687 });
688 }
689
690 match self.create_initial_session() {
692 Ok((session_id, model, _)) => {
693 tracing::info!(session_id, model = %model, "Created initial session");
694 }
695 Err(e) => {
696 tracing::warn!(error = %e, "No initial session created");
697 }
698 }
699
700 let to_controller_tx = self.to_controller_tx.clone();
702 self.runtime.block_on(async {
703 while let Some(input) = input_source.recv().await {
704 if let Err(e) = to_controller_tx.send(input).await {
705 tracing::error!(error = %e, "Failed to send input to controller");
706 break;
707 }
708 }
709 });
710
711 self.shutdown();
713 tracing::info!("{} stopped", self.name);
714
715 Ok(())
716 }
717
718 pub fn to_controller_tx(&self) -> ToControllerTx {
722 self.to_controller_tx.clone()
723 }
724
725 pub fn take_from_controller_rx(&mut self) -> Option<FromControllerRx> {
727 self.from_controller_rx.take()
728 }
729
730 pub fn controller(&self) -> &Arc<LLMController> {
732 &self.controller
733 }
734
735 pub fn runtime(&self) -> &Runtime {
737 &self.runtime
738 }
739
740 pub fn runtime_handle(&self) -> tokio::runtime::Handle {
742 self.runtime.handle().clone()
743 }
744
745 pub fn user_interaction_registry(&self) -> &Arc<UserInteractionRegistry> {
747 &self.user_interaction_registry
748 }
749
750 pub fn permission_registry(&self) -> &Arc<PermissionRegistry> {
752 &self.permission_registry
753 }
754
755 pub async fn remove_session(&self, session_id: i64) -> bool {
769 let removed = self.controller.remove_session(session_id).await;
771
772 self.permission_registry.cancel_session(session_id).await;
774
775 self.user_interaction_registry.cancel_session(session_id).await;
777
778 self.controller.tool_registry().cleanup_session(session_id).await;
780
781 if removed {
782 tracing::info!(session_id, "Session removed with full cleanup");
783 }
784
785 removed
786 }
787
788 pub fn llm_registry(&self) -> Option<&LLMRegistry> {
790 self.llm_registry.as_ref()
791 }
792
793 pub fn take_llm_registry(&mut self) -> Option<LLMRegistry> {
795 self.llm_registry.take()
796 }
797
798 pub fn cancel_token(&self) -> CancellationToken {
800 self.cancel_token.clone()
801 }
802
803 pub fn name(&self) -> &str {
805 &self.name
806 }
807
808 pub fn from_controller_tx(&self) -> FromControllerTx {
812 self.from_controller_tx.clone()
813 }
814
815 pub fn tool_definitions(&self) -> &[LLMTool] {
817 &self.tool_definitions
818 }
819
820 pub fn skill_registry(&self) -> &Arc<SkillRegistry> {
824 &self.skill_registry
825 }
826
827 pub fn register_list_skills_tool(&mut self) -> Result<LLMTool, AgentError> {
835 let tool = ListSkillsTool::new(self.skill_registry.clone());
836 let llm_tool = tool.to_llm_tool();
837
838 self.runtime.block_on(async {
839 self.controller
840 .tool_registry()
841 .register(Arc::new(tool))
842 .await
843 }).map_err(|e| AgentError::ToolRegistration(e.to_string()))?;
844
845 self.tool_definitions.push(llm_tool.clone());
846 tracing::info!("Registered list_skills tool");
847
848 Ok(llm_tool)
849 }
850
851 pub fn add_skill_path(&mut self, path: std::path::PathBuf) -> &mut Self {
856 self.skill_discovery.add_path(path);
857 self
858 }
859
860 pub fn load_skills(&mut self) -> (usize, Vec<SkillDiscoveryError>) {
867 let results = self.skill_discovery.discover();
868 self.register_discovered_skills(results)
869 }
870
871 pub fn load_skills_from(&self, paths: Vec<std::path::PathBuf>) -> (usize, Vec<SkillDiscoveryError>) {
880 let mut discovery = SkillDiscovery::empty();
881 for path in paths {
882 discovery.add_path(path);
883 }
884
885 let results = discovery.discover();
886 self.register_discovered_skills(results)
887 }
888
889 fn register_discovered_skills(
893 &self,
894 results: Vec<Result<crate::skills::Skill, SkillDiscoveryError>>,
895 ) -> (usize, Vec<SkillDiscoveryError>) {
896 let mut errors = Vec::new();
897 let mut count = 0;
898
899 for result in results {
900 match result {
901 Ok(skill) => {
902 let skill_name = skill.metadata.name.clone();
903 let skill_path = skill.path.clone();
904 let replaced = self.skill_registry.register(skill);
905
906 if let Some(old_skill) = replaced {
907 tracing::warn!(
908 skill_name = %skill_name,
909 new_path = %skill_path.display(),
910 old_path = %old_skill.path.display(),
911 "Duplicate skill name detected - replaced existing skill"
912 );
913 }
914
915 tracing::info!(
916 skill_name = %skill_name,
917 skill_path = %skill_path.display(),
918 "Loaded skill"
919 );
920 count += 1;
921 }
922 Err(e) => {
923 tracing::warn!(
924 path = %e.path.display(),
925 error = %e.message,
926 "Failed to load skill"
927 );
928 errors.push(e);
929 }
930 }
931 }
932
933 tracing::info!("Loaded {} skill(s)", count);
934 (count, errors)
935 }
936
937 pub fn reload_skills(&mut self) -> SkillReloadResult {
946 let current_names: std::collections::HashSet<String> =
947 self.skill_registry.names().into_iter().collect();
948
949 let results = self.skill_discovery.discover();
950 let mut discovered_names = std::collections::HashSet::new();
951 let mut result = SkillReloadResult::default();
952
953 for discovery_result in results {
955 match discovery_result {
956 Ok(skill) => {
957 let name = skill.metadata.name.clone();
958 discovered_names.insert(name.clone());
959
960 if !current_names.contains(&name) {
961 tracing::info!(skill_name = %name, "Added new skill");
962 result.added.push(name);
963 }
964 self.skill_registry.register(skill);
965 }
966 Err(e) => {
967 tracing::warn!(
968 path = %e.path.display(),
969 error = %e.message,
970 "Failed to load skill during reload"
971 );
972 result.errors.push(e);
973 }
974 }
975 }
976
977 for name in ¤t_names {
979 if !discovered_names.contains(name) {
980 tracing::info!(skill_name = %name, "Removed skill");
981 self.skill_registry.unregister(name);
982 result.removed.push(name.clone());
983 }
984 }
985
986 tracing::info!(
987 added = result.added.len(),
988 removed = result.removed.len(),
989 errors = result.errors.len(),
990 "Skills reloaded"
991 );
992
993 result
994 }
995
996 pub fn skills_prompt_xml(&self) -> String {
1001 self.skill_registry.to_prompt_xml()
1002 }
1003
1004 pub async fn refresh_session_skills(&self, session_id: i64) -> Result<(), AgentError> {
1012 let skills_xml = self.skills_prompt_xml();
1013 if skills_xml.is_empty() {
1014 return Ok(());
1015 }
1016
1017 let session = self
1018 .controller
1019 .get_session(session_id)
1020 .await
1021 .ok_or_else(|| AgentError::SessionNotFound(session_id))?;
1022
1023 let current_prompt = session.system_prompt().await.unwrap_or_default();
1024
1025 let new_prompt = if current_prompt.contains("<available_skills>") {
1027 replace_skills_section(¤t_prompt, &skills_xml)
1029 } else if current_prompt.is_empty() {
1030 skills_xml
1032 } else {
1033 format!("{}\n\n{}", current_prompt, skills_xml)
1035 };
1036
1037 session.set_system_prompt(new_prompt).await;
1038 tracing::debug!(session_id, "Refreshed session skills");
1039 Ok(())
1040 }
1041}
1042
1043fn replace_skills_section(prompt: &str, new_skills_xml: &str) -> String {
1045 if let Some(start) = prompt.find("<available_skills>") {
1046 if let Some(end) = prompt.find("</available_skills>") {
1047 let end = end + "</available_skills>".len();
1048 let mut result = String::with_capacity(prompt.len());
1049 result.push_str(&prompt[..start]);
1050 result.push_str(new_skills_xml);
1051 result.push_str(&prompt[end..]);
1052 return result;
1053 }
1054 }
1055 format!("{}\n\n{}", prompt, new_skills_xml)
1057}
1058
1059pub fn convert_controller_event_to_ui_message(event: ControllerEvent) -> UiMessage {
1076 match event {
1077 ControllerEvent::StreamStart { session_id, .. } => {
1078 UiMessage::System {
1080 session_id,
1081 message: String::new(),
1082 }
1083 }
1084 ControllerEvent::TextChunk {
1085 session_id,
1086 text,
1087 turn_id,
1088 } => UiMessage::TextChunk {
1089 session_id,
1090 turn_id,
1091 text,
1092 input_tokens: 0,
1093 output_tokens: 0,
1094 },
1095 ControllerEvent::ToolUseStart {
1096 session_id,
1097 tool_name,
1098 turn_id,
1099 ..
1100 } => UiMessage::Display {
1101 session_id,
1102 turn_id,
1103 message: format!("Executing tool: {}", tool_name),
1104 },
1105 ControllerEvent::ToolUse {
1106 session_id,
1107 tool,
1108 display_name,
1109 display_title,
1110 turn_id,
1111 } => UiMessage::ToolExecuting {
1112 session_id,
1113 turn_id,
1114 tool_use_id: tool.id.clone(),
1115 display_name: display_name.unwrap_or_else(|| tool.name.clone()),
1116 display_title: display_title.unwrap_or_default(),
1117 },
1118 ControllerEvent::Complete {
1119 session_id,
1120 turn_id,
1121 stop_reason,
1122 } => UiMessage::Complete {
1123 session_id,
1124 turn_id,
1125 input_tokens: 0,
1126 output_tokens: 0,
1127 stop_reason,
1128 },
1129 ControllerEvent::Error {
1130 session_id,
1131 error,
1132 turn_id,
1133 } => UiMessage::Error {
1134 session_id,
1135 turn_id,
1136 error,
1137 },
1138 ControllerEvent::TokenUpdate {
1139 session_id,
1140 input_tokens,
1141 output_tokens,
1142 context_limit,
1143 } => UiMessage::TokenUpdate {
1144 session_id,
1145 turn_id: None,
1146 input_tokens,
1147 output_tokens,
1148 context_limit,
1149 },
1150 ControllerEvent::ToolResult {
1151 session_id,
1152 tool_use_id,
1153 status,
1154 error,
1155 turn_id,
1156 ..
1157 } => UiMessage::ToolCompleted {
1158 session_id,
1159 turn_id,
1160 tool_use_id,
1161 status,
1162 error,
1163 },
1164 ControllerEvent::CommandComplete {
1165 session_id,
1166 command,
1167 success,
1168 message,
1169 } => UiMessage::CommandComplete {
1170 session_id,
1171 command,
1172 success,
1173 message,
1174 },
1175 ControllerEvent::UserInteractionRequired {
1176 session_id,
1177 tool_use_id,
1178 request,
1179 turn_id,
1180 } => UiMessage::UserInteractionRequired {
1181 session_id,
1182 tool_use_id,
1183 request,
1184 turn_id,
1185 },
1186 ControllerEvent::PermissionRequired {
1187 session_id,
1188 tool_use_id,
1189 request,
1190 turn_id,
1191 } => UiMessage::PermissionRequired {
1192 session_id,
1193 tool_use_id,
1194 request,
1195 turn_id,
1196 },
1197 ControllerEvent::BatchPermissionRequired {
1198 session_id,
1199 batch,
1200 turn_id,
1201 } => UiMessage::BatchPermissionRequired {
1202 session_id,
1203 batch,
1204 turn_id,
1205 },
1206 }
1207}
1208
1209#[cfg(test)]
1210mod tests {
1211 use super::*;
1212 use crate::controller::TurnId;
1213
1214 #[test]
1215 fn test_convert_text_chunk_event() {
1216 let event = ControllerEvent::TextChunk {
1217 session_id: 1,
1218 text: "Hello".to_string(),
1219 turn_id: Some(TurnId::new_user_turn(1)),
1220 };
1221
1222 let msg = convert_controller_event_to_ui_message(event);
1223
1224 match msg {
1225 UiMessage::TextChunk {
1226 session_id, text, ..
1227 } => {
1228 assert_eq!(session_id, 1);
1229 assert_eq!(text, "Hello");
1230 }
1231 _ => panic!("Expected TextChunk message"),
1232 }
1233 }
1234
1235 #[test]
1236 fn test_convert_error_event() {
1237 let event = ControllerEvent::Error {
1238 session_id: 1,
1239 error: "Test error".to_string(),
1240 turn_id: None,
1241 };
1242
1243 let msg = convert_controller_event_to_ui_message(event);
1244
1245 match msg {
1246 UiMessage::Error {
1247 session_id, error, ..
1248 } => {
1249 assert_eq!(session_id, 1);
1250 assert_eq!(error, "Test error");
1251 }
1252 _ => panic!("Expected Error message"),
1253 }
1254 }
1255
1256 #[test]
1257 fn test_replace_skills_section_replaces_existing() {
1258 let prompt = "System prompt.\n\n<available_skills>\n <skill>old</skill>\n</available_skills>\n\nMore text.";
1259 let new_xml = "<available_skills>\n <skill>new</skill>\n</available_skills>";
1260
1261 let result = replace_skills_section(prompt, new_xml);
1262
1263 assert!(result.contains("<skill>new</skill>"));
1264 assert!(!result.contains("<skill>old</skill>"));
1265 assert!(result.contains("System prompt."));
1266 assert!(result.contains("More text."));
1267 }
1268
1269 #[test]
1270 fn test_replace_skills_section_no_existing() {
1271 let prompt = "System prompt without skills.";
1272 let new_xml = "<available_skills>\n <skill>new</skill>\n</available_skills>";
1273
1274 let result = replace_skills_section(prompt, new_xml);
1275
1276 assert!(result.contains("System prompt without skills."));
1278 assert!(result.contains("<skill>new</skill>"));
1279 }
1280
1281 #[test]
1282 fn test_replace_skills_section_malformed_no_closing_tag() {
1283 let prompt = "System prompt.\n\n<available_skills>\n <skill>old</skill>\n\nNo closing tag.";
1284 let new_xml = "<available_skills>\n <skill>new</skill>\n</available_skills>";
1285
1286 let result = replace_skills_section(prompt, new_xml);
1287
1288 assert!(result.contains("<skill>old</skill>"));
1290 assert!(result.contains("<skill>new</skill>"));
1291 }
1292
1293 #[test]
1294 fn test_replace_skills_section_at_end() {
1295 let prompt = "System prompt.\n\n<available_skills>\n <skill>old</skill>\n</available_skills>";
1296 let new_xml = "<available_skills>\n <skill>new</skill>\n</available_skills>";
1297
1298 let result = replace_skills_section(prompt, new_xml);
1299
1300 assert!(result.contains("<skill>new</skill>"));
1301 assert!(!result.contains("<skill>old</skill>"));
1302 assert!(result.starts_with("System prompt."));
1303 }
1304}