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
//! KodaSession — per-conversation state.
//!
//! Holds mutable, per-turn state: database handle, session ID,
//! provider instance, approval settings, and cancellation token.
//! Instantiable N times for parallel sub-agents or cowork mode.
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 crate::settings::Settings;
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 settings, 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,
/// User settings (last provider, preferences).
pub settings: Settings,
/// Cancellation token for graceful shutdown.
pub cancel: CancellationToken,
/// File lifecycle tracker — tracks files created by Koda (#465).
pub file_tracker: FileTracker,
}
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);
let settings = Settings::load();
// 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,
settings,
cancel: CancellationToken::new(),
file_tracker,
}
}
/// 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,
settings: &mut self.settings,
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);
}
}