1use crate::actor::{self, ActorCommand, ActorHandle};
6use crate::run;
7use kaynine_core::budget::BudgetPolicy;
8use kaynine_core::compaction::{CompactionConfig, CompactionModelSelector};
9use kaynine_core::error::KaynineError;
10use kaynine_core::event::{EventEnvelope, RealtimeEvent};
11use kaynine_core::ids::{BranchId, ModelId, RunId, SessionId};
12use kaynine_core::message::ContentBlock;
13use kaynine_core::policy::Policy;
14use kaynine_core::provider::{
15 CredentialProvider, ModelCapabilities, ModelProvider, ReasoningLevel, TokenCounter,
16};
17use kaynine_core::store::{
18 BranchRecord, CreateSessionRequest, EntryRecord, RunState, SessionRecord, SessionStore,
19 SteerRecord,
20};
21use kaynine_core::tool::Tool;
22use serde::{Deserialize, Serialize};
23use std::collections::HashMap;
24use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
25use std::sync::{Arc, Mutex};
26use std::time::Duration;
27use tokio::sync::{broadcast, mpsc, oneshot};
28
29pub struct AgentRuntime {
30 store: Arc<dyn SessionStore>,
31 actors: Arc<Mutex<HashMap<SessionId, ActorHandle>>>,
32 actor_seq: AtomicU64,
33 shutting_down: AtomicBool,
34}
35
36#[derive(Clone)]
37pub struct StartRunRequest {
38 pub command_id: String,
39 pub session_id: SessionId,
40 pub branch_id: BranchId,
41 pub content: Vec<ContentBlock>,
42 pub model_override: Option<ModelId>,
43 pub reasoning_override: Option<ReasoningLevel>,
44 pub capabilities: ModelCapabilities,
45 pub system_prompt: String,
46 pub provider: Arc<dyn ModelProvider>,
47 pub token_counter: Arc<dyn TokenCounter>,
48 pub credentials: Arc<dyn CredentialProvider>,
49 pub tools: Vec<Arc<dyn Tool>>,
50 pub budget: BudgetPolicy,
51 pub max_turns: Option<u32>,
52 pub policy: Arc<dyn Policy>,
55 pub approval_timeout: Option<Duration>,
59 pub prompt: Option<Arc<kaynine_core::prompt::PromptComposer>>,
62 pub compaction: Option<CompactionConfig>,
64 pub compaction_selector: Option<Arc<dyn CompactionModelSelector>>,
66}
67
68#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
69pub struct RunAccepted {
70 pub run_id: RunId,
71 pub user_entry_id: kaynine_core::ids::EntryId,
72 pub revision: u64,
73}
74
75#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
76pub struct CancelRequest {
77 pub command_id: String,
78 pub session_id: SessionId,
79 pub run_id: RunId,
80}
81
82#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
83pub enum CancelOutcome {
84 Cancelled { revision: u64 },
85 AlreadyTerminal { state: RunState },
86}
87
88#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
89pub struct SessionSnapshot {
90 pub session: SessionRecord,
91 pub branches: Vec<BranchRecord>,
92 pub chains: HashMap<BranchId, Vec<EntryRecord>>,
93 pub active_run: Option<ActiveRunInfo>,
96 pub current_revision: u64,
97 pub last_run_seq: Option<u64>,
98 pub unapplied_steers: Vec<SteerRecord>,
99}
100
101#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
102pub struct ActiveRunInfo {
103 pub run_id: RunId,
104 pub branch_id: BranchId,
105 pub model: ModelId,
106}
107
108#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
109pub enum ReleaseOutcome {
110 Released,
111 RunAlreadyActive,
112 NotFound,
113}
114
115#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
116pub struct ShutdownReport {
117 pub cancelled_runs: Vec<RunId>,
118 pub interrupted_runs: Vec<RunId>,
119 pub released_sessions: usize,
120}
121
122pub enum SubscriptionItem {
129 Snapshot(Box<SessionSnapshot>),
130 Event(EventEnvelope<RealtimeEvent>),
131 ResyncRequired,
132}
133
134pub struct SessionSubscription {
135 store: Arc<dyn SessionStore>,
136 session_id: SessionId,
137 receiver: broadcast::Receiver<EventEnvelope<RealtimeEvent>>,
138 pending_snapshot: Option<Box<SessionSnapshot>>,
139 resynced: bool,
140}
141
142impl SessionSubscription {
143 pub async fn next(&mut self) -> Option<SubscriptionItem> {
144 if let Some(snapshot) = self.pending_snapshot.take() {
145 return Some(SubscriptionItem::Snapshot(snapshot));
146 }
147 if self.resynced {
148 self.resynced = false;
149 let snapshot =
150 run::build_snapshot(self.store.as_ref(), &self.session_id, None, None, None)
151 .await
152 .ok()?;
153 return Some(SubscriptionItem::Snapshot(Box::new(snapshot)));
154 }
155 match self.receiver.recv().await {
156 Ok(envelope) => Some(SubscriptionItem::Event(envelope)),
157 Err(broadcast::error::RecvError::Lagged(_)) => {
158 self.resynced = true;
159 Some(SubscriptionItem::ResyncRequired)
160 }
161 Err(broadcast::error::RecvError::Closed) => None,
162 }
163 }
164}
165
166#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
167pub struct UpdateSessionRequest {
168 pub command_id: Option<String>,
169 pub session_id: SessionId,
170 pub default_model: Option<ModelId>,
171 pub reasoning: Option<ReasoningLevel>,
172 pub metadata: Option<serde_json::Value>,
173}
174
175#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
176pub struct SteerRequest {
177 pub command_id: String,
178 pub session_id: SessionId,
179 pub run_id: RunId,
180 pub content: String,
181}
182
183#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
184pub struct SteerAccepted {
185 pub steer_id: String,
186 pub revision: u64,
187}
188
189#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
190pub struct ApprovalResolution {
191 pub command_id: String,
192 pub session_id: SessionId,
193 pub run_id: RunId,
194 pub call_id: kaynine_core::ids::ToolCallId,
195 pub approved: bool,
196}
197
198#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
202pub enum ApprovalOutcome {
203 Delivered,
204 Expired,
205 NotFound,
206}
207
208impl AgentRuntime {
209 pub fn new(store: Arc<dyn SessionStore>) -> Self {
210 Self {
211 store,
212 actors: Arc::new(Mutex::new(HashMap::new())),
213 actor_seq: AtomicU64::new(0),
214 shutting_down: AtomicBool::new(false),
215 }
216 }
217
218 pub async fn create_session(
219 &self,
220 request: CreateSessionRequest,
221 ) -> Result<SessionRecord, KaynineError> {
222 self.store.create_session(request).await
223 }
224
225 pub async fn list_sessions(&self) -> Result<Vec<SessionRecord>, KaynineError> {
226 self.store.list_sessions().await
227 }
228
229 pub async fn list_branches(
230 &self,
231 session_id: &SessionId,
232 ) -> Result<Vec<BranchRecord>, KaynineError> {
233 self.store.list_branches(session_id).await
234 }
235
236 pub async fn get_snapshot(
237 &self,
238 session_id: &SessionId,
239 ) -> Result<SessionSnapshot, KaynineError> {
240 let handle = {
241 let actors = self.actors.lock().expect("actor registry mutex poisoned");
242 actors.get(session_id).cloned()
243 };
244 if let Some(handle) = handle {
245 let (tx, rx) = oneshot::channel();
246 if handle
247 .tx
248 .send(ActorCommand::Snapshot { reply: tx })
249 .await
250 .is_ok()
251 {
252 if let Ok(result) = rx.await {
253 return result;
254 }
255 }
256 self.remove_actor(session_id, &handle);
259 }
260 run::build_snapshot(self.store.as_ref(), session_id, None, None, None).await
261 }
262
263 pub async fn start_run(&self, request: StartRunRequest) -> Result<RunAccepted, KaynineError> {
264 self.check_shutting_down()?;
265 let session_id = request.session_id.clone();
266 let request = Box::new(request);
267 self.with_actor(&session_id, move |reply| ActorCommand::StartRun {
268 request: request.clone(),
269 reply,
270 })
271 .await
272 }
273
274 pub async fn cancel(&self, request: CancelRequest) -> Result<CancelOutcome, KaynineError> {
275 self.check_shutting_down()?;
276 let session_id = request.session_id.clone();
277 self.with_actor(&session_id, move |reply| ActorCommand::Cancel {
278 request: request.clone(),
279 reply,
280 })
281 .await
282 }
283
284 pub async fn watch_session(
289 &self,
290 session_id: &SessionId,
291 ) -> Result<SessionSubscription, KaynineError> {
292 let receiver = self
296 .with_actor(session_id, |reply| ActorCommand::Subscribe { reply })
297 .await?;
298 let snapshot = self
299 .with_actor(session_id, |reply| ActorCommand::Snapshot { reply })
300 .await?;
301 Ok(SessionSubscription {
302 store: self.store.clone(),
303 session_id: session_id.clone(),
304 receiver,
305 pending_snapshot: Some(Box::new(snapshot)),
306 resynced: false,
307 })
308 }
309
310 pub async fn steer(&self, request: SteerRequest) -> Result<SteerAccepted, KaynineError> {
311 self.check_shutting_down()?;
312 let session_id = request.session_id.clone();
313 self.with_actor(&session_id, move |reply| ActorCommand::Steer {
314 request: request.clone(),
315 reply,
316 })
317 .await
318 }
319
320 pub async fn resolve_approval(
321 &self,
322 request: ApprovalResolution,
323 ) -> Result<ApprovalOutcome, KaynineError> {
324 self.check_shutting_down()?;
325 let session_id = request.session_id.clone();
326 self.with_actor(&session_id, move |reply| ActorCommand::ResolveApproval {
327 request: request.clone(),
328 reply,
329 })
330 .await
331 }
332
333 pub async fn update_session(
334 &self,
335 request: UpdateSessionRequest,
336 ) -> Result<SessionRecord, KaynineError> {
337 self.check_shutting_down()?;
338 let session_id = request.session_id.clone();
339 self.with_actor(&session_id, move |reply| ActorCommand::UpdateSession {
340 request: request.clone(),
341 reply,
342 })
343 .await
344 }
345
346 pub async fn release_session(
347 &self,
348 session_id: &SessionId,
349 ) -> Result<ReleaseOutcome, KaynineError> {
350 let handle = {
351 let actors = self.actors.lock().expect("actor registry mutex poisoned");
352 actors.get(session_id).cloned()
353 };
354 let Some(handle) = handle else {
355 return Ok(ReleaseOutcome::NotFound);
356 };
357 let (tx, rx) = oneshot::channel();
358 if handle
359 .tx
360 .send(ActorCommand::Release { reply: tx })
361 .await
362 .is_err()
363 {
364 self.remove_actor(session_id, &handle);
365 return Ok(ReleaseOutcome::NotFound);
366 }
367 match rx.await {
368 Ok(result) => result,
369 Err(_) => {
370 self.remove_actor(session_id, &handle);
371 Ok(ReleaseOutcome::NotFound)
372 }
373 }
374 }
375
376 pub async fn shutdown(&self, grace: Duration) -> Result<ShutdownReport, KaynineError> {
377 self.shutting_down.store(true, Ordering::SeqCst);
378 let handles: Vec<(SessionId, ActorHandle)> = {
379 let actors = self.actors.lock().expect("actor registry mutex poisoned");
380 actors
381 .iter()
382 .map(|(id, h)| (id.clone(), h.clone()))
383 .collect()
384 };
385 let mut report = ShutdownReport::default();
386 for (session_id, handle) in handles {
387 let (tx, rx) = oneshot::channel();
388 if handle
389 .tx
390 .send(ActorCommand::Shutdown { grace, reply: tx })
391 .await
392 .is_err()
393 {
394 self.remove_actor(&session_id, &handle);
395 continue;
396 }
397 if let Ok(Ok((cancelled, interrupted))) = rx.await {
398 report.cancelled_runs.extend(cancelled);
399 report.interrupted_runs.extend(interrupted);
400 report.released_sessions += 1;
401 }
402 self.remove_actor(&session_id, &handle);
403 }
404 Ok(report)
405 }
406
407 fn check_shutting_down(&self) -> Result<(), KaynineError> {
408 if self.shutting_down.load(Ordering::SeqCst) {
409 return Err(KaynineError::InvalidRequest);
410 }
411 Ok(())
412 }
413
414 async fn with_actor<T>(
419 &self,
420 session_id: &SessionId,
421 build: impl Fn(oneshot::Sender<Result<T, KaynineError>>) -> ActorCommand,
422 ) -> Result<T, KaynineError> {
423 let mut handle = self.get_or_spawn_actor(session_id)?;
424 for attempt in 0..2 {
425 let (tx, rx) = oneshot::channel();
426 if handle.tx.send(build(tx)).await.is_ok() {
427 return match rx.await {
428 Ok(result) => result,
429 Err(_) => {
430 if attempt == 0 {
431 self.remove_actor(session_id, &handle);
432 handle = self.get_or_spawn_actor(session_id)?;
433 continue;
434 }
435 Err(KaynineError::Internal)
436 }
437 };
438 }
439 if attempt == 0 {
440 self.remove_actor(session_id, &handle);
441 handle = self.get_or_spawn_actor(session_id)?;
442 continue;
443 }
444 return Err(KaynineError::Internal);
445 }
446 Err(KaynineError::Internal)
447 }
448
449 fn get_or_spawn_actor(&self, session_id: &SessionId) -> Result<ActorHandle, KaynineError> {
450 self.check_shutting_down()?;
451 let mut actors = self.actors.lock().expect("actor registry mutex poisoned");
452 if let Some(handle) = actors.get(session_id) {
453 return Ok(handle.clone());
454 }
455 let (tx, rx) = mpsc::channel(64);
456 let handle = ActorHandle {
457 actor_id: self.actor_seq.fetch_add(1, Ordering::SeqCst),
458 tx,
459 };
460 actors.insert(session_id.clone(), handle.clone());
461 drop(actors);
462 actor::spawn(
463 self.store.clone(),
464 self.actors.clone(),
465 session_id.clone(),
466 handle.clone(),
467 rx,
468 );
469 Ok(handle)
470 }
471
472 fn remove_actor(&self, session_id: &SessionId, handle: &ActorHandle) {
473 let mut actors = self.actors.lock().expect("actor registry mutex poisoned");
474 if actors
475 .get(session_id)
476 .is_some_and(|current| current.actor_id == handle.actor_id)
477 {
478 actors.remove(session_id);
479 }
480 }
481}