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