1use crate::{
2 agents::{
3 agent::Agent, auto_pr::AutoPrAgent, catalog::AgentCatalog, code_review::CodeReviewAgent,
4 coder::CoderAgent, context::ContextAgent, coordinator::CoordinatorAgent,
5 debugger::DebuggerAgent, memory_manager::MemoryManagerAgent,
6 memory_summarizer::MemorySummarizerAgent, one_shot::OneShotAgent, planner::PlannerAgent,
7 tycode::TycodeAgent,
8 },
9 ai::{
10 mock::{MockBehavior, MockProvider},
11 provider::AiProvider,
12 types::{
13 Content, ContentBlock, ImageData, Message, MessageRole, TokenUsage, ToolResultData,
14 ToolUseData,
15 },
16 },
17 analyzer::AnalyzerModule,
18 chat::{
19 ai,
20 events::{ChatEvent, ChatMessage, EventSender, ModuleSchemaInfo},
21 tools,
22 },
23 file::{modify::FileModifyModule, read_only::ReadOnlyFileModule},
24 mcp::McpModule,
25 module::{ContextBuilder, Module, PromptBuilder, PromptComponent},
26 modules::{
27 execution::{CommandResult, ExecutionModule},
28 memory::{
29 background::{safe_conversation_slice, spawn_memory_manager},
30 log::MemoryLog,
31 MemoryConfig, MemoryModule,
32 },
33 task_list::TaskListModule,
34 },
35 settings::{ProviderConfig, Settings, SettingsManager},
36 skills::SkillsModule,
37 spawn::SpawnModule,
38 steering::{SteeringDocuments, SteeringModule},
39 tools::{ask_user_question::AskUserQuestion, r#trait::ToolExecutor},
40};
41
42use anyhow::{bail, Result};
43use aws_config::timeout::TimeoutConfig;
44use chrono::Utc;
45use dirs;
46use rand::Rng;
47use serde::{Deserialize, Serialize};
48use std::collections::BTreeSet;
49use std::path::PathBuf;
50use std::sync::Arc;
51use std::time::{Duration, Instant};
52use tokio::sync::mpsc;
53use tracing::{error, info};
54
55#[derive(Debug, Clone, Copy, PartialEq, Eq)]
56pub enum TimingState {
57 Idle,
58 WaitingForHuman,
59 ProcessingAI,
60 ExecutingTools,
61}
62
63#[derive(Clone, Debug, Default)]
64pub struct TimingStat {
65 pub waiting_for_human: Duration,
66 pub ai_processing: Duration,
67 pub tool_execution: Duration,
68}
69
70impl std::ops::AddAssign for TimingStat {
71 fn add_assign(&mut self, rhs: Self) {
72 self.waiting_for_human += rhs.waiting_for_human;
73 self.ai_processing += rhs.ai_processing;
74 self.tool_execution += rhs.tool_execution;
75 }
76}
77
78#[derive(Debug, Clone)]
79pub struct TimingStats {
80 message: TimingStat,
81 session: TimingStat,
82 current_state: TimingState,
83 state_start: Option<Instant>,
84}
85
86impl TimingStats {
87 fn new() -> Self {
88 Self {
89 message: TimingStat::default(),
90 session: TimingStat::default(),
91 current_state: TimingState::Idle,
92 state_start: Some(Instant::now()),
93 }
94 }
95
96 pub fn session(&self) -> TimingStat {
97 self.session.clone()
98 }
99}
100
101pub struct ChatActorBuilder {
102 workspace_roots: Vec<PathBuf>,
103 root_dir: PathBuf,
104 profile: Option<String>,
105 provider_override: Option<Arc<dyn AiProvider>>,
106 agent_name_override: Option<String>,
107 additional_agents: Vec<Arc<dyn Agent>>,
108 tools: Vec<Arc<dyn ToolExecutor>>,
109 prompt_builder: PromptBuilder,
110 context_builder: ContextBuilder,
111 memory_log: Arc<MemoryLog>,
112 event_sender: EventSender,
113 event_rx: mpsc::UnboundedReceiver<ChatEvent>,
114 modules: Vec<Arc<dyn Module>>,
115 settings_manager: Option<SettingsManager>,
116}
117
118impl ChatActorBuilder {
119 pub fn tycode(
130 workspace_roots: Vec<PathBuf>,
131 root_dir: Option<PathBuf>,
132 profile: Option<String>,
133 ) -> Result<Self> {
134 let root_dir = root_dir.unwrap_or_else(|| {
135 dirs::home_dir()
136 .expect("Failed to get home directory")
137 .join(".tycode")
138 });
139
140 let settings_manager =
141 SettingsManager::from_settings_dir(root_dir.clone(), profile.as_deref())?;
142 let settings = settings_manager.settings();
143
144 let steering = Arc::new(SteeringDocuments::new(
145 workspace_roots.clone(),
146 root_dir.clone(),
147 settings.communication_tone,
148 ));
149
150 let memory_path = root_dir.join("memory").join("memories_log.json");
151 let memory_log = Arc::new(MemoryLog::new(memory_path));
152
153 let (event_sender, event_rx) = EventSender::new();
155
156 let read_only_file_module = Arc::new(ReadOnlyFileModule::new(
158 workspace_roots.clone(),
159 settings_manager.clone(),
160 )?);
161 let task_list_module = Arc::new(TaskListModule::new(event_sender.clone()));
162 let memory_module = MemoryModule::new(memory_log.clone(), settings_manager.clone());
163
164 let mut builder = Self {
165 workspace_roots,
166 root_dir,
167 profile,
168 provider_override: None,
169 agent_name_override: None,
170 additional_agents: Vec::new(),
171 tools: Vec::new(),
172 prompt_builder: PromptBuilder::new(),
173 context_builder: ContextBuilder::new(),
174 memory_log,
175 event_sender,
176 event_rx,
177 modules: Vec::new(),
178 settings_manager: Some(settings_manager.clone()),
179 };
180
181 builder.with_module(read_only_file_module);
182 builder.with_module(task_list_module);
183 builder.with_module(Arc::new(memory_module));
184
185 let execution_module = Arc::new(
186 ExecutionModule::new(builder.workspace_roots.clone(), settings_manager.clone())
187 .expect("Failed to create ExecutionModule"),
188 );
189 builder.with_module(execution_module);
190
191 let home_dir = dirs::home_dir().expect("Failed to get home directory");
193 let skills_module = Arc::new(SkillsModule::new(
194 &builder.workspace_roots,
195 &home_dir,
196 &settings.skills,
197 ));
198 builder.with_module(skills_module);
199
200 builder = builder.with_tool(AskUserQuestion);
202
203 let roots = builder.workspace_roots.clone();
205 let file_modify_module = Arc::new(FileModifyModule::new(roots, settings_manager.clone())?);
206 builder.with_module(file_modify_module);
207
208 let workspace_roots_for_analyzer = builder.workspace_roots.clone();
210 builder.with_module(Arc::new(
211 AnalyzerModule::new(workspace_roots_for_analyzer)
212 .expect("Failed to create AnalyzerModule"),
213 ));
214
215 let steering_module = Arc::new(SteeringModule::new(steering, settings_manager.clone()));
216 builder.with_module(steering_module);
217
218 Ok(builder)
219 }
220
221 pub fn new(workspace_roots: Vec<PathBuf>, root_dir: PathBuf) -> Self {
222 let memory_path = root_dir.join("memory").join("memories_log.json");
223 let memory_log = Arc::new(MemoryLog::new(memory_path));
224 let (event_sender, event_rx) = EventSender::new();
225
226 let task_list_module = Arc::new(TaskListModule::new(event_sender.clone()));
227
228 let mut builder = Self {
229 workspace_roots,
230 root_dir,
231 profile: None,
232 provider_override: None,
233 agent_name_override: None,
234 additional_agents: Vec::new(),
235 tools: Vec::new(),
236 prompt_builder: PromptBuilder::new(),
237 context_builder: ContextBuilder::new(),
238 memory_log,
239 event_sender,
240 event_rx,
241 modules: Vec::new(),
242 settings_manager: None,
243 };
244
245 builder.with_module(task_list_module);
246
247 builder
248 }
249
250 pub fn profile(mut self, name: Option<String>) -> Self {
251 self.profile = name;
252 self
253 }
254
255 pub fn provider(mut self, provider: Arc<dyn AiProvider>) -> Self {
256 self.provider_override = Some(provider);
257 self
258 }
259
260 pub fn agent_name(mut self, name: String) -> Self {
261 self.agent_name_override = Some(name);
262 self
263 }
264
265 pub fn with_agent(mut self, agent: impl Agent + 'static) -> Self {
266 self.additional_agents.push(Arc::new(agent));
267 self
268 }
269
270 pub fn with_tool(mut self, tool: impl ToolExecutor + 'static) -> Self {
271 self.tools.push(Arc::new(tool));
272 self
273 }
274
275 pub fn with_prompt_component(mut self, component: impl PromptComponent + 'static) -> Self {
276 self.prompt_builder.add(Arc::new(component));
277 self
278 }
279
280 pub fn with_context_component(
281 mut self,
282 component: impl crate::module::ContextComponent + 'static,
283 ) -> Self {
284 self.context_builder.add(Arc::new(component));
285 self
286 }
287
288 pub fn with_module(&mut self, module: Arc<dyn Module>) {
289 self.modules.push(module);
290 }
291
292 pub fn build(self) -> Result<(ChatActor, mpsc::UnboundedReceiver<ChatEvent>)> {
293 let (tx, rx) = mpsc::unbounded_channel();
294 let (cancel_tx, cancel_rx) = mpsc::unbounded_channel();
295
296 let workspace_roots = self.workspace_roots;
297 let root_dir = self.root_dir;
298 let profile = self.profile;
299 let provider_override = self.provider_override;
300 let agent_name_override = self.agent_name_override;
301 let additional_agents = self.additional_agents;
302 let tools = self.tools;
303 let prompt_builder = self.prompt_builder;
304 let context_builder = self.context_builder;
305 let memory_log = self.memory_log;
306 let event_sender = self.event_sender;
307 let event_rx = self.event_rx;
308 let modules = self.modules;
309 let settings_manager = self.settings_manager;
310
311 tokio::task::spawn_local(async move {
312 let actor_state = ActorState::new(
313 workspace_roots,
314 event_sender,
315 root_dir,
316 profile,
317 agent_name_override,
318 additional_agents,
319 tools,
320 prompt_builder,
321 context_builder,
322 memory_log,
323 modules,
324 provider_override,
325 settings_manager,
326 )
327 .await;
328
329 run_actor(actor_state, rx, cancel_rx).await;
330 });
331
332 Ok((ChatActor { tx, cancel_tx }, event_rx))
333 }
334}
335
336#[derive(Serialize, Deserialize)]
343pub enum ChatActorMessage {
344 UserInput(String),
346
347 UserInputWithImages {
349 text: String,
350 images: Vec<crate::ai::types::ImageData>,
351 },
352
353 ChangeProvider(String),
357
358 GetSettings,
360 SaveSettings {
361 settings: serde_json::Value,
362 persist: bool,
363 },
364
365 SwitchProfile { profile_name: String },
367
368 SaveProfile { profile_name: String },
370
371 ListProfiles,
373
374 ListSessions,
376
377 ResumeSession { session_id: String },
379
380 GetModuleSchemas,
382}
383
384pub struct ChatActor {
400 pub tx: mpsc::UnboundedSender<ChatActorMessage>,
401 pub cancel_tx: mpsc::UnboundedSender<()>,
402}
403
404impl ChatActor {
405 pub fn send_message(&self, message: String) -> Result<()> {
406 self.tx.send(ChatActorMessage::UserInput(message))?;
407 Ok(())
408 }
409
410 pub fn send_message_with_images(&self, message: String, images: Vec<ImageData>) -> Result<()> {
411 self.tx.send(ChatActorMessage::UserInputWithImages {
412 text: message,
413 images,
414 })?;
415 Ok(())
416 }
417
418 pub fn change_provider(&self, provider: String) -> Result<()> {
419 self.tx.send(ChatActorMessage::ChangeProvider(provider))?;
420 Ok(())
421 }
422
423 pub fn get_settings(&self) -> Result<()> {
424 self.tx.send(ChatActorMessage::GetSettings)?;
425 Ok(())
426 }
427
428 pub fn save_settings(&self, settings: serde_json::Value, persist: bool) -> Result<()> {
429 self.tx
430 .send(ChatActorMessage::SaveSettings { settings, persist })?;
431 Ok(())
432 }
433
434 pub fn cancel(&self) -> Result<()> {
435 self.cancel_tx.send(())?;
436 Ok(())
437 }
438
439 pub fn get_module_schemas(&self) -> Result<()> {
440 self.tx.send(ChatActorMessage::GetModuleSchemas)?;
441 Ok(())
442 }
443}
444
445pub struct ActorState {
446 pub event_sender: EventSender,
447 pub provider: Arc<dyn AiProvider>,
448 pub spawn_module: Arc<SpawnModule>,
449 pub agent_catalog: Arc<AgentCatalog>,
450 pub workspace_roots: Vec<PathBuf>,
451 pub settings: SettingsManager,
452 pub steering: SteeringDocuments,
453 pub tracked_files: BTreeSet<PathBuf>,
454 pub last_command_outputs: Vec<CommandResult>,
455 pub session_token_usage: TokenUsage,
456 pub session_cost: f64,
457 pub profile_name: Option<String>,
458 pub session_id: Option<String>,
459 pub sessions_dir: PathBuf,
460 pub timing_stats: TimingStats,
461 pub memory_log: Arc<MemoryLog>,
462 pub additional_agents: Vec<Arc<dyn Agent>>,
463 pub tools: Vec<Arc<dyn ToolExecutor>>,
464 pub mcp_manager: Arc<McpModule>,
465 pub prompt_builder: PromptBuilder,
466 pub context_builder: ContextBuilder,
467 pub modules: Vec<Arc<dyn Module>>,
468}
469
470impl ActorState {
471 fn generate_session_id() -> String {
472 let timestamp = Utc::now().format("%Y%m%d_%H%M%S");
473 let random: u32 = rand::thread_rng().gen_range(1000..9999);
474 format!("{}_{}", timestamp, random)
475 }
476
477 pub fn save_session(&mut self) -> Result<()> {
478 let Some(ref session_id) = self.session_id else {
479 return Ok(());
480 };
481
482 let messages = self
483 .spawn_module
484 .with_root_agent(|a| a.conversation.clone())
485 .ok_or_else(|| anyhow::anyhow!("No active agent"))?;
486 let tracked_files: Vec<PathBuf> = self.tracked_files.iter().cloned().collect();
487
488 let mut session =
489 crate::persistence::storage::load_session(session_id, Some(&self.sessions_dir))
490 .unwrap_or_else(|_| {
491 crate::persistence::session::SessionData::new(
492 session_id.clone(),
493 Vec::new(),
494 Vec::new(),
495 )
496 });
497
498 session.messages = messages;
499 session.tracked_files = tracked_files;
500
501 for module in &self.modules {
502 if let Some(session_state) = module.session_state() {
503 session
504 .module_state
505 .insert(session_state.key().to_string(), session_state.save());
506 }
507 }
508 session
509 .events
510 .extend_from_slice(&self.event_sender.event_history());
511
512 crate::persistence::storage::save_session(&session, Some(&self.sessions_dir))?;
513
514 self.event_sender.clear_history();
515
516 Ok(())
517 }
518
519 async fn new(
520 workspace_roots: Vec<PathBuf>,
521 event_sender: EventSender,
522 root_dir: PathBuf,
523 profile: Option<String>,
524 agent_name_override: Option<String>,
525 additional_agents: Vec<Arc<dyn Agent>>,
526 tools: Vec<Arc<dyn ToolExecutor>>,
527 prompt_builder: PromptBuilder,
528 context_builder: ContextBuilder,
529 memory_log: Arc<MemoryLog>,
530 mut modules: Vec<Arc<dyn Module>>,
531 provider_override: Option<Arc<dyn AiProvider>>,
532 settings_manager: Option<SettingsManager>,
533 ) -> Self {
534 let settings = settings_manager.unwrap_or_else(|| {
535 SettingsManager::from_settings_dir(root_dir.clone(), profile.as_deref())
536 .expect("Failed to create settings")
537 });
538 let profile_name = profile;
539 let sessions_dir = root_dir.join("sessions");
540
541 let settings_snapshot = settings.settings();
542
543 if settings_snapshot.active_provider().is_none() {
544 event_sender.add_message(ChatMessage::error(
545 "No AI provider is configured. Configure one in settings or with the command /provider add ..."
546 .to_string(),
547 ));
548 }
549
550 if settings_snapshot.model_quality.is_none() && settings_snapshot.agent_models.is_empty() {
552 event_sender.add_message(ChatMessage::warning(
553 "Warning: Cost preferences have not been set. Tycode will default to the highest quality model. Run /cost set <free|low|medium|high|unlimited> to explicitly set a preference.".to_string()
554 ));
555 }
556
557 let provider = if let Some(p) = provider_override {
558 p
559 } else {
560 match create_default_provider(&settings).await {
561 Ok(p) => p,
562 Err(e) => {
563 error!("Failed to initialize provider: {}", e);
564 Arc::new(MockProvider::new(MockBehavior::AlwaysNonRetryableError))
565 }
566 }
567 };
568
569 let mcp_module = McpModule::from_settings(&settings_snapshot)
570 .await
571 .expect("Failed to initialize MCP module");
572
573 modules.push(mcp_module.clone());
574
575 let home_dir = dirs::home_dir().unwrap_or_else(|| PathBuf::from("/"));
576 let steering = SteeringDocuments::new(
577 workspace_roots.clone(),
578 home_dir,
579 settings_snapshot.communication_tone,
580 );
581
582 let mut agent_catalog = AgentCatalog::new();
584 agent_catalog.register_agent(Arc::new(CoordinatorAgent));
585 agent_catalog.register_agent(Arc::new(OneShotAgent));
586 agent_catalog.register_agent(Arc::new(ContextAgent));
587 agent_catalog.register_agent(Arc::new(CoderAgent));
588 agent_catalog.register_agent(Arc::new(DebuggerAgent));
589 agent_catalog.register_agent(Arc::new(PlannerAgent));
590 agent_catalog.register_agent(Arc::new(TycodeAgent));
591 agent_catalog.register_agent(Arc::new(CodeReviewAgent));
592 agent_catalog.register_agent(Arc::new(AutoPrAgent));
593 agent_catalog.register_agent(Arc::new(MemoryManagerAgent));
594 agent_catalog.register_agent(Arc::new(MemorySummarizerAgent));
595
596 for agent in &additional_agents {
598 agent_catalog.register_agent(agent.clone());
599 }
600
601 let agent_catalog = Arc::new(agent_catalog);
602
603 let agent_name = agent_name_override
604 .as_deref()
605 .unwrap_or_else(|| settings_snapshot.default_agent.as_str());
606
607 let agent = agent_catalog
608 .create_agent(agent_name)
609 .unwrap_or_else(|| Arc::new(OneShotAgent));
610
611 let spawn_module = Arc::new(SpawnModule::new(agent_catalog.clone(), agent.clone()));
612 modules.push(spawn_module.clone());
613
614 Self {
615 event_sender,
616 provider,
617 spawn_module,
618 agent_catalog,
619 workspace_roots,
620 settings,
621 steering,
622 tracked_files: BTreeSet::new(),
623 last_command_outputs: Vec::new(),
624 session_token_usage: TokenUsage::empty(),
625 session_cost: 0.0,
626 profile_name,
627 session_id: None,
628 sessions_dir,
629 timing_stats: TimingStats::new(),
630 memory_log,
631 additional_agents,
632 tools,
633 mcp_manager: mcp_module,
634 prompt_builder,
635 context_builder,
636 modules,
637 }
638 }
639
640 pub fn clear_conversation(&mut self) {
641 self.event_sender
642 .send_replay(ChatEvent::ConversationCleared);
643 }
644
645 pub(crate) fn send_event_replay(&mut self, event: ChatEvent) {
646 self.event_sender.send_replay(event);
647 }
648
649 pub fn transition_timing_state(&mut self, new_state: TimingState) {
650 if let Some(start) = self.timing_stats.state_start {
651 let elapsed = start.elapsed();
652 match self.timing_stats.current_state {
653 TimingState::WaitingForHuman => {
654 self.timing_stats.message.waiting_for_human += elapsed;
655 }
656 TimingState::ProcessingAI => {
657 self.timing_stats.message.ai_processing += elapsed;
658 }
659 TimingState::ExecutingTools => {
660 self.timing_stats.message.tool_execution += elapsed;
661 }
662 TimingState::Idle => {}
663 }
664 }
665
666 if matches!(new_state, TimingState::WaitingForHuman) {
667 let message = std::mem::replace(&mut self.timing_stats.message, TimingStat::default());
668 self.timing_stats.session += message;
669 }
670
671 self.timing_stats.current_state = new_state;
672 self.timing_stats.state_start = Some(Instant::now());
673 }
674
675 pub async fn reload_from_settings(&mut self) -> Result<(), anyhow::Error> {
676 let settings_snapshot = self.settings.settings();
677
678 let active_provider = settings_snapshot
679 .active_provider
680 .clone()
681 .unwrap_or_else(|| self.provider.name().to_string());
682 self.provider = create_provider(&self.settings, &active_provider).await?;
683
684 let old_conversation = self
685 .spawn_module
686 .with_root_agent(|a| a.conversation.clone())
687 .unwrap_or_default();
688
689 let default_agent = settings_snapshot.default_agent.clone();
690
691 let home_dir = dirs::home_dir().unwrap_or_else(|| PathBuf::from("/"));
692 self.steering = SteeringDocuments::new(
693 self.workspace_roots.clone(),
694 home_dir,
695 settings_snapshot.communication_tone,
696 );
697
698 let new_agent_dyn =
699 self.agent_catalog
700 .create_agent(&default_agent)
701 .ok_or(anyhow::anyhow!(
702 "Failed to create default agent: {}",
703 default_agent
704 ))?;
705 self.spawn_module.reset_to_agent(new_agent_dyn);
706 self.spawn_module
707 .with_root_agent_mut(|a| a.conversation = old_conversation);
708
709 self.profile_name = self.settings.current_profile().map(|s| s.to_string());
710
711 Ok(())
712 }
713}
714
715async fn run_actor(
717 mut state: ActorState,
718 mut rx: mpsc::UnboundedReceiver<ChatActorMessage>,
719 mut cancel_rx: mpsc::UnboundedReceiver<()>,
720) {
721 info!("ChatActor started");
722
723 loop {
724 tokio::select! {
725 result = process_message(&mut rx, &mut state) => {
726 if let Err(e) = result {
727 error!(?e, "Error processing message");
728 state.event_sender.send_message(ChatMessage::error(format!("Error: {e:?}")));
729 }
730 }
731
732 Some(_) = cancel_rx.recv() => {
734 info!("Cancellation received while idle");
735 handle_cancelled(&mut state);
736 }
737 }
738
739 state.event_sender.send(ChatEvent::TimingUpdate {
740 waiting_for_human: state.timing_stats.message.waiting_for_human,
741 ai_processing: state.timing_stats.message.ai_processing,
742 tool_execution: state.timing_stats.message.tool_execution,
743 });
744 state.event_sender.set_typing(false);
745 state.transition_timing_state(TimingState::WaitingForHuman);
746 }
747}
748
749async fn process_message(
750 rx: &mut mpsc::UnboundedReceiver<ChatActorMessage>,
751 state: &mut ActorState,
752) -> Result<()> {
753 let Some(message) = rx.recv().await else {
754 bail!("request queue dropped")
755 };
756
757 state.transition_timing_state(TimingState::Idle);
758
759 state.event_sender.set_typing(true);
762
763 match message {
764 ChatActorMessage::UserInput(input) => handle_user_input(state, input, vec![]).await,
765 ChatActorMessage::UserInputWithImages { text, images } => {
766 handle_user_input(state, text, images).await
767 }
768 ChatActorMessage::ChangeProvider(provider) => handle_provider_change(state, provider).await,
769 ChatActorMessage::GetSettings => {
770 let settings = state.settings.settings();
771 let mut settings_json = serde_json::to_value(settings)
772 .map_err(|e| anyhow::anyhow!("Failed to serialize settings: {}", e))?;
773 if let serde_json::Value::Object(ref mut map) = settings_json {
775 let profile = state
776 .profile_name
777 .clone()
778 .or_else(|| state.settings.current_profile().map(|s| s.to_string()))
779 .unwrap_or_else(|| "default".to_string());
780 map.insert("profile".to_string(), serde_json::Value::String(profile));
781 }
782 state.event_sender.send(ChatEvent::Settings(settings_json));
783 Ok(())
784 }
785 ChatActorMessage::SaveSettings { settings, persist } => {
786 let new_settings: Settings = serde_json::from_value(settings)
787 .map_err(|e| anyhow::anyhow!("Failed to deserialize settings: {}", e))?;
788 state.settings.update_setting(|s| *s = new_settings);
789 if persist {
790 state.settings.save()?;
791 }
792 Ok(())
793 }
794 ChatActorMessage::SwitchProfile { profile_name } => {
795 state.settings.switch_profile(&profile_name)?;
796 state.reload_from_settings().await?;
797 let settings = state.settings.settings();
798 let settings_json = serde_json::to_value(settings)
799 .map_err(|e| anyhow::anyhow!("Failed to serialize settings: {}", e))?;
800 state.event_sender.send(ChatEvent::Settings(settings_json));
801 state.event_sender.send_message(ChatMessage::system(format!(
802 "Switched to profile: {}",
803 profile_name
804 )));
805 Ok(())
806 }
807 ChatActorMessage::SaveProfile { profile_name } => {
808 state.settings.save_as_profile(&profile_name)?;
809 state.event_sender.send_message(ChatMessage::system(format!(
810 "Settings saved as profile: {}",
811 profile_name
812 )));
813 Ok(())
814 }
815 ChatActorMessage::ListProfiles => {
816 let profiles = state.settings.list_profiles()?;
817 state
818 .event_sender
819 .send(ChatEvent::ProfilesList { profiles });
820 Ok(())
821 }
822 ChatActorMessage::ListSessions => {
823 let sessions = crate::persistence::storage::list_session_metadata(&state.sessions_dir)?;
824 state
825 .event_sender
826 .send(ChatEvent::SessionsList { sessions });
827 Ok(())
828 }
829 ChatActorMessage::ResumeSession { session_id } => resume_session(state, &session_id).await,
830 ChatActorMessage::GetModuleSchemas => {
831 let schemas: Vec<ModuleSchemaInfo> = state
832 .modules
833 .iter()
834 .filter_map(|m| {
835 let namespace = m.settings_namespace()?;
836 let schema = m.settings_json_schema()?;
837 Some(ModuleSchemaInfo {
838 namespace: namespace.to_string(),
839 schema,
840 })
841 })
842 .collect();
843 state
844 .event_sender
845 .send(ChatEvent::ModuleSchemas { schemas });
846 Ok(())
847 }
848 }
849}
850
851fn get_pending_tool_uses(state: &ActorState) -> Vec<ToolUseData> {
853 tools::current_agent(state, |current| {
854 if let Some(last_message) = current.conversation.last() {
855 if last_message.role == MessageRole::Assistant {
856 return last_message
857 .content
858 .tool_uses()
859 .into_iter()
860 .cloned()
861 .collect();
862 }
863 }
864 Vec::new()
865 })
866}
867
868fn create_cancellation_error_results(
870 tool_uses: Vec<ToolUseData>,
871 state: &mut ActorState,
872) -> Vec<ContentBlock> {
873 tool_uses
874 .into_iter()
875 .map(|tool_use| {
876 let result = ToolResultData {
877 tool_use_id: tool_use.id.clone(),
878 content: "Tool execution was cancelled by user".to_string(),
879 is_error: true,
880 };
881
882 state.event_sender.send(ChatEvent::ToolExecutionCompleted {
884 tool_call_id: tool_use.id.clone(),
885 tool_name: tool_use.name.clone(),
886 tool_result: crate::chat::events::ToolExecutionResult::Error {
887 short_message: "Cancelled".to_string(),
888 detailed_message: "Tool execution was cancelled by user".to_string(),
889 },
890 success: false,
891 error: Some("Cancelled by user".to_string()),
892 });
893
894 ContentBlock::ToolResult(result)
895 })
896 .collect()
897}
898
899fn handle_cancelled(state: &mut ActorState) {
900 let pending_tool_uses = get_pending_tool_uses(state);
902
903 if !pending_tool_uses.is_empty() {
904 info!(
905 "Cancellation with {} pending tool calls - generating error results",
906 pending_tool_uses.len()
907 );
908
909 let error_results = create_cancellation_error_results(pending_tool_uses, state);
911
912 tools::current_agent_mut(state, |a| {
914 a.conversation.push(Message {
915 role: MessageRole::User,
916 content: Content::from(error_results),
917 })
918 });
919 }
920
921 state.event_sender.send(ChatEvent::OperationCancelled {
922 message: "Operation cancelled by user".to_string(),
923 });
924}
925
926async fn handle_user_input(
927 state: &mut ActorState,
928 input: String,
929 images: Vec<ImageData>,
930) -> Result<()> {
931 if input.trim().is_empty() && images.is_empty() {
932 return Ok(());
933 }
934
935 if state.session_id.is_none() {
937 state.session_id = Some(ActorState::generate_session_id());
938 }
939
940 if images.is_empty() {
941 state
942 .event_sender
943 .send_message(ChatMessage::user(input.clone()));
944 } else {
945 state
946 .event_sender
947 .send_message(ChatMessage::user_with_images(input.clone(), images.clone()));
948 }
949
950 if let Some(command) = input.strip_prefix('/') {
951 if crate::chat::commands::is_known_command(command, &state.modules) {
952 let messages = crate::chat::commands::process_command(state, command).await;
953
954 for message in messages {
955 state.event_sender.send_message(message);
956 }
957 return Ok(());
958 }
959 }
960
961 let content = if images.is_empty() {
962 Content::text_only(input.clone())
963 } else {
964 let mut blocks = vec![ContentBlock::Text(input.clone())];
965 for image in images {
966 blocks.push(ContentBlock::Image(image));
967 }
968 Content::new(blocks)
969 };
970
971 tools::current_agent_mut(state, |a| {
972 a.conversation.push(Message {
973 role: MessageRole::User,
974 content,
975 })
976 });
977
978 let memory_config: MemoryConfig = state.settings.get_module_config(MemoryConfig::NAMESPACE);
979 if memory_config.enabled {
980 let context_message_count = memory_config.context_message_count;
981
982 let conversation = tools::current_agent(state, |current| {
983 safe_conversation_slice(¤t.conversation, context_message_count)
984 });
985
986 spawn_memory_manager(
987 state.provider.clone(),
988 state.memory_log.clone(),
989 state.settings.clone(),
990 conversation,
991 state.steering.clone(),
992 state.prompt_builder.clone(),
993 state.context_builder.clone(),
994 state.modules.clone(),
995 );
996 }
997
998 ai::send_ai_request(state).await?;
999
1000 if let Err(e) = state.save_session() {
1001 tracing::warn!("Failed to auto-save session: {}", e);
1002 }
1003
1004 Ok(())
1005}
1006
1007async fn handle_provider_change(state: &mut ActorState, provider_name: String) -> Result<()> {
1008 info!("Changing provider to: {}", provider_name);
1009 state.provider = create_provider(&state.settings, &provider_name).await?;
1010
1011 state.event_sender.send_message(ChatMessage::system(format!(
1012 "Switched to provider: {provider_name}"
1013 )));
1014
1015 Ok(())
1016}
1017
1018pub async fn create_provider(
1021 settings: &SettingsManager,
1022 provider: &str,
1023) -> Result<Arc<dyn AiProvider>> {
1024 let config = settings.settings();
1025 let Some(provider_config) = config.providers.get(provider) else {
1026 bail!("No active provider configured in settings")
1027 };
1028
1029 match provider_config {
1030 ProviderConfig::Bedrock { profile, region } => {
1031 use crate::ai::bedrock::BedrockProvider;
1032 use aws_config::retry::RetryConfig;
1033 use aws_config::Region;
1034
1035 if region.is_empty() {
1036 bail!("AWS region is empty")
1037 };
1038
1039 let aws_config = aws_config::defaults(aws_config::BehaviorVersion::latest())
1040 .profile_name(profile)
1041 .region(Region::new(region.to_string()))
1042 .retry_config(RetryConfig::disabled())
1043 .timeout_config(
1044 TimeoutConfig::builder()
1046 .connect_timeout(Duration::from_secs(60))
1047 .operation_attempt_timeout(Duration::from_secs(300))
1048 .read_timeout(Duration::from_secs(300))
1049 .build(),
1050 )
1051 .load()
1052 .await;
1053
1054 let client = aws_sdk_bedrockruntime::Client::new(&aws_config);
1055 Ok(Arc::new(BedrockProvider::new(client)))
1056 }
1057 ProviderConfig::OpenRouter { api_key } => {
1058 use crate::ai::openrouter::OpenRouterProvider;
1059 Ok(Arc::new(OpenRouterProvider::new(api_key.clone())))
1060 }
1061 ProviderConfig::ClaudeCode {
1062 command,
1063 extra_args,
1064 env,
1065 } => {
1066 use crate::ai::claude_code::ClaudeCodeProvider;
1067
1068 let command_path = if command.trim().is_empty() {
1069 PathBuf::from("claude")
1070 } else {
1071 PathBuf::from(command.as_str())
1072 };
1073
1074 Ok(Arc::new(ClaudeCodeProvider::new(
1075 command_path,
1076 extra_args.clone(),
1077 env.clone(),
1078 )))
1079 }
1080 ProviderConfig::Mock { behavior } => Ok(Arc::new(MockProvider::new(behavior.clone()))),
1081 }
1082}
1083
1084async fn create_default_provider(settings: &SettingsManager) -> Result<Arc<dyn AiProvider>> {
1088 let default = &settings.settings().active_provider.unwrap_or_default();
1089 create_provider(settings, default).await
1090}
1091
1092pub async fn resume_session(state: &mut ActorState, session_id: &str) -> Result<()> {
1093 let session_data =
1094 crate::persistence::storage::load_session(session_id, Some(&state.sessions_dir))?;
1095
1096 tools::current_agent_mut(state, |a| {
1097 a.conversation = session_data.messages.clone();
1098 });
1099
1100 for module in &state.modules {
1101 if let Some(session_state) = module.session_state() {
1102 let key = session_state.key();
1103 if let Some(module_state) = session_data.module_state.get(key) {
1104 session_state
1105 .load(module_state.clone())
1106 .map_err(|e| anyhow::anyhow!("Failed to load module state for {key}: {e:?}"))?;
1107 }
1108 }
1109 }
1110
1111 state.tracked_files.clear();
1112 for path in session_data.tracked_files {
1113 state.tracked_files.insert(path);
1114 }
1115
1116 state.session_id = Some(session_data.id.clone());
1117
1118 state.clear_conversation();
1119
1120 for event in session_data.events {
1121 state.send_event_replay(event);
1122 }
1123
1124 Ok(())
1125}