1use std::{
2 collections::{BTreeMap, HashMap, HashSet},
3 sync::Arc,
4};
5
6use cookie_agent_protocol::{
7 AgentId, AgentUsageResult, ChildSummary, EventOrigin, GlobalUsageResult, InvocationId,
8 PermissionMode, RunSelection, SessionForkResult, SessionId, SessionMeta, SessionOrigin,
9 SessionRenameChange, SessionRenameParams, SessionRenameResult, SessionRevertResult,
10 SessionTreeUsageResult, SessionUsageResult, UsageRollup,
11};
12
13use super::{
14 Engine, EngineError, Event, SessionCommand,
15 delegation::DELEGATION_RECOVERY_SETTLE_BOUND,
16 helpers::{cwd_identity, root_id, session_depth},
17};
18use crate::policy::{self, freeze_root_agent_policy, resolve_agent};
19
20impl Engine {
21 pub fn session_model_selection(&self, session: SessionId) -> Result<RunSelection, EngineError> {
23 let projection = self.inner.store.get(session)?;
24 Ok(
25 cookie_agent_protocol::SessionModelState::from_events(&projection.log.event_snapshot())
26 .selection
27 .unwrap_or(projection.meta.creation_selection),
28 )
29 }
30 pub(super) fn rebuild_visible_tree_grants(&self) {
33 let invalidated = self.inner.grant_journal.invalidated_ids();
34 let mut grants = Vec::new();
35 for session in self.inner.store.root_snapshots() {
36 for event in session.log.event_snapshot().iter() {
37 if let Event::TreeApprovalGrantCommitted { grant } = &event.payload
38 && !invalidated.contains(&grant.grant_id)
39 {
40 grants.push(grant.clone());
41 }
42 }
43 }
44 grants.extend(
45 self.inner
46 .store
47 .loaded_tree_grants()
48 .into_iter()
49 .filter(|grant| !invalidated.contains(&grant.grant_id)),
50 );
51 self.inner.approvals.store.replace(grants);
52 }
53
54 pub fn create_session(&self, selection: RunSelection) -> Result<SessionMeta, EngineError> {
55 self.reconcile_provider_store()?;
56 let runtime = self.current_runtime();
57 if runtime.result.snapshot.models.is_empty() {
58 return Err(EngineError::NoRunnableModel);
59 }
60 let agents = runtime.agents_for_preset(selection.preset.as_deref())?;
61 let agent = resolve_agent(&agents, &selection.agent)?.clone();
62 let policy = freeze_root_agent_policy(
63 &agent,
64 agents,
65 Arc::clone(&runtime),
66 &selection.model,
67 self.inner.config.runtime.delegation.max_depth,
68 policy::ResultLimits {
69 tool_output_max_lines: self.inner.config.runtime.tool_output.max_lines,
70 tool_output_max_bytes: self.inner.config.runtime.tool_output.max_bytes,
71 },
72 self.inner.config.runtime.model_retry,
73 )?;
74 let id = SessionId::new_v7();
75 let cwd_identity = cwd_identity(self.inner.store.cwd())?;
76 let short_id = super::handles::generate_handle(
80 selection.agent.as_str(),
81 &HashSet::new(),
82 super::handles::random_hex,
83 )?;
84 let creation = Event::SessionCreated {
85 origin: SessionOrigin::Root,
86 short_id: Some(short_id),
87 cwd_identity: cwd_identity.clone(),
88 creation_selection: selection.clone(),
89 creation_agent: Box::new(policy.agent.clone()),
90 runtime_revision: runtime.result.snapshot.runtime_revision.clone(),
91 catalog_revision: runtime.result.snapshot.catalog_revision.clone(),
92 provider_state_revision: runtime.result.snapshot.provider_state_revision.clone(),
93 model_revision: runtime.result.snapshot.model_revision.clone(),
94 agent_revision: runtime.result.snapshot.agent_revision.clone(),
95 recipe_registry_revision: runtime.result.snapshot.recipe_registry_revision.clone(),
96 manifest_revision: runtime.current_manifest.revision.clone(),
97 };
98 self.inner.store.create(
99 id,
100 EventOrigin::new("user").expect("static event origin is valid"),
101 creation,
102 )?;
103 self.spawn_actor(id)?;
104 Ok(self.inner.store.get(id)?.metadata())
105 }
106
107 #[must_use]
112 pub fn list_sessions(&self) -> Vec<SessionMeta> {
113 self.inner
114 .store
115 .all_summaries()
116 .into_iter()
117 .map(|session| session.meta)
118 .filter(|meta| matches!(meta.origin, SessionOrigin::Root))
119 .collect()
120 }
121 pub fn get_session(&self, id: SessionId) -> Result<SessionMeta, EngineError> {
122 let session = self.inner.store.get(id)?;
123 #[cfg(test)]
124 let hook = self
125 .inner
126 .test_hooks
127 .read_only_reopen_hook
128 .lock()
129 .expect("read-only reopen hook lock poisoned")
130 .take();
131 #[cfg(test)]
132 if let Some(hook) = hook {
133 if let Some(reached) = hook
134 .reached
135 .lock()
136 .expect("read-only reopen reached lock poisoned")
137 .take()
138 {
139 let _ = reached.send(());
140 }
141 let _ = hook
142 .release
143 .lock()
144 .expect("read-only reopen release lock poisoned")
145 .recv();
146 }
147 Ok(session.metadata())
148 }
149
150 #[cfg(test)]
151 pub(crate) fn install_read_only_reopen_hook_for_test(
152 &self,
153 ) -> (
154 tokio::sync::oneshot::Receiver<()>,
155 std::sync::mpsc::Sender<()>,
156 ) {
157 let (reached, receiver) = tokio::sync::oneshot::channel();
158 let (release, release_receiver) = std::sync::mpsc::channel();
159 *self
160 .inner
161 .test_hooks
162 .read_only_reopen_hook
163 .lock()
164 .expect("read-only reopen hook lock poisoned") = Some(super::ReadOnlyReopenHook {
165 reached: std::sync::Mutex::new(Some(reached)),
166 release: std::sync::Mutex::new(release_receiver),
167 });
168 (receiver, release)
169 }
170 pub fn session_usage(&self, id: SessionId) -> Result<SessionUsageResult, EngineError> {
171 let catalog = self.catalog_pricing();
172 Ok(self
173 .inner
174 .store
175 .session_usage(id, &self.inner.config.runtime.pricing, &catalog)?)
176 }
177 pub fn agent_usage(&self, agent_id: AgentId) -> AgentUsageResult {
178 let mut usage = UsageRollup::default();
179 for session in self.inner.store.all_summaries() {
180 if let Some(agent) = session.agent_usage.get(&agent_id) {
181 crate::usage::merge(&mut usage, agent);
182 }
183 }
184 AgentUsageResult {
185 agent_id,
186 usage: crate::usage::with_pricing(
187 usage,
188 &self.inner.config.runtime.pricing,
189 &self.catalog_pricing(),
190 ),
191 }
192 }
193 pub fn global_usage(&self) -> GlobalUsageResult {
194 let mut usage = UsageRollup::default();
195 for session in self.inner.store.all_summaries() {
196 crate::usage::merge(&mut usage, &session.usage_rollup);
197 }
198 GlobalUsageResult {
199 usage: crate::usage::with_pricing(
200 usage,
201 &self.inner.config.runtime.pricing,
202 &self.catalog_pricing(),
203 ),
204 }
205 }
206 pub fn session_tree_usage(&self, id: SessionId) -> Result<SessionTreeUsageResult, EngineError> {
207 self.inner.store.ensure_tree_for(id)?;
211 let summaries = self.inner.store.all_summaries();
212 let mut children: HashMap<SessionId, Vec<(SessionId, InvocationId)>> = HashMap::new();
213 let mut rollups = HashMap::new();
214 for summary in summaries {
215 let session_id = summary.meta.session_id;
216 if let SessionOrigin::Delegated {
217 root_session_id,
218 parent_session_id,
219 invocation_id,
220 ..
221 } = summary.meta.origin
222 && root_session_id == id
223 {
224 let edge = (session_id, invocation_id);
225 children.entry(parent_session_id).or_default().push(edge);
226 }
227 rollups.insert(session_id, summary.usage_rollup);
228 }
229
230 let mut usage = UsageRollup::default();
231 let session_count = merge_session_tree_usage(id, &children, &rollups, &mut usage)?;
232 Ok(SessionTreeUsageResult {
233 session_id: id,
234 usage: crate::usage::with_pricing(
235 usage,
236 &self.inner.config.runtime.pricing,
237 &self.catalog_pricing(),
238 ),
239 session_count,
240 })
241 }
242 pub(super) fn catalog_pricing(
243 &self,
244 ) -> BTreeMap<cookie_agent_protocol::ModelKey, cookie_agent_models::catalog::CatalogModelCost>
245 {
246 self.inner
247 .model_manager
248 .current()
249 .models()
250 .iter()
251 .filter_map(|(model, runtime)| {
252 runtime
253 .model
254 .cost
255 .as_ref()
256 .map(|cost| (model.clone(), cost.clone()))
257 })
258 .collect()
259 }
260 pub fn delegate_targets(&self, id: SessionId) -> Result<Vec<AgentId>, EngineError> {
261 let session = self.inner.store.get(id)?;
262 let active = self
263 .inner
264 .sessions
265 .active
266 .lock()
267 .unwrap_or_else(|poisoned| poisoned.into_inner())
268 .values()
269 .find(|active| active.session == id)
270 .cloned();
271 let Some(active) = active else {
272 return Ok(Vec::new());
273 };
274 let depth = session_depth(&session.meta.origin);
275 Ok(active.policy.delegate_targets(depth))
276 }
277 pub fn children(
278 &self,
279 id: SessionId,
280 ) -> Result<Vec<cookie_agent_protocol::ChildSummary>, EngineError> {
281 self.ensure_tree_loaded(id)?;
285 let known: HashSet<_> = self
286 .inner
287 .delegation_events
288 .entries()
289 .into_iter()
290 .map(|entry| {
291 (
292 entry.reservation.invocation_id,
293 entry.reservation.parent_session_id,
294 entry.reservation.child_session_id,
295 )
296 })
297 .collect();
298 Ok(self
299 .inner
300 .store
301 .all_summaries()
302 .into_iter()
303 .filter_map(|child| match child.meta.origin {
304 SessionOrigin::Delegated {
305 parent_session_id,
306 invocation_id,
307 ..
308 } if parent_session_id == id
309 && known.contains(&(
310 invocation_id,
311 parent_session_id,
312 child.meta.session_id,
313 )) =>
314 {
315 Some(ChildSummary {
316 session_id: child.meta.session_id,
317 agent: child.meta.creation_selection.agent.clone(),
318 title: child.meta.title.clone(),
319 title_updated_seq: child.meta.title_updated_seq,
320 status: child.meta.status,
321 usage: child.usage,
322 })
323 }
324 _ => None,
325 })
326 .collect())
327 }
328
329 pub fn tree(&self, id: SessionId) -> Result<cookie_agent_protocol::SessionTree, EngineError> {
330 self.ensure_tree_loaded(id)?;
331 self.inner.store.get(id)?;
332 self.tree_summary(id)
333 }
334
335 fn tree_summary(
336 &self,
337 id: SessionId,
338 ) -> Result<cookie_agent_protocol::SessionTree, EngineError> {
339 Ok(cookie_agent_protocol::SessionTree {
340 session: self.inner.store.summary(id)?.meta,
341 children: self
342 .children(id)?
343 .into_iter()
344 .map(|child| self.tree_summary(child.session_id))
345 .collect::<Result<Vec<_>, _>>()?,
346 })
347 }
348 pub async fn resume(&self, id: SessionId) -> Result<SessionMeta, EngineError> {
361 let meta = self
362 .request(id, |reply| SessionCommand::Resume { reply })
363 .await?;
364 let root = self.inner.store.root_of(id).unwrap_or(id);
365 if !self
366 .await_delegation_recovery(root, DELEGATION_RECOVERY_SETTLE_BOUND)
367 .await
368 {
369 eprintln!(
370 "session {id} delegation recovery still settling after {DELEGATION_RECOVERY_SETTLE_BOUND:?}"
371 );
372 }
373 Ok(meta)
374 }
375 pub async fn revert_session(
376 &self,
377 session_id: SessionId,
378 through_seq: u64,
379 origin: EventOrigin,
380 ) -> Result<SessionRevertResult, EngineError> {
381 let context_id = crate::plugin::plugin_context_id();
382 let mut instructions = self
383 .inner
384 .store
385 .get(session_id)?
386 .log
387 .event_snapshot()
388 .iter()
389 .find_map(|event| match &event.payload {
390 cookie_agent_protocol::EventPayload::UserInputSubmitted { input }
391 if event.seq > through_seq =>
392 {
393 Some(input.clone())
394 }
395 _ => None,
396 });
397 let mut instructions_override = None;
398 for plugin in self.inner.plugins.interception_plugins(
399 cookie_agent_protocol::ExtensionInterceptionHook::SessionBeforeRevert,
400 ) {
401 let result = self
402 .inner
403 .plugins
404 .intercept_named::<_, cookie_agent_protocol::ExtensionSessionBeforeRevertResult>(
405 &plugin,
406 cookie_agent_protocol::PLUGIN_INTERCEPT_SESSION_BEFORE_REVERT_METHOD,
407 &cookie_agent_protocol::ExtensionSessionBeforeRevertParams {
408 session_id,
409 context_id: context_id.clone(),
410 through_seq,
411 instructions: instructions.clone(),
412 },
413 Some(session_id),
414 Some(&context_id),
415 )
416 .await;
417 match result {
418 Ok(result)
419 if result.action
420 == cookie_agent_protocol::ExtensionSessionBeforeRevertAction::Block =>
421 {
422 let reason = result
423 .reason
424 .unwrap_or_else(|| format!("session revert blocked by {plugin}"));
425 self.record_plugin_diagnostic(
426 session_id,
427 plugin,
428 cookie_agent_protocol::PluginDiagnosticKind::HookBlocked,
429 reason.clone(),
430 );
431 return Err(EngineError::SessionOperationBlocked(reason));
432 }
433 Ok(result)
434 if result.action
435 == cookie_agent_protocol::ExtensionSessionBeforeRevertAction::Override =>
436 {
437 if let Some(replacement) = result
438 .instructions_override
439 .filter(|value| !value.trim().is_empty())
440 {
441 instructions = Some(replacement.clone());
442 instructions_override = Some(replacement);
443 } else {
444 self.record_plugin_diagnostic(
445 session_id,
446 plugin,
447 cookie_agent_protocol::PluginDiagnosticKind::InvalidModification,
448 "empty revert instruction override".into(),
449 );
450 }
451 }
452 Ok(_) => {}
453 Err(error) => self.record_interception_error(session_id, plugin, error),
454 }
455 }
456 self.request(session_id, |reply| SessionCommand::Revert {
457 through_seq,
458 origin,
459 instructions_override,
460 reply,
461 })
462 .await
463 }
464 pub async fn fork_session(
465 &self,
466 session_id: SessionId,
467 through_seq: u64,
468 origin: EventOrigin,
469 ) -> Result<SessionForkResult, EngineError> {
470 let context_id = crate::plugin::plugin_context_id();
471 for plugin in self.inner.plugins.interception_plugins(
472 cookie_agent_protocol::ExtensionInterceptionHook::SessionBeforeFork,
473 ) {
474 let result = self
475 .inner
476 .plugins
477 .intercept_named::<_, cookie_agent_protocol::ExtensionSessionBeforeForkResult>(
478 &plugin,
479 cookie_agent_protocol::PLUGIN_INTERCEPT_SESSION_BEFORE_FORK_METHOD,
480 &cookie_agent_protocol::ExtensionSessionBeforeForkParams {
481 session_id,
482 context_id: context_id.clone(),
483 through_seq,
484 },
485 Some(session_id),
486 Some(&context_id),
487 )
488 .await;
489 match result {
490 Ok(result)
491 if result.action == cookie_agent_protocol::ExtensionAllowBlockAction::Block =>
492 {
493 let reason = result
494 .reason
495 .unwrap_or_else(|| format!("session fork blocked by {plugin}"));
496 self.record_plugin_diagnostic(
497 session_id,
498 plugin,
499 cookie_agent_protocol::PluginDiagnosticKind::HookBlocked,
500 reason.clone(),
501 );
502 return Err(EngineError::SessionOperationBlocked(reason));
503 }
504 Ok(_) => {}
505 Err(error) => self.record_interception_error(session_id, plugin, error),
506 }
507 }
508 let forked = self.inner.store.fork(session_id, through_seq, origin)?;
509 let producer_state = self.goal_producer_projection(forked)?;
510 if producer_state.goal.is_some()
511 || producer_state
512 .messages
513 .iter()
514 .any(|message| !message.consumed && !message.discarded)
515 {
516 self.reconcile_producers(forked).await?;
517 }
518 Ok(SessionForkResult { session_id: forked })
519 }
520 pub fn set_permission_mode(
521 &self,
522 session_id: SessionId,
523 mode: PermissionMode,
524 ) -> Result<(), EngineError> {
525 self.ensure_session_owned(session_id)?;
526 let session = self.inner.store.get(session_id)?;
527 let root = root_id(&session.meta.origin, session_id);
528 self.inner
529 .approvals
530 .permission_modes
531 .lock()
532 .unwrap_or_else(|poisoned| poisoned.into_inner())
533 .insert(root, mode);
534 Ok(())
535 }
536 pub(crate) fn permission_mode(&self, session_id: SessionId) -> PermissionMode {
537 let root = self
538 .inner
539 .store
540 .get(session_id)
541 .map(|session| root_id(&session.meta.origin, session_id))
542 .unwrap_or(session_id);
543 self.inner
544 .approvals
545 .permission_modes
546 .lock()
547 .unwrap_or_else(|poisoned| poisoned.into_inner())
548 .get(&root)
549 .copied()
550 .unwrap_or_default()
551 }
552 pub async fn rename_session(
553 &self,
554 params: SessionRenameParams,
555 origin: EventOrigin,
556 ) -> Result<SessionRenameResult, EngineError> {
557 let session_id = params.session_id;
558 let reset = matches!(params.change, SessionRenameChange::Reset);
559 let mut result = self
560 .request(session_id, |reply| SessionCommand::Rename {
561 params,
562 origin,
563 reply,
564 })
565 .await?;
566 if reset {
567 self.generate_title_after_reset(session_id).await?;
568 result.session = self.inner.store.get(session_id)?.metadata();
569 }
570 Ok(result)
571 }
572}
573
574#[cfg(test)]
575fn validated_tree_usage_edge(
576 origin: &SessionOrigin,
577 child_session_id: SessionId,
578 known: &HashSet<(InvocationId, SessionId, SessionId)>,
579) -> Option<(SessionId, (SessionId, InvocationId))> {
580 match origin {
581 SessionOrigin::Delegated {
582 parent_session_id,
583 invocation_id,
584 ..
585 } if known.contains(&(*invocation_id, *parent_session_id, child_session_id)) => {
586 Some((*parent_session_id, (child_session_id, *invocation_id)))
587 }
588 _ => None,
589 }
590}
591
592fn merge_session_tree_usage(
593 session_id: SessionId,
594 children: &HashMap<SessionId, Vec<(SessionId, InvocationId)>>,
595 rollups: &HashMap<SessionId, UsageRollup>,
596 usage: &mut UsageRollup,
597) -> Result<u64, EngineError> {
598 let mut visited = HashSet::new();
599 let mut pending = vec![(session_id, None)];
600 let mut session_count = 0_u64;
601 while let Some((current_id, incoming_invocation)) = pending.pop() {
602 if !visited.insert(current_id) {
603 let invocation_id = incoming_invocation.expect("only descendants can repeat");
604 return Err(
605 crate::delegation_events::DelegationEventError::Corrupt(invocation_id).into(),
606 );
607 }
608 if let Some(rollup) = rollups.get(¤t_id) {
609 crate::usage::merge(usage, rollup);
610 session_count = session_count.saturating_add(1);
611 }
612 if let Some(descendants) = children.get(¤t_id) {
613 pending.extend(
614 descendants
615 .iter()
616 .map(|(child_id, invocation_id)| (*child_id, Some(*invocation_id))),
617 );
618 }
619 }
620 Ok(session_count)
621}
622
623#[cfg(test)]
624mod tests {
625 use super::*;
626
627 #[test]
628 fn malformed_usage_tree_cycle_is_reported_as_delegation_corruption() {
629 let first = SessionId::new_v7();
630 let second = SessionId::new_v7();
631 let first_to_second = InvocationId::new_v7();
632 let second_to_first = InvocationId::new_v7();
633 let first_run = cookie_agent_protocol::RunId::new_v7();
634 let second_run = cookie_agent_protocol::RunId::new_v7();
635 let first_origin = SessionOrigin::Delegated {
636 root_session_id: first,
637 parent_session_id: second,
638 parent_run_id: second_run,
639 parent_tool_call_id: cookie_agent_protocol::ToolCallId::new_v7(),
640 invocation_id: second_to_first,
641 depth: 2,
642 };
643 let second_origin = SessionOrigin::Delegated {
644 root_session_id: first,
645 parent_session_id: first,
646 parent_run_id: first_run,
647 parent_tool_call_id: cookie_agent_protocol::ToolCallId::new_v7(),
648 invocation_id: first_to_second,
649 depth: 1,
650 };
651 let known = HashSet::from([
652 (first_to_second, first, second),
653 (second_to_first, second, first),
654 ]);
655 let mut children: HashMap<SessionId, Vec<(SessionId, InvocationId)>> = HashMap::new();
656 for (session_id, origin) in [(first, first_origin), (second, second_origin)] {
657 let (parent_id, edge) = validated_tree_usage_edge(&origin, session_id, &known)
658 .expect("matching origin and reservation triple");
659 children.entry(parent_id).or_default().push(edge);
660 }
661 let rollups = HashMap::from([
662 (first, UsageRollup::default()),
663 (second, UsageRollup::default()),
664 ]);
665 let error =
666 merge_session_tree_usage(first, &children, &rollups, &mut UsageRollup::default())
667 .expect_err("cycle must fail");
668 assert!(matches!(
669 error,
670 EngineError::DelegationEvents(
671 crate::delegation_events::DelegationEventError::Corrupt(invocation_id)
672 ) if invocation_id == second_to_first
673 ));
674 }
675}