1#[cfg(loom)]
2use loom::sync::atomic::{AtomicU64, Ordering};
3use std::collections::{HashMap, HashSet, VecDeque};
4#[cfg(not(loom))]
5use std::sync::atomic::{AtomicU64, Ordering};
6use std::time::{Instant, SystemTime, UNIX_EPOCH};
7
8use chio_core::crypto::{canonical_json_bytes, sha256_hex};
9use chio_core::session::{
10 CreateElicitationOperation, NormalizedRoot, OperationContext, OperationKind,
11 OperationTerminalState, ProgressToken, RequestId, RequestOwnershipSnapshot, RootDefinition,
12 SessionAnchorReference, SessionAuthContext, SessionId,
13};
14#[cfg(not(loom))]
17use chio_core::session::{
18 CompletionResult, PromptDefinition, PromptResult, ResourceContent, ResourceDefinition,
19 ResourceTemplateDefinition,
20};
21use chio_core::{capability::token::CapabilityToken, AgentId};
22#[cfg(loom)]
23use loom::sync::{RwLock, RwLockReadGuard, RwLockWriteGuard};
24#[cfg(not(loom))]
25use std::sync::{RwLock, RwLockReadGuard, RwLockWriteGuard};
26
27#[cfg(not(loom))]
28use crate::{ToolCallResponse, ToolServerEvent};
29#[cfg(not(loom))]
31use chio_core::receipt::body::ChioReceipt;
32
33fn read_lock<T>(lock: &RwLock<T>) -> RwLockReadGuard<'_, T> {
34 match lock.read() {
35 Ok(value) => value,
36 Err(poisoned) => poisoned.into_inner(),
37 }
38}
39
40fn write_lock<T>(lock: &RwLock<T>) -> RwLockWriteGuard<'_, T> {
41 match lock.write() {
42 Ok(value) => value,
43 Err(poisoned) => poisoned.into_inner(),
44 }
45}
46
47#[derive(Debug)]
48struct SnapshotState<T> {
49 current: RwLock<T>,
50}
51
52impl<T> SnapshotState<T> {
53 fn new(initial: T) -> Self {
54 Self {
55 current: RwLock::new(initial),
56 }
57 }
58
59 fn with_current<R>(&self, read: impl FnOnce(&T) -> R) -> R {
60 let current = read_lock(&self.current);
61 read(¤t)
62 }
63
64 fn replace(&self, next: T) {
65 *write_lock(&self.current) = next;
66 }
67
68 fn replace_with<R>(&self, update: impl FnOnce(&T) -> (Option<T>, R)) -> R {
69 let mut current = write_lock(&self.current);
70 let (next, result) = update(¤t);
71 if let Some(next) = next {
72 *current = next;
73 }
74 result
75 }
76}
77
78impl<T: Clone> Clone for SnapshotState<T> {
79 fn clone(&self) -> Self {
80 Self::new(self.with_current(Clone::clone))
81 }
82}
83
84#[derive(Debug, Clone, Copy, PartialEq, Eq)]
86pub enum SessionState {
87 Initializing,
88 Ready,
89 Draining,
90 Closed,
91}
92
93impl SessionState {
94 pub fn as_str(self) -> &'static str {
95 match self {
96 Self::Initializing => "initializing",
97 Self::Ready => "ready",
98 Self::Draining => "draining",
99 Self::Closed => "closed",
100 }
101 }
102}
103
104#[derive(Debug, Clone, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
106pub struct PeerCapabilities {
107 pub supports_progress: bool,
108 pub supports_cancellation: bool,
109 pub supports_subscriptions: bool,
110 pub supports_chio_tool_streaming: bool,
111 pub supports_roots: bool,
112 pub roots_list_changed: bool,
113 pub supports_sampling: bool,
114 pub sampling_context: bool,
115 pub sampling_tools: bool,
116 pub supports_elicitation: bool,
117 pub elicitation_form: bool,
118 pub elicitation_url: bool,
119}
120
121#[derive(Debug, Clone)]
123pub struct InflightRequest {
124 pub request_id: RequestId,
125 pub parent_request_id: Option<RequestId>,
126 pub operation_kind: OperationKind,
127 pub session_anchor_id: String,
128 pub started_at: Instant,
129 pub progress_token: Option<ProgressToken>,
130 pub cancellation_requested: bool,
131 pub cancellable: bool,
132 pub pending_execution_nonce_id: Option<String>,
133}
134
135impl InflightRequest {
136 pub fn ownership(&self) -> RequestOwnershipSnapshot {
137 RequestOwnershipSnapshot::request_owned()
138 }
139}
140
141#[derive(Debug)]
143pub struct InflightRegistry {
144 requests: RwLock<HashMap<RequestId, InflightRequest>>,
145 active_count: AtomicU64,
146}
147
148impl Clone for InflightRegistry {
149 fn clone(&self) -> Self {
150 let requests = self.read_requests().clone();
151 Self {
152 active_count: AtomicU64::new(requests.len() as u64),
153 requests: RwLock::new(requests),
154 }
155 }
156}
157
158impl Default for InflightRegistry {
159 fn default() -> Self {
160 Self {
161 requests: RwLock::new(HashMap::new()),
162 active_count: AtomicU64::new(0),
163 }
164 }
165}
166
167impl InflightRegistry {
168 fn read_requests(&self) -> RwLockReadGuard<'_, HashMap<RequestId, InflightRequest>> {
169 read_lock(&self.requests)
170 }
171
172 fn write_requests(&self) -> RwLockWriteGuard<'_, HashMap<RequestId, InflightRequest>> {
173 write_lock(&self.requests)
174 }
175
176 pub fn track(
177 &self,
178 context: &OperationContext,
179 operation_kind: OperationKind,
180 session_anchor_id: &str,
181 cancellable: bool,
182 ) -> Result<(), SessionError> {
183 let mut requests = self.write_requests();
184 if requests.contains_key(&context.request_id) {
185 return Err(SessionError::DuplicateInflightRequest {
186 request_id: context.request_id.clone(),
187 });
188 }
189
190 requests.insert(
191 context.request_id.clone(),
192 InflightRequest {
193 request_id: context.request_id.clone(),
194 parent_request_id: context.parent_request_id.clone(),
195 operation_kind,
196 session_anchor_id: session_anchor_id.to_string(),
197 started_at: Instant::now(),
198 progress_token: context.progress_token.clone(),
199 cancellation_requested: false,
200 cancellable,
201 pending_execution_nonce_id: None,
202 },
203 );
204 self.active_count.fetch_add(1, Ordering::AcqRel);
205 Ok(())
206 }
207
208 fn track_locked(
209 &self,
210 requests: &mut HashMap<RequestId, InflightRequest>,
211 context: &OperationContext,
212 operation_kind: OperationKind,
213 session_anchor_id: &str,
214 cancellable: bool,
215 ) -> Result<(), SessionError> {
216 if requests.contains_key(&context.request_id) {
217 return Err(SessionError::DuplicateInflightRequest {
218 request_id: context.request_id.clone(),
219 });
220 }
221
222 requests.insert(
223 context.request_id.clone(),
224 InflightRequest {
225 request_id: context.request_id.clone(),
226 parent_request_id: context.parent_request_id.clone(),
227 operation_kind,
228 session_anchor_id: session_anchor_id.to_string(),
229 started_at: Instant::now(),
230 progress_token: context.progress_token.clone(),
231 cancellation_requested: false,
232 cancellable,
233 pending_execution_nonce_id: None,
234 },
235 );
236 self.active_count.fetch_add(1, Ordering::AcqRel);
237 Ok(())
238 }
239
240 pub fn complete(&self, request_id: &RequestId) -> Result<InflightRequest, SessionError> {
241 let mut requests = self.write_requests();
242 let completed =
243 requests
244 .remove(request_id)
245 .ok_or_else(|| SessionError::RequestNotInflight {
246 request_id: request_id.clone(),
247 })?;
248 if self
249 .active_count
250 .fetch_update(Ordering::AcqRel, Ordering::Acquire, |current| {
251 current.checked_sub(1)
252 })
253 .is_err()
254 {
255 self.active_count
256 .store(requests.len() as u64, Ordering::Release);
257 }
258 Ok(completed)
259 }
260
261 pub fn mark_cancellation_requested(&self, request_id: &RequestId) -> Result<(), SessionError> {
262 let mut requests = self.write_requests();
263 let request =
264 requests
265 .get_mut(request_id)
266 .ok_or_else(|| SessionError::RequestNotInflight {
267 request_id: request_id.clone(),
268 })?;
269
270 if !request.cancellable {
271 return Err(SessionError::RequestNotCancellable {
272 request_id: request_id.clone(),
273 });
274 }
275
276 request.cancellation_requested = true;
277 Ok(())
278 }
279
280 pub fn get(&self, request_id: &RequestId) -> Option<InflightRequest> {
281 self.read_requests().get(request_id).cloned()
282 }
283
284 pub fn mark_execution_nonce_pending(
285 &self,
286 request_id: &RequestId,
287 nonce_id: &str,
288 ) -> Result<(), SessionError> {
289 let mut requests = self.write_requests();
290 let request = requests.get_mut(request_id).ok_or_else(|| {
291 SessionError::ExecutionNonceRetryMismatch {
292 request_id: request_id.clone(),
293 }
294 })?;
295 if request.pending_execution_nonce_id.is_some() {
296 return Err(SessionError::ExecutionNonceRetryMismatch {
297 request_id: request_id.clone(),
298 });
299 }
300 request.pending_execution_nonce_id = Some(nonce_id.to_string());
301 Ok(())
302 }
303
304 pub fn len(&self) -> usize {
305 self.active_count.load(Ordering::Acquire) as usize
306 }
307
308 pub fn is_empty(&self) -> bool {
309 self.active_count.load(Ordering::Acquire) == 0
310 }
311
312 pub fn clear(&self) {
313 self.write_requests().clear();
314 self.active_count.store(0, Ordering::Release);
315 }
316}
317
318#[derive(Debug, Clone, PartialEq, Eq, Hash)]
319enum SubscriptionSubject {
320 Resource(String),
321}
322
323#[derive(Debug)]
325pub struct SubscriptionRegistry {
326 subscriptions: RwLock<HashSet<SubscriptionSubject>>,
327 subscription_count: AtomicU64,
328}
329
330impl Clone for SubscriptionRegistry {
331 fn clone(&self) -> Self {
332 let subscriptions = read_lock(&self.subscriptions).clone();
333 Self {
334 subscription_count: AtomicU64::new(subscriptions.len() as u64),
335 subscriptions: RwLock::new(subscriptions),
336 }
337 }
338}
339
340impl Default for SubscriptionRegistry {
341 fn default() -> Self {
342 Self {
343 subscriptions: RwLock::new(HashSet::new()),
344 subscription_count: AtomicU64::new(0),
345 }
346 }
347}
348
349#[derive(Debug, Clone, PartialEq, Eq)]
350pub enum LateSessionEvent {
351 ElicitationCompleted {
352 elicitation_id: String,
353 related_task_id: Option<String>,
354 },
355 ResourceUpdated {
356 uri: String,
357 },
358 ResourcesListChanged,
359 ToolsListChanged,
360 PromptsListChanged,
361}
362
363#[derive(Debug, Clone)]
364struct PendingUrlElicitation {
365 related_task_id: Option<String>,
366}
367
368impl SubscriptionRegistry {
369 pub fn subscribe_resource(&self, uri: impl Into<String>) {
370 let mut subscriptions = write_lock(&self.subscriptions);
371 subscriptions.insert(SubscriptionSubject::Resource(uri.into()));
372 self.subscription_count
373 .store(subscriptions.len() as u64, Ordering::Release);
374 }
375
376 pub fn unsubscribe_resource(&self, uri: &str) {
377 let mut subscriptions = write_lock(&self.subscriptions);
378 subscriptions.remove(&SubscriptionSubject::Resource(uri.to_string()));
379 self.subscription_count
380 .store(subscriptions.len() as u64, Ordering::Release);
381 }
382
383 pub fn contains_resource(&self, uri: &str) -> bool {
384 read_lock(&self.subscriptions).contains(&SubscriptionSubject::Resource(uri.to_string()))
385 }
386
387 pub fn len(&self) -> usize {
388 self.subscription_count.load(Ordering::Acquire) as usize
389 }
390
391 pub fn is_empty(&self) -> bool {
392 self.subscription_count.load(Ordering::Acquire) == 0
393 }
394
395 pub fn clear(&self) {
396 write_lock(&self.subscriptions).clear();
397 self.subscription_count.store(0, Ordering::Release);
398 }
399}
400
401const TERMINAL_HISTORY_LIMIT: usize = 256;
402
403#[derive(Debug, Clone)]
405struct TerminalRegistryInner {
406 states: HashMap<RequestId, OperationTerminalState>,
407 order: VecDeque<RequestId>,
408 limit: usize,
409}
410
411impl Default for TerminalRegistryInner {
412 fn default() -> Self {
413 Self {
414 states: HashMap::new(),
415 order: VecDeque::new(),
416 limit: TERMINAL_HISTORY_LIMIT,
417 }
418 }
419}
420
421#[derive(Debug)]
423pub struct TerminalRegistry {
424 inner: SnapshotState<TerminalRegistryInner>,
425}
426
427impl Clone for TerminalRegistry {
428 fn clone(&self) -> Self {
429 Self {
430 inner: self.inner.clone(),
431 }
432 }
433}
434
435impl Default for TerminalRegistry {
436 fn default() -> Self {
437 Self {
438 inner: SnapshotState::new(TerminalRegistryInner::default()),
439 }
440 }
441}
442
443impl TerminalRegistry {
444 pub fn record(&self, request_id: RequestId, state: OperationTerminalState) -> bool {
445 self.inner.replace_with(|current| {
446 if current.states.contains_key(&request_id) {
447 return (None, false);
448 }
449
450 let mut next = current.clone();
451 next.order.push_back(request_id.clone());
452 next.states.insert(request_id, state);
453
454 while next.order.len() > next.limit {
455 if let Some(oldest) = next.order.pop_front() {
456 next.states.remove(&oldest);
457 }
458 }
459 (Some(next), true)
460 })
461 }
462
463 pub fn get(&self, request_id: &RequestId) -> Option<OperationTerminalState> {
464 self.inner
465 .with_current(|current| current.states.get(request_id).cloned())
466 }
467
468 pub fn remove(&self, request_id: &RequestId) {
469 self.inner.replace_with(|current| {
470 if !current.states.contains_key(request_id) {
471 return (None, ());
472 }
473
474 let mut next = current.clone();
475 next.states.remove(request_id);
476 next.order.retain(|existing| existing != request_id);
477 (Some(next), ())
478 });
479 }
480
481 pub fn len(&self) -> usize {
482 self.inner.with_current(|current| current.states.len())
483 }
484
485 pub fn is_empty(&self) -> bool {
486 self.inner.with_current(|current| current.states.is_empty())
487 }
488}
489
490#[derive(Debug, thiserror::Error, PartialEq, Eq)]
492pub enum SessionError {
493 #[error("invalid session transition from {from} to {to}")]
494 InvalidTransition {
495 from: &'static str,
496 to: &'static str,
497 },
498
499 #[error("session {session_id} cannot handle {operation} while {state}")]
500 OperationNotAllowed {
501 session_id: SessionId,
502 operation: &'static str,
503 state: &'static str,
504 },
505
506 #[error("operation context session {actual} does not match runtime session {expected}")]
507 ContextSessionMismatch {
508 expected: SessionId,
509 actual: SessionId,
510 },
511
512 #[error("operation context agent {actual} does not match session agent {expected}")]
513 ContextAgentMismatch { expected: AgentId, actual: AgentId },
514
515 #[error("request {request_id} is already in flight")]
516 DuplicateInflightRequest { request_id: RequestId },
517
518 #[error("request {request_id} already has authoritative lineage in this session")]
519 DuplicateRequestLineage { request_id: RequestId },
520
521 #[error("request {request_id} is not in flight")]
522 RequestNotInflight { request_id: RequestId },
523
524 #[error(
525 "execution nonce retry for request {request_id} does not match a pending session preflight"
526 )]
527 ExecutionNonceRetryMismatch { request_id: RequestId },
528
529 #[error("request {request_id} is not cancellable")]
530 RequestNotCancellable { request_id: RequestId },
531
532 #[error("session {session_id} cannot close while {active_count} request(s) remain active")]
533 CloseRequiresDrain {
534 session_id: SessionId,
535 active_count: u64,
536 },
537
538 #[error("parent request {parent_request_id} is not in flight for child request {request_id}")]
539 ParentRequestNotInflight {
540 request_id: RequestId,
541 parent_request_id: RequestId,
542 },
543
544 #[error(
545 "parent request {parent_request_id} for child request {request_id} belongs to stale session anchor {parent_session_anchor_id}, current anchor is {current_session_anchor_id}"
546 )]
547 ParentRequestAnchorMismatch {
548 request_id: RequestId,
549 parent_request_id: RequestId,
550 parent_session_anchor_id: String,
551 current_session_anchor_id: String,
552 },
553}
554
555#[derive(Debug, PartialEq, Eq)]
556pub enum SessionPersistError<E> {
557 Session(SessionError),
558 Persist(E),
559}
560
561#[derive(Debug, Clone, PartialEq, Eq)]
562pub struct SessionAnchorState {
563 id: String,
564 auth_epoch: u64,
565 auth_context_hash: String,
566 issued_at: u64,
567}
568
569impl SessionAnchorState {
570 fn new(session_id: &SessionId, auth_context: &SessionAuthContext, auth_epoch: u64) -> Self {
571 let auth_context_hash = auth_context_hash(auth_context);
572 let hash_prefix = &auth_context_hash[..12.min(auth_context_hash.len())];
573 Self {
574 id: format!("{session_id}:anchor:{auth_epoch}:{hash_prefix}"),
575 auth_epoch,
576 auth_context_hash,
577 issued_at: current_unix_timestamp(),
578 }
579 }
580
581 pub fn id(&self) -> &str {
582 &self.id
583 }
584
585 pub fn auth_epoch(&self) -> u64 {
586 self.auth_epoch
587 }
588
589 pub fn auth_context_hash(&self) -> &str {
590 &self.auth_context_hash
591 }
592
593 pub fn issued_at(&self) -> u64 {
594 self.issued_at
595 }
596
597 pub fn reference(&self) -> SessionAnchorReference {
598 SessionAnchorReference::new(self.id.clone(), self.auth_context_hash.clone())
599 }
600}
601
602#[derive(Debug, Clone, PartialEq, Eq)]
603pub struct RequestLineageRecord {
604 pub request_id: RequestId,
605 pub session_anchor_id: String,
606 pub auth_epoch: u64,
607 pub parent_request_id: Option<RequestId>,
608 pub operation_kind: OperationKind,
609 pub started_at: u64,
610 pub terminal_state: Option<OperationTerminalState>,
611}
612
613#[derive(Debug, Clone)]
614struct SessionInner {
615 state: SessionState,
616}
617
618#[derive(Debug, Clone)]
619struct SessionAuthState {
620 auth_context: SessionAuthContext,
621 session_anchor: SessionAnchorState,
622}
623
624#[derive(Debug, Clone)]
625struct SessionRoots {
626 roots: Vec<RootDefinition>,
627 normalized_roots: Vec<NormalizedRoot>,
628}
629
630#[derive(Debug, Clone)]
631pub struct SessionAnchorSnapshot {
632 pub session_id: SessionId,
633 pub agent_id: AgentId,
634 pub auth_context: SessionAuthContext,
635 pub session_anchor: SessionAnchorState,
636}
637
638#[derive(Debug, Clone)]
639pub struct SessionRequestStart {
640 pub session: SessionAnchorSnapshot,
641 pub lineage: RequestLineageRecord,
642}
643
644#[derive(Debug)]
646pub struct Session {
647 id: SessionId,
648 agent_id: AgentId,
649 inner: RwLock<SessionInner>,
650 auth_state: SnapshotState<SessionAuthState>,
651 peer_capabilities: SnapshotState<PeerCapabilities>,
652 roots: SnapshotState<SessionRoots>,
653 issued_capabilities: Vec<CapabilityToken>,
654 inflight: InflightRegistry,
655 subscriptions: SubscriptionRegistry,
656 terminal: TerminalRegistry,
657 request_lineage: RwLock<HashMap<RequestId, RequestLineageRecord>>,
658 pending_url_elicitations: RwLock<HashMap<String, PendingUrlElicitation>>,
659 late_events: RwLock<VecDeque<LateSessionEvent>>,
660}
661
662fn operation_allowed_for_state(state: SessionState, operation: OperationKind) -> bool {
663 match state {
664 SessionState::Initializing => matches!(
665 operation,
666 OperationKind::ListCapabilities | OperationKind::Heartbeat
667 ),
668 SessionState::Ready => true,
669 SessionState::Draining => matches!(
670 operation,
671 OperationKind::ListCapabilities | OperationKind::Heartbeat
672 ),
673 SessionState::Closed => false,
674 }
675}
676
677fn validate_parent_request_lineage_locked(
678 request_id: &RequestId,
679 parent_request_id: &RequestId,
680 requests: &HashMap<RequestId, InflightRequest>,
681 request_lineage: &HashMap<RequestId, RequestLineageRecord>,
682 current_session_anchor_id: &str,
683) -> Result<RequestLineageRecord, SessionError> {
684 let Some(parent_inflight) = requests.get(parent_request_id) else {
685 return Err(SessionError::ParentRequestNotInflight {
686 request_id: request_id.clone(),
687 parent_request_id: parent_request_id.clone(),
688 });
689 };
690 let Some(parent_lineage) = request_lineage.get(parent_request_id).cloned() else {
691 return Err(SessionError::ParentRequestNotInflight {
692 request_id: request_id.clone(),
693 parent_request_id: parent_request_id.clone(),
694 });
695 };
696 if parent_lineage.session_anchor_id != current_session_anchor_id {
697 return Err(SessionError::ParentRequestAnchorMismatch {
698 request_id: request_id.clone(),
699 parent_request_id: parent_request_id.clone(),
700 parent_session_anchor_id: parent_inflight.session_anchor_id.clone(),
701 current_session_anchor_id: current_session_anchor_id.to_string(),
702 });
703 }
704 Ok(parent_lineage)
705}
706
707impl Clone for Session {
708 fn clone(&self) -> Self {
709 let inner = self.read_inner().clone();
710 Self {
711 id: self.id.clone(),
712 agent_id: self.agent_id.clone(),
713 inner: RwLock::new(inner),
714 auth_state: self.auth_state.clone(),
715 peer_capabilities: self.peer_capabilities.clone(),
716 roots: self.roots.clone(),
717 issued_capabilities: self.issued_capabilities.clone(),
718 inflight: self.inflight.clone(),
719 subscriptions: self.subscriptions.clone(),
720 terminal: self.terminal.clone(),
721 request_lineage: RwLock::new(self.read_request_lineage().clone()),
722 pending_url_elicitations: RwLock::new(self.read_pending_url_elicitations().clone()),
723 late_events: RwLock::new(self.read_late_events().clone()),
724 }
725 }
726}
727
728impl Session {
729 pub fn new(
730 id: SessionId,
731 agent_id: AgentId,
732 issued_capabilities: Vec<CapabilityToken>,
733 ) -> Self {
734 let auth_context = SessionAuthContext::in_process_anonymous();
735 let session_anchor = SessionAnchorState::new(&id, &auth_context, 0);
736 Self {
737 id,
738 agent_id,
739 inner: RwLock::new(SessionInner {
740 state: SessionState::Initializing,
741 }),
742 auth_state: SnapshotState::new(SessionAuthState {
743 auth_context,
744 session_anchor,
745 }),
746 peer_capabilities: SnapshotState::new(PeerCapabilities::default()),
747 roots: SnapshotState::new(SessionRoots {
748 roots: Vec::new(),
749 normalized_roots: Vec::new(),
750 }),
751 issued_capabilities,
752 inflight: InflightRegistry::default(),
753 subscriptions: SubscriptionRegistry::default(),
754 terminal: TerminalRegistry::default(),
755 request_lineage: RwLock::new(HashMap::new()),
756 pending_url_elicitations: RwLock::new(HashMap::new()),
757 late_events: RwLock::new(VecDeque::new()),
758 }
759 }
760
761 fn read_inner(&self) -> RwLockReadGuard<'_, SessionInner> {
762 read_lock(&self.inner)
763 }
764
765 fn write_inner(&self) -> RwLockWriteGuard<'_, SessionInner> {
766 write_lock(&self.inner)
767 }
768
769 fn read_request_lineage(
770 &self,
771 ) -> RwLockReadGuard<'_, HashMap<RequestId, RequestLineageRecord>> {
772 read_lock(&self.request_lineage)
773 }
774
775 fn read_pending_url_elicitations(
776 &self,
777 ) -> RwLockReadGuard<'_, HashMap<String, PendingUrlElicitation>> {
778 read_lock(&self.pending_url_elicitations)
779 }
780
781 fn write_pending_url_elicitations(
782 &self,
783 ) -> RwLockWriteGuard<'_, HashMap<String, PendingUrlElicitation>> {
784 write_lock(&self.pending_url_elicitations)
785 }
786
787 fn read_late_events(&self) -> RwLockReadGuard<'_, VecDeque<LateSessionEvent>> {
788 read_lock(&self.late_events)
789 }
790
791 fn write_late_events(&self) -> RwLockWriteGuard<'_, VecDeque<LateSessionEvent>> {
792 write_lock(&self.late_events)
793 }
794
795 fn write_request_lineage(
796 &self,
797 ) -> RwLockWriteGuard<'_, HashMap<RequestId, RequestLineageRecord>> {
798 write_lock(&self.request_lineage)
799 }
800
801 pub fn id(&self) -> &SessionId {
802 &self.id
803 }
804
805 pub fn agent_id(&self) -> &str {
806 &self.agent_id
807 }
808
809 pub fn state(&self) -> SessionState {
810 self.read_inner().state
811 }
812
813 pub fn auth_context(&self) -> SessionAuthContext {
814 self.auth_state
815 .with_current(|current| current.auth_context.clone())
816 }
817
818 pub fn session_anchor(&self) -> SessionAnchorState {
819 self.auth_state
820 .with_current(|current| current.session_anchor.clone())
821 }
822
823 pub fn session_anchor_snapshot(&self) -> SessionAnchorSnapshot {
824 self.auth_state
825 .with_current(|current| SessionAnchorSnapshot {
826 session_id: self.id.clone(),
827 agent_id: self.agent_id.clone(),
828 auth_context: current.auth_context.clone(),
829 session_anchor: current.session_anchor.clone(),
830 })
831 }
832
833 pub fn request_lineage(&self, request_id: &RequestId) -> Option<RequestLineageRecord> {
834 self.read_request_lineage().get(request_id).cloned()
835 }
836
837 pub fn peer_capabilities(&self) -> PeerCapabilities {
838 self.peer_capabilities.with_current(Clone::clone)
839 }
840
841 pub fn capabilities(&self) -> &[CapabilityToken] {
842 &self.issued_capabilities
843 }
844
845 pub fn roots(&self) -> Vec<RootDefinition> {
846 self.roots.with_current(|current| current.roots.clone())
847 }
848
849 pub fn normalized_roots(&self) -> Vec<NormalizedRoot> {
850 self.roots
851 .with_current(|current| current.normalized_roots.clone())
852 }
853
854 pub fn enforceable_filesystem_roots(&self) -> Vec<NormalizedRoot> {
855 self.roots.with_current(|current| {
856 current
857 .normalized_roots
858 .iter()
859 .filter(|root| root.is_enforceable_filesystem())
860 .cloned()
861 .collect()
862 })
863 }
864
865 pub fn inflight(&self) -> &InflightRegistry {
866 &self.inflight
867 }
868
869 pub fn mark_execution_nonce_pending(
870 &self,
871 request_id: &RequestId,
872 nonce_id: &str,
873 ) -> Result<(), SessionError> {
874 self.inflight
875 .mark_execution_nonce_pending(request_id, nonce_id)
876 }
877
878 pub fn validate_execution_nonce_retry(
879 &self,
880 context: &OperationContext,
881 operation_kind: OperationKind,
882 nonce_id: &str,
883 ) -> Result<(), SessionError> {
884 self.validate_context(context)?;
885 self.ensure_operation_allowed(operation_kind)?;
886 let current_anchor = self.session_anchor();
887 let request = self.inflight.get(&context.request_id).ok_or_else(|| {
888 SessionError::ExecutionNonceRetryMismatch {
889 request_id: context.request_id.clone(),
890 }
891 })?;
892 if request.operation_kind != operation_kind
893 || request.session_anchor_id != current_anchor.id()
894 || request.parent_request_id != context.parent_request_id
895 || request.progress_token != context.progress_token
896 || request.pending_execution_nonce_id.as_deref() != Some(nonce_id)
897 {
898 return Err(SessionError::ExecutionNonceRetryMismatch {
899 request_id: context.request_id.clone(),
900 });
901 }
902 Ok(())
903 }
904
905 pub fn subscriptions(&self) -> &SubscriptionRegistry {
906 &self.subscriptions
907 }
908
909 pub fn terminal(&self) -> TerminalRegistry {
910 self.terminal.clone()
911 }
912
913 pub fn register_pending_url_elicitation(
914 &self,
915 elicitation_id: impl Into<String>,
916 related_task_id: Option<String>,
917 ) {
918 self.write_pending_url_elicitations().insert(
919 elicitation_id.into(),
920 PendingUrlElicitation { related_task_id },
921 );
922 }
923
924 pub fn register_required_url_elicitations(
925 &self,
926 elicitations: &[CreateElicitationOperation],
927 related_task_id: Option<&str>,
928 ) {
929 for elicitation in elicitations {
930 let CreateElicitationOperation::Url { elicitation_id, .. } = elicitation else {
931 continue;
932 };
933 self.register_pending_url_elicitation(
934 elicitation_id.clone(),
935 related_task_id.map(ToString::to_string),
936 );
937 }
938 }
939
940 pub fn queue_late_event(&self, event: LateSessionEvent) {
941 self.write_late_events().push_back(event);
942 }
943
944 pub fn take_late_events(&self) -> Vec<LateSessionEvent> {
945 self.write_late_events().drain(..).collect()
946 }
947
948 #[cfg(not(loom))]
949 pub fn queue_tool_server_event(&self, event: ToolServerEvent) {
950 match event {
951 ToolServerEvent::ElicitationCompleted { elicitation_id } => {
952 let Some(pending) = self
953 .write_pending_url_elicitations()
954 .remove(&elicitation_id)
955 else {
956 return;
957 };
958 self.queue_late_event(LateSessionEvent::ElicitationCompleted {
959 elicitation_id,
960 related_task_id: pending.related_task_id,
961 });
962 }
963 ToolServerEvent::ResourceUpdated { uri } => {
964 if self.is_resource_subscribed(&uri) {
965 self.queue_late_event(LateSessionEvent::ResourceUpdated { uri });
966 }
967 }
968 ToolServerEvent::ResourcesListChanged => {
969 self.queue_late_event(LateSessionEvent::ResourcesListChanged);
970 }
971 ToolServerEvent::ToolsListChanged => {
972 self.queue_late_event(LateSessionEvent::ToolsListChanged);
973 }
974 ToolServerEvent::PromptsListChanged => {
975 self.queue_late_event(LateSessionEvent::PromptsListChanged);
976 }
977 }
978 }
979
980 pub fn queue_elicitation_completion(&self, elicitation_id: &str) {
981 let Some(pending) = self.write_pending_url_elicitations().remove(elicitation_id) else {
982 return;
983 };
984 self.queue_late_event(LateSessionEvent::ElicitationCompleted {
985 elicitation_id: elicitation_id.to_string(),
986 related_task_id: pending.related_task_id,
987 });
988 }
989
990 pub fn subscribe_resource(&self, uri: impl Into<String>) {
991 self.subscriptions.subscribe_resource(uri);
992 }
993
994 pub fn unsubscribe_resource(&self, uri: &str) {
995 self.subscriptions.unsubscribe_resource(uri);
996 }
997
998 pub fn is_resource_subscribed(&self, uri: &str) -> bool {
999 self.subscriptions.contains_resource(uri)
1000 }
1001
1002 pub fn set_auth_context(
1003 &self,
1004 auth_context: SessionAuthContext,
1005 ) -> (bool, SessionAnchorSnapshot, Option<String>) {
1006 self.auth_state.replace_with(|current| {
1007 let rotated = current.auth_context != auth_context;
1008 if rotated {
1009 let previous_anchor_id = current.session_anchor.id().to_string();
1010 let next_epoch = current.session_anchor.auth_epoch.saturating_add(1);
1011 let session_anchor = SessionAnchorState::new(&self.id, &auth_context, next_epoch);
1012 let snapshot = SessionAnchorSnapshot {
1013 session_id: self.id.clone(),
1014 agent_id: self.agent_id.clone(),
1015 auth_context: auth_context.clone(),
1016 session_anchor: session_anchor.clone(),
1017 };
1018 (
1019 Some(SessionAuthState {
1020 auth_context,
1021 session_anchor,
1022 }),
1023 (true, snapshot, Some(previous_anchor_id)),
1024 )
1025 } else {
1026 (
1027 None,
1028 (
1029 false,
1030 SessionAnchorSnapshot {
1031 session_id: self.id.clone(),
1032 agent_id: self.agent_id.clone(),
1033 auth_context: current.auth_context.clone(),
1034 session_anchor: current.session_anchor.clone(),
1035 },
1036 None,
1037 ),
1038 )
1039 }
1040 })
1041 }
1042
1043 pub fn set_auth_context_persisted<E>(
1044 &self,
1045 auth_context: SessionAuthContext,
1046 persist: impl FnOnce(&SessionAnchorSnapshot, Option<&str>) -> Result<(), E>,
1047 ) -> Result<(), SessionPersistError<E>> {
1048 let state_guard = self.write_inner();
1049 if state_guard.state == SessionState::Closed {
1050 return Err(SessionPersistError::Session(
1051 SessionError::OperationNotAllowed {
1052 session_id: self.id.clone(),
1053 operation: "set_auth_context",
1054 state: state_guard.state.as_str(),
1055 },
1056 ));
1057 }
1058
1059 self.auth_state.replace_with(|current| {
1060 let rotated = current.auth_context != auth_context;
1061 let (next, snapshot, supersedes_anchor_id) = if rotated {
1062 let previous_anchor_id = current.session_anchor.id().to_string();
1063 let next_epoch = current.session_anchor.auth_epoch.saturating_add(1);
1064 let session_anchor = SessionAnchorState::new(&self.id, &auth_context, next_epoch);
1065 let snapshot = SessionAnchorSnapshot {
1066 session_id: self.id.clone(),
1067 agent_id: self.agent_id.clone(),
1068 auth_context: auth_context.clone(),
1069 session_anchor: session_anchor.clone(),
1070 };
1071 (
1072 Some(SessionAuthState {
1073 auth_context,
1074 session_anchor,
1075 }),
1076 snapshot,
1077 Some(previous_anchor_id),
1078 )
1079 } else {
1080 (
1081 None,
1082 SessionAnchorSnapshot {
1083 session_id: self.id.clone(),
1084 agent_id: self.agent_id.clone(),
1085 auth_context: current.auth_context.clone(),
1086 session_anchor: current.session_anchor.clone(),
1087 },
1088 None,
1089 )
1090 };
1091
1092 let result = persist(&snapshot, supersedes_anchor_id.as_deref());
1093 match result {
1094 Ok(()) => (next, Ok(())),
1095 Err(error) => (None, Err(SessionPersistError::Persist(error))),
1096 }
1097 })
1098 }
1099
1100 pub fn set_peer_capabilities(&self, peer_capabilities: PeerCapabilities) {
1101 self.peer_capabilities.replace(peer_capabilities);
1102 }
1103
1104 pub fn replace_roots(&self, roots: Vec<RootDefinition>) {
1105 let normalized_roots = roots
1106 .iter()
1107 .map(RootDefinition::normalize_for_runtime)
1108 .collect();
1109 self.roots.replace(SessionRoots {
1110 roots,
1111 normalized_roots,
1112 });
1113 }
1114
1115 pub fn activate(&self) -> Result<(), SessionError> {
1116 self.transition(SessionState::Ready)
1117 }
1118
1119 pub fn begin_draining(&self) -> Result<(), SessionError> {
1120 self.transition(SessionState::Draining)
1121 }
1122
1123 pub fn close(&self) -> Result<(), SessionError> {
1124 {
1125 let mut inner = self.write_inner();
1126 if inner.state == SessionState::Closed {
1127 return Ok(());
1128 }
1129
1130 let active_count = self.inflight.len() as u64;
1131 if active_count > 0 {
1132 if inner.state != SessionState::Closed {
1133 inner.state = SessionState::Draining;
1134 }
1135 return Err(SessionError::CloseRequiresDrain {
1136 session_id: self.id.clone(),
1137 active_count,
1138 });
1139 }
1140
1141 inner.state = SessionState::Closed;
1142 }
1143
1144 self.inflight.clear();
1145 self.subscriptions.clear();
1146 self.auth_state.replace_with(|current| {
1147 let auth_context = SessionAuthContext::in_process_anonymous();
1148 let next_epoch = current.session_anchor.auth_epoch.saturating_add(1);
1149 let session_anchor = SessionAnchorState::new(&self.id, &auth_context, next_epoch);
1150 (
1151 Some(SessionAuthState {
1152 auth_context,
1153 session_anchor,
1154 }),
1155 (),
1156 )
1157 });
1158 self.roots.replace(SessionRoots {
1159 roots: Vec::new(),
1160 normalized_roots: Vec::new(),
1161 });
1162 self.write_pending_url_elicitations().clear();
1163 self.write_late_events().clear();
1164 Ok(())
1165 }
1166
1167 pub fn close_persisted<E>(
1168 &self,
1169 persist: impl FnOnce(&SessionAnchorSnapshot, Option<&str>) -> Result<(), E>,
1170 ) -> Result<(), SessionPersistError<E>> {
1171 let mut inner = self.write_inner();
1172 if inner.state == SessionState::Closed {
1173 return Ok(());
1174 }
1175
1176 let active_count = self.inflight.len() as u64;
1177 if active_count > 0 {
1178 if inner.state != SessionState::Closed {
1179 inner.state = SessionState::Draining;
1180 }
1181 return Err(SessionPersistError::Session(
1182 SessionError::CloseRequiresDrain {
1183 session_id: self.id.clone(),
1184 active_count,
1185 },
1186 ));
1187 }
1188
1189 self.auth_state.replace_with(|current| {
1190 let auth_context = SessionAuthContext::in_process_anonymous();
1191 let previous_anchor_id = current.session_anchor.id().to_string();
1192 let next_epoch = current.session_anchor.auth_epoch.saturating_add(1);
1193 let session_anchor = SessionAnchorState::new(&self.id, &auth_context, next_epoch);
1194 let snapshot = SessionAnchorSnapshot {
1195 session_id: self.id.clone(),
1196 agent_id: self.agent_id.clone(),
1197 auth_context: auth_context.clone(),
1198 session_anchor: session_anchor.clone(),
1199 };
1200 let result = persist(&snapshot, Some(previous_anchor_id.as_str()));
1201 match result {
1202 Ok(()) => (
1203 Some(SessionAuthState {
1204 auth_context,
1205 session_anchor,
1206 }),
1207 Ok(()),
1208 ),
1209 Err(error) => (None, Err(SessionPersistError::Persist(error))),
1210 }
1211 })?;
1212
1213 inner.state = SessionState::Closed;
1214 drop(inner);
1215
1216 self.inflight.clear();
1217 self.subscriptions.clear();
1218 self.roots.replace(SessionRoots {
1219 roots: Vec::new(),
1220 normalized_roots: Vec::new(),
1221 });
1222 self.write_pending_url_elicitations().clear();
1223 self.write_late_events().clear();
1224 Ok(())
1225 }
1226
1227 pub fn ensure_operation_allowed(&self, operation: OperationKind) -> Result<(), SessionError> {
1228 let state = self.state();
1229 let allowed = operation_allowed_for_state(state, operation);
1230
1231 if allowed {
1232 Ok(())
1233 } else {
1234 Err(SessionError::OperationNotAllowed {
1235 session_id: self.id.clone(),
1236 operation: operation.as_str(),
1237 state: state.as_str(),
1238 })
1239 }
1240 }
1241
1242 pub fn track_request(
1243 &self,
1244 context: &OperationContext,
1245 operation_kind: OperationKind,
1246 cancellable: bool,
1247 ) -> Result<SessionRequestStart, SessionError> {
1248 self.validate_context(context)?;
1249
1250 let state_guard = self.read_inner();
1251 let state = state_guard.state;
1252 if !operation_allowed_for_state(state, operation_kind) {
1253 return Err(SessionError::OperationNotAllowed {
1254 session_id: self.id.clone(),
1255 operation: operation_kind.as_str(),
1256 state: state.as_str(),
1257 });
1258 }
1259
1260 let start = self.auth_state.with_current(|auth_state| {
1261 let session_snapshot = SessionAnchorSnapshot {
1262 session_id: self.id.clone(),
1263 agent_id: self.agent_id.clone(),
1264 auth_context: auth_state.auth_context.clone(),
1265 session_anchor: auth_state.session_anchor.clone(),
1266 };
1267 let mut requests = self.inflight.write_requests();
1268 let mut request_lineage = self.write_request_lineage();
1269 if requests.contains_key(&context.request_id) {
1270 return Err(SessionError::DuplicateInflightRequest {
1271 request_id: context.request_id.clone(),
1272 });
1273 }
1274 if let Some(parent_request_id) = &context.parent_request_id {
1275 validate_parent_request_lineage_locked(
1276 &context.request_id,
1277 parent_request_id,
1278 &requests,
1279 &request_lineage,
1280 auth_state.session_anchor.id(),
1281 )?;
1282 }
1283 if request_lineage.contains_key(&context.request_id) {
1284 return Err(SessionError::DuplicateRequestLineage {
1285 request_id: context.request_id.clone(),
1286 });
1287 }
1288 self.inflight.track_locked(
1289 &mut requests,
1290 context,
1291 operation_kind,
1292 auth_state.session_anchor.id(),
1293 cancellable,
1294 )?;
1295 let lineage = RequestLineageRecord {
1296 request_id: context.request_id.clone(),
1297 session_anchor_id: auth_state.session_anchor.id().to_string(),
1298 auth_epoch: auth_state.session_anchor.auth_epoch(),
1299 parent_request_id: context.parent_request_id.clone(),
1300 operation_kind,
1301 started_at: current_unix_timestamp(),
1302 terminal_state: None,
1303 };
1304 request_lineage.insert(context.request_id.clone(), lineage.clone());
1305 Ok(SessionRequestStart {
1306 session: session_snapshot,
1307 lineage,
1308 })
1309 })?;
1310 drop(state_guard);
1311 Ok(start)
1312 }
1313
1314 pub fn complete_request(
1315 &self,
1316 request_id: &RequestId,
1317 ) -> Result<InflightRequest, SessionError> {
1318 self.complete_request_with_terminal_state(request_id, OperationTerminalState::Completed)
1319 }
1320
1321 pub fn complete_request_with_terminal_state(
1322 &self,
1323 request_id: &RequestId,
1324 terminal_state: OperationTerminalState,
1325 ) -> Result<InflightRequest, SessionError> {
1326 let inflight = self.inflight.complete(request_id)?;
1327 self.mark_request_terminal(request_id, terminal_state);
1328 Ok(inflight)
1329 }
1330
1331 pub fn discard_unpersisted_request_start(&self, request_id: &RequestId) {
1332 let _ = self.inflight.complete(request_id);
1333 self.write_request_lineage().remove(request_id);
1334 self.terminal.remove(request_id);
1335 }
1336
1337 fn mark_request_terminal(
1338 &self,
1339 request_id: &RequestId,
1340 terminal_state: OperationTerminalState,
1341 ) {
1342 let recorded = self
1343 .terminal
1344 .record(request_id.clone(), terminal_state.clone());
1345 if recorded {
1346 if let Some(lineage) = self.write_request_lineage().get_mut(request_id) {
1347 lineage.terminal_state = Some(terminal_state);
1348 }
1349 }
1350 }
1351
1352 pub fn request_cancellation(&self, request_id: &RequestId) -> Result<(), SessionError> {
1353 self.inflight.mark_cancellation_requested(request_id)
1354 }
1355
1356 pub fn validate_parent_request_lineage(
1357 &self,
1358 request_id: &RequestId,
1359 parent_request_id: &RequestId,
1360 ) -> Result<RequestLineageRecord, SessionError> {
1361 self.auth_state.with_current(|auth_state| {
1362 let requests = self.inflight.read_requests();
1363 let request_lineage = self.read_request_lineage();
1364 validate_parent_request_lineage_locked(
1365 request_id,
1366 parent_request_id,
1367 &requests,
1368 &request_lineage,
1369 auth_state.session_anchor.id(),
1370 )
1371 })
1372 }
1373
1374 fn transition(&self, next: SessionState) -> Result<(), SessionError> {
1375 let mut inner = self.write_inner();
1376 let valid = match (inner.state, next) {
1377 (SessionState::Initializing, SessionState::Ready)
1378 | (SessionState::Initializing, SessionState::Closed)
1379 | (SessionState::Ready, SessionState::Draining)
1380 | (SessionState::Ready, SessionState::Closed)
1381 | (SessionState::Draining, SessionState::Closed) => true,
1382 _ if inner.state == next => true,
1383 _ => false,
1384 };
1385
1386 if !valid {
1387 return Err(SessionError::InvalidTransition {
1388 from: inner.state.as_str(),
1389 to: next.as_str(),
1390 });
1391 }
1392
1393 inner.state = next;
1394 Ok(())
1395 }
1396
1397 pub fn validate_context(&self, context: &OperationContext) -> Result<(), SessionError> {
1398 if context.session_id != self.id {
1399 return Err(SessionError::ContextSessionMismatch {
1400 expected: self.id.clone(),
1401 actual: context.session_id.clone(),
1402 });
1403 }
1404
1405 if context.agent_id != self.agent_id {
1406 return Err(SessionError::ContextAgentMismatch {
1407 expected: self.agent_id.clone(),
1408 actual: context.agent_id.clone(),
1409 });
1410 }
1411
1412 Ok(())
1413 }
1414}
1415
1416fn current_unix_timestamp() -> u64 {
1417 SystemTime::now()
1418 .duration_since(UNIX_EPOCH)
1419 .map(|duration| duration.as_secs())
1420 .unwrap_or(0)
1421}
1422
1423fn auth_context_hash(auth_context: &SessionAuthContext) -> String {
1424 canonical_json_bytes(auth_context)
1425 .map(|bytes| sha256_hex(&bytes))
1426 .unwrap_or_else(|_| "session-auth-context-hash-unavailable".to_string())
1427}
1428
1429#[cfg(not(loom))]
1431#[derive(Debug)]
1432pub enum SessionOperationResponse {
1433 ToolCall(ToolCallResponse),
1434 RootList {
1435 roots: Vec<RootDefinition>,
1436 },
1437 ResourceList {
1438 resources: Vec<ResourceDefinition>,
1439 },
1440 ResourceRead {
1441 contents: Vec<ResourceContent>,
1442 },
1443 ResourceReadDenied {
1444 receipt: ChioReceipt,
1445 },
1446 ResourceTemplateList {
1447 templates: Vec<ResourceTemplateDefinition>,
1448 },
1449 PromptList {
1450 prompts: Vec<PromptDefinition>,
1451 },
1452 PromptGet {
1453 prompt: PromptResult,
1454 },
1455 Completion {
1456 completion: CompletionResult,
1457 },
1458 CapabilityList {
1459 capabilities: Vec<CapabilityToken>,
1460 },
1461 Heartbeat,
1462}
1463
1464#[cfg(test)]
1465#[allow(clippy::expect_used, clippy::unwrap_used)]
1466mod tests;