koda_core/session.rs
1//! KodaSession — per-conversation state.
2//!
3//! Holds mutable, per-turn state: database handle, session ID,
4//! provider instance, approval mode, and cancellation token.
5//! Instantiable N times for parallel sub-agents or cowork mode.
6//!
7//! ## Architecture
8//!
9//! ```text
10//! KodaAgent (shared, immutable)
11//! ├─ tools, system prompt, project root
12//! └─ shared via Arc across sessions
13//!
14//! KodaSession (per-conversation, mutable)
15//! ├─ database handle (SQLite)
16//! ├─ session_id (UUID)
17//! ├─ provider instance
18//! ├─ trust mode (plan/safe/auto)
19//! └─ cancellation token
20//! ```
21//!
22//! This split allows the same agent to power multiple concurrent sessions
23//! (e.g., main REPL + background sub-agents) without shared mutable state.
24
25use crate::agent::KodaAgent;
26use crate::config::KodaConfig;
27use crate::db::Database;
28use crate::engine::{EngineCommand, EngineSink};
29use crate::file_tracker::FileTracker;
30use crate::inference::InferenceContext;
31use crate::providers::{self, ImageData, LlmProvider};
32use crate::trust::TrustMode;
33
34use anyhow::Result;
35use std::sync::Arc;
36use tokio::sync::mpsc;
37use tokio_util::sync::CancellationToken;
38
39/// A single conversation session with its own state.
40///
41/// Each session has its own provider, trust mode, and cancel token.
42/// Multiple sessions can share the same `Arc<KodaAgent>`.
43pub struct KodaSession {
44 /// Unique session identifier.
45 pub id: String,
46 /// Shared agent configuration (tools, system prompt).
47 pub agent: Arc<KodaAgent>,
48 /// Database handle for message persistence.
49 pub db: Database,
50 /// LLM provider for this session.
51 pub provider: Box<dyn LlmProvider>,
52 /// Current trust mode (Plan / Safe / Auto).
53 pub mode: TrustMode,
54 /// Cancellation token for graceful shutdown.
55 pub cancel: CancellationToken,
56 /// File lifecycle tracker — tracks files created by Koda (#465).
57 pub file_tracker: FileTracker,
58 /// Whether the session title has already been set (first-message guard).
59 pub title_set: bool,
60}
61
62impl KodaSession {
63 /// Create a new session from an agent, config, and database.
64 pub async fn new(
65 id: String,
66 agent: Arc<KodaAgent>,
67 db: Database,
68 config: &KodaConfig,
69 mode: TrustMode,
70 ) -> Self {
71 let provider = providers::create_provider(config);
72 // Wire db+session into ToolRegistry for RecallContext
73 agent.tools.set_session(Arc::new(db.clone()), id.clone());
74
75 // Start MCP servers from DB config (#662)
76 // TODO(#662 Phase 2): Move MCP manager to app-level ownership so
77 // servers are shared across sessions and not duplicated on resume.
78 match crate::mcp::McpManager::start_from_db(&db).await {
79 Ok(manager) => {
80 if !manager.is_empty() {
81 let mgr = Arc::new(tokio::sync::RwLock::new(manager));
82 agent.tools.set_mcp_manager(mgr);
83 }
84 }
85 Err(e) => {
86 tracing::warn!(error = %e, "failed to start MCP servers (non-fatal)");
87 }
88 }
89 let file_tracker = FileTracker::new(&id, db.clone()).await;
90 Self {
91 id,
92 agent,
93 db,
94 provider,
95 mode,
96 cancel: CancellationToken::new(),
97 file_tracker,
98 title_set: false,
99 }
100 }
101
102 /// Run one inference turn: prompt → streaming → tool execution → response.
103 ///
104 /// Emits `TurnStart` and `TurnEnd` lifecycle events. The loop-cap prompt is handled via `EngineEvent::LoopCapReached` / `EngineCommand::LoopDecision`
105 /// through the `cmd_rx` channel.
106 pub async fn run_turn(
107 &mut self,
108 config: &KodaConfig,
109 pending_images: Option<Vec<ImageData>>,
110 sink: &dyn EngineSink,
111 cmd_rx: &mut mpsc::Receiver<EngineCommand>,
112 ) -> Result<()> {
113 let turn_id = uuid::Uuid::new_v4().to_string();
114 sink.emit(crate::engine::EngineEvent::TurnStart {
115 turn_id: turn_id.clone(),
116 });
117
118 let result = crate::inference::inference_loop(InferenceContext {
119 project_root: &self.agent.project_root,
120 config,
121 db: &self.db,
122 session_id: &self.id,
123 system_prompt: &self.agent.system_prompt,
124 provider: self.provider.as_ref(),
125 tools: &self.agent.tools,
126 tool_defs: &self.agent.tool_defs,
127 pending_images,
128 mode: self.mode,
129 sink,
130 cancel: self.cancel.clone(),
131 cmd_rx,
132 file_tracker: &mut self.file_tracker,
133 })
134 .await;
135
136 let reason = match &result {
137 Ok(()) if self.cancel.is_cancelled() => crate::engine::event::TurnEndReason::Cancelled,
138 Ok(()) => crate::engine::event::TurnEndReason::Complete,
139 Err(e) => crate::engine::event::TurnEndReason::Error {
140 message: e.to_string(),
141 },
142 };
143 sink.emit(crate::engine::EngineEvent::TurnEnd { turn_id, reason });
144
145 result
146 }
147
148 /// Replace the provider (e.g., after switching models or providers).
149 pub fn update_provider(&mut self, config: &KodaConfig) {
150 self.provider = providers::create_provider(config);
151 }
152}