Skip to main content

tycode_core/chat/
actor.rs

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    /// Create a Tycode coding agent builder with all standard setup.
120    ///
121    /// This handles:
122    /// - Setting root_dir to ~/.tycode if not provided
123    /// - Creating SettingsManager with the specified profile
124    /// - Creating SteeringDocuments with settings from that profile
125    /// - Creating MemoryLog
126    /// - Creating all context managers and prompt components
127    ///
128    /// After this, callers only need to set optional overrides and call build().
129    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        // Create EventSender upfront so components can use it
154        let (event_sender, event_rx) = EventSender::new();
155
156        // Create modules
157        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        // Install skills module
192        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        // Agent control tools
201        builder = builder.with_tool(AskUserQuestion);
202
203        // File modification module (write, delete, modify tools)
204        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        // LSP/analyzer module
209        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/// Defines the possible input messages to the `ChatActor`.
337///
338/// These messages derive serde for use across processes. Applications such as
339/// VSCode spawn tycode-core in a sub-process and communicate to the actor over
340/// stdin/stdout. In such applications, these messages are serialized to json
341/// and sent over stdin.
342#[derive(Serialize, Deserialize)]
343pub enum ChatActorMessage {
344    /// A user input to the conversation with the current AI agent
345    UserInput(String),
346
347    /// A user input with attached images
348    UserInputWithImages {
349        text: String,
350        images: Vec<crate::ai::types::ImageData>,
351    },
352
353    /// Changes the AI provider (i.e. Bedrock, OpenRouter, etc) that this actor
354    /// is using. This is an in-memory only change that only lasts for the
355    /// duration of this actor's lifetime.
356    ChangeProvider(String),
357
358    /// Sends the current settings (from SettingsManager) to the EventSender
359    GetSettings,
360    SaveSettings {
361        settings: serde_json::Value,
362        persist: bool,
363    },
364
365    /// Switches to a different settings profile
366    SwitchProfile { profile_name: String },
367
368    /// Saves current settings as a new profile
369    SaveProfile { profile_name: String },
370
371    /// Lists all available settings profiles
372    ListProfiles,
373
374    /// Requests all available sessions
375    ListSessions,
376
377    /// Requests to resume a specific session
378    ResumeSession { session_id: String },
379
380    /// Requests JSON schemas for all module settings
381    GetModuleSchemas,
382}
383
384/// The `ChatActor` implements the core (or backend) of tycode.
385///
386/// Tycode UI applications (such as the CLI and VSCode extension) do not
387/// contain any  application logic; instead they are simple UI wrappers that
388/// take input from the user, send it to the actor, and render events from the
389/// actor back in to the UI.
390///
391/// The interface to the actor is essentially two channels: an input and output
392/// channel. `ChatActorMessage` are sent to the input channel by UI
393/// applications and `ChatEvents` are emitted by the actor to the output queue.
394/// The ChatActor struct wraps the input channel and provides some convenience
395/// methods and offers cancellation (technically there is a third cancellation
396/// channel, however that is encapsulated by the ChatActor). Events from the
397/// actor are received through a `mpsc::UnboundedReceiver<ChatEvent>` which is
398/// returned when the actor is launched.
399pub 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        // Check if cost preferences are set and send warning if not
551        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        // Create and populate agent catalog with hardcoded agents
583        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        // Register custom agents from builder
597        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
715// Actor implementation as free functions
716async 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            // Handle cancellation even when no message is being processed
733            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    // At the start of each event processing, we set "typing" to true to
760    // indicate to UI applications that we are thinking.
761    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            // Include the current profile name in the settings response
774            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
851/// Extract any pending tool uses from the last assistant message
852fn 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
868/// Create error results for cancelled tool calls
869fn 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            // Emit event for UI
883            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    // Check if there are any pending tool uses that need error results
901    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        // Create error results for all pending tool calls
910        let error_results = create_cancellation_error_results(pending_tool_uses, state);
911
912        // Add these error results to the conversation as a User message
913        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    // Generate session ID on first user message
936    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(&current.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
1018/// Initializes the provider with the given name if it exists in settings, else
1019/// raises an error.
1020pub 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                    // Tuned for Alaska airline's Wifi
1045                    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
1084/// Creates the provider marked as default from the current settings. Note: the
1085/// "active" provider in the settings is just the default that is used if the
1086/// user hasn't selected an overriding provider (using the ChangeProvider event)
1087async 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}