1use super::{model_context::ModelContextResetReason, Session, SessionKind};
4
5#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
9#[serde(rename_all = "lowercase")]
10pub enum RootThinkingMode {
11 Standard,
12 Ultra,
13}
14
15#[derive(Debug, thiserror::Error, PartialEq, Eq)]
16#[error("thinking_mode and root orchestration enabled selection conflict")]
17pub struct RootThinkingModeConflict;
18
19impl RootThinkingMode {
20 pub fn deserialize_selection<'de, D>(deserializer: D) -> Result<Option<Self>, D::Error>
22 where
23 D: serde::Deserializer<'de>,
24 {
25 <Self as serde::Deserialize>::deserialize(deserializer).map(Some)
26 }
27
28 pub fn from_enabled(enabled: bool) -> Self {
29 if enabled {
30 Self::Ultra
31 } else {
32 Self::Standard
33 }
34 }
35
36 pub fn enabled(self) -> bool {
37 self == Self::Ultra
38 }
39
40 pub fn resolve_selection(
42 mode: Option<Self>,
43 enabled: Option<bool>,
44 ) -> Result<Option<bool>, RootThinkingModeConflict> {
45 if let (Some(mode), Some(enabled)) = (mode, enabled) {
46 if mode.enabled() != enabled {
47 return Err(RootThinkingModeConflict);
48 }
49 }
50 Ok(mode.map(Self::enabled).or(enabled))
51 }
52}
53
54pub const ROOT_ORCHESTRATION_TOOLS: [&str; 16] = [
58 "SubAgent",
59 "Plan",
60 "Task",
61 "session_history_current",
62 "session_message",
63 "Read",
64 "Grep",
65 "Glob",
66 "GetFileInfo",
67 "ViewImage",
68 "work_overview",
69 "work_search",
70 "work_inspect",
71 "work_changes",
72 "work_update",
73 "work_dispatch",
74];
75
76pub fn orchestration_only_allows_execution_name(execution_name: &str) -> bool {
77 ROOT_ORCHESTRATION_TOOLS.contains(&execution_name)
78}
79
80pub const SUPERVISOR_ORCHESTRATION_TOOLS: [&str; 2] = ["session_control", "session_history"];
83
84pub fn orchestration_only_allows_execution_name_for_supervisor(
85 execution_name: &str,
86 executing_supervisor: bool,
87) -> bool {
88 orchestration_only_allows_execution_name(execution_name)
89 || (executing_supervisor && SUPERVISOR_ORCHESTRATION_TOOLS.contains(&execution_name))
90}
91
92#[derive(Debug, thiserror::Error, PartialEq, Eq)]
93pub enum RootToolAuthorityError {
94 #[error("only a Root Session may select orchestration-only tool authority")]
95 NotRoot,
96 #[error("Root tool authority revision cannot advance")]
97 RevisionOverflow,
98 #[error("Root tool authority snapshot is stale or inconsistent")]
99 StaleSnapshot,
100 #[error("orchestration-only Root cannot enter while legacy Plan mode is active")]
101 LegacyPlanActive,
102 #[error("orchestration-only Root cannot use a selected Skill or Workflow")]
103 WorkflowSelected,
104}
105
106impl Session {
107 pub fn root_thinking_mode(&self) -> RootThinkingMode {
108 RootThinkingMode::from_enabled(self.root_orchestration_only_enabled())
109 }
110
111 pub fn root_orchestration_only_enabled(&self) -> bool {
112 self.kind == SessionKind::Root
113 && self.parent_session_id.is_none()
114 && self.root_orchestration_only
115 }
116
117 pub fn set_root_orchestration_only(
119 &mut self,
120 enabled: bool,
121 ) -> Result<(), RootToolAuthorityError> {
122 if self.kind != SessionKind::Root || self.parent_session_id.is_some() {
123 return Err(RootToolAuthorityError::NotRoot);
124 }
125 if enabled {
126 self.validate_root_orchestration_compatibility()?;
127 }
128 if self.root_orchestration_only == enabled {
129 return Ok(());
130 }
131 let next = self
132 .root_tool_authority_revision
133 .checked_add(1)
134 .ok_or(RootToolAuthorityError::RevisionOverflow)?;
135 self.root_orchestration_only = enabled;
136 self.root_tool_authority_revision = next;
137 if self.model_context_state.is_some() {
138 self.reset_model_context_epoch(ModelContextResetReason::CacheScopeChanged);
139 }
140 Ok(())
141 }
142
143 pub fn validate_root_orchestration_compatibility(&self) -> Result<(), RootToolAuthorityError> {
146 if self
147 .agent_runtime_state
148 .as_ref()
149 .is_some_and(|state| state.plan_mode.is_some())
150 {
151 return Err(RootToolAuthorityError::LegacyPlanActive);
152 }
153 if self.selected_skill_ids().is_some_and(|ids| !ids.is_empty()) {
154 return Err(RootToolAuthorityError::WorkflowSelected);
155 }
156 Ok(())
157 }
158
159 pub fn model_orchestration_only_enabled(&self) -> bool {
163 self.root_orchestration_only_enabled() || self.supervisor_incarnation_id().is_some()
164 }
165
166 pub fn allows_model_tool_execution(&self, execution_name: &str) -> bool {
168 !self.model_orchestration_only_enabled()
169 || orchestration_only_allows_execution_name_for_supervisor(
170 execution_name,
171 self.supervisor_incarnation_id().is_some(),
172 )
173 }
174
175 pub fn adopt_root_tool_authority_from(
178 &mut self,
179 latest: &Session,
180 ) -> Result<(), RootToolAuthorityError> {
181 fn root_id(session: &Session) -> &str {
185 if session.kind == SessionKind::Root && session.root_session_id.is_empty() {
186 session.id.as_str()
187 } else {
188 session.root_session_id.as_str()
189 }
190 }
191 if self.id != latest.id
192 || self.kind != latest.kind
193 || self.created_at != latest.created_at
194 || self.authority_identity != latest.authority_identity
195 || self.parent_session_id != latest.parent_session_id
196 || root_id(self) != root_id(latest)
197 || self.spawn_depth != latest.spawn_depth
198 || (self.root_orchestration_only && self.root_tool_authority_revision == 0)
199 || (latest.root_orchestration_only && latest.root_tool_authority_revision == 0)
200 || latest.root_tool_authority_revision < self.root_tool_authority_revision
201 || (latest.root_tool_authority_revision == self.root_tool_authority_revision
202 && latest.root_orchestration_only != self.root_orchestration_only)
203 || latest.root_mode_transition_epoch < self.root_mode_transition_epoch
204 || (latest.root_mode_transition_epoch == self.root_mode_transition_epoch
205 && latest.root_mode_operations != self.root_mode_operations)
206 {
207 return Err(RootToolAuthorityError::StaleSnapshot);
208 }
209 let selections = latest.root_tool_authority_revision - self.root_tool_authority_revision;
210 self.root_orchestration_only = latest.root_orchestration_only;
211 self.root_tool_authority_revision = latest.root_tool_authority_revision;
212 self.root_mode_transition_epoch = latest.root_mode_transition_epoch;
213 self.root_mode_operations = latest.root_mode_operations.clone();
214 if selections > 0 {
215 self.model_context_state = latest.model_context_state.clone();
219 self.provider_transcript.invalidate_repeated(
220 super::provider_transcript::ProviderTranscriptResetReason::CacheScopeChanged,
221 selections,
222 );
223 }
224 Ok(())
225 }
226}
227
228#[cfg(test)]
229mod tests {
230 use super::*;
231 use crate::session::{AgentRuntimeState, ModelContextState, PlanModeState, PlanModeStatus};
232
233 #[test]
234 fn supervisor_delegation_first_does_not_depend_on_ultra_or_leak_to_children() {
235 use crate::{SessionAuthorityIdentity, DEFAULT_SUPERVISOR_SESSION_ID};
236 let mut supervisor = Session::new(DEFAULT_SUPERVISOR_SESSION_ID, "model");
237 supervisor.authority_identity = SessionAuthorityIdentity::Supervisor {
238 incarnation_id: uuid::Uuid::new_v4(),
239 };
240 for ultra in [false, true, false] {
241 supervisor.set_root_orchestration_only(ultra).unwrap();
242 let restored: Session =
243 serde_json::from_value(serde_json::to_value(&supervisor).unwrap()).unwrap();
244 for name in ROOT_ORCHESTRATION_TOOLS
245 .into_iter()
246 .chain(SUPERVISOR_ORCHESTRATION_TOOLS)
247 {
248 assert!(
249 restored.allows_model_tool_execution(name),
250 "{ultra}: {name}"
251 );
252 }
253 for name in [
254 "exec_command",
255 "write_stdin",
256 "Bash",
257 "Edit",
258 "Write",
259 "apply_patch",
260 "load_skill",
261 "workflow_run",
262 "mcp__vendor__read",
263 "default::Read",
264 ] {
265 assert!(
266 !restored.allows_model_tool_execution(name),
267 "Supervisor {ultra}: {name}"
268 );
269 }
270 let child = Session::new_child_of("worker", &restored, "model", "worker");
271 assert!(child.authority_identity.is_ordinary());
272 assert!(child.allows_model_tool_execution("Edit"));
273 assert!(child.allows_model_tool_execution("exec_command"));
274 }
275 let mut ordinary = Session::new(DEFAULT_SUPERVISOR_SESSION_ID, "model");
276 ordinary.metadata.insert("role".into(), "supervisor".into());
277 assert!(ordinary.allows_model_tool_execution("Edit"));
278 ordinary.set_root_orchestration_only(true).unwrap();
279 assert!(!ordinary.allows_model_tool_execution("session_control"));
280 assert!(!ordinary.allows_model_tool_execution("session_history"));
281 }
282
283 #[test]
284 fn trusted_supervisor_session_management_survives_thinking_mode_selection() {
285 use crate::{SessionAuthorityIdentity, DEFAULT_SUPERVISOR_SESSION_ID};
286 let mut supervisor = Session::new(DEFAULT_SUPERVISOR_SESSION_ID, "model");
287 supervisor.authority_identity = SessionAuthorityIdentity::Supervisor {
288 incarnation_id: uuid::Uuid::new_v4(),
289 };
290 for ultra in [false, true] {
291 supervisor.set_root_orchestration_only(ultra).unwrap();
292 let restored: Session =
293 serde_json::from_value(serde_json::to_value(&supervisor).unwrap()).unwrap();
294 for name in ["session_control", "session_history"] {
295 assert!(
296 restored.allows_model_tool_execution(name),
297 "trusted Supervisor lost {name} in {:?}",
298 restored.root_thinking_mode()
299 );
300 }
301 }
302 }
303
304 #[test]
305 fn ultra_management_exception_requires_canonical_supervisor_and_exact_tool() {
306 use crate::{SessionAuthorityIdentity, DEFAULT_SUPERVISOR_SESSION_ID};
307 let mut supervisor = Session::new(DEFAULT_SUPERVISOR_SESSION_ID, "model");
308 supervisor.authority_identity = SessionAuthorityIdentity::Supervisor {
309 incarnation_id: uuid::Uuid::new_v4(),
310 };
311 supervisor.set_root_orchestration_only(true).unwrap();
312 for mutation in [
313 |s: &mut Session| s.authority_identity = SessionAuthorityIdentity::Ordinary,
314 |s: &mut Session| s.id = "other".into(),
315 |s: &mut Session| s.root_session_id = "other".into(),
316 |s: &mut Session| s.spawn_depth = 1,
317 |s: &mut Session| {
318 s.authority_identity = SessionAuthorityIdentity::Supervisor {
319 incarnation_id: uuid::Uuid::nil(),
320 }
321 },
322 ] {
323 let mut invalid = supervisor.clone();
324 mutation(&mut invalid);
325 invalid.metadata.insert("role".into(), "supervisor".into());
326 for name in ["session_control", "session_history"] {
327 assert!(!invalid.allows_model_tool_execution(name));
328 }
329 }
330 for name in [
331 "default::session_control",
332 "vendor::session_control",
333 "exec_command",
334 "Write",
335 ] {
336 assert!(!supervisor.allows_model_tool_execution(name), "{name}");
337 }
338 let child = Session::new_child_of("child", &supervisor, "model", "child");
339 assert_eq!(child.supervisor_incarnation_id(), None);
340 let mut ordinary = Session::new(DEFAULT_SUPERVISOR_SESSION_ID, "model");
341 ordinary.set_root_orchestration_only(true).unwrap();
342 ordinary.metadata.insert("role".into(), "supervisor".into());
343 for name in ["session_control", "session_history"] {
344 assert!(!ordinary.allows_model_tool_execution(name));
345 }
346 }
347
348 #[test]
349 fn thinking_mode_is_a_derived_root_policy_independent_of_effort() {
350 use crate::ReasoningEffort;
351 let mut root = Session::new("ultra-root", "model");
352 root.reasoning_effort = Some(ReasoningEffort::Max);
353 root.set_root_orchestration_prompt_enabled(true);
354 assert_eq!(root.root_thinking_mode(), RootThinkingMode::Standard);
355 root.set_root_orchestration_only(true).unwrap();
356 let persisted = serde_json::to_value(&root).unwrap();
357 assert!(
358 persisted.get("thinking_mode").is_none(),
359 "no second persisted authority"
360 );
361 let reloaded: Session = serde_json::from_value(persisted.clone()).unwrap();
362 assert_eq!(reloaded.root_thinking_mode(), RootThinkingMode::Ultra);
363 assert_eq!(reloaded.reasoning_effort, Some(ReasoningEffort::Max));
364 assert_eq!(
365 serde_json::to_value(&reloaded).unwrap(),
366 persisted,
367 "projection never writes"
368 );
369 let mut child = Session::new_child_of("ultra-child", &reloaded, "model", "child");
370 assert_eq!(child.root_thinking_mode(), RootThinkingMode::Standard);
371 assert_eq!(child.reasoning_effort, None);
372 child.root_orchestration_only = true;
373 assert_eq!(
374 child.root_thinking_mode(),
375 RootThinkingMode::Standard,
376 "raw child bool has no authority"
377 );
378 assert_eq!(
379 child.set_root_orchestration_only(true),
380 Err(RootToolAuthorityError::NotRoot)
381 );
382 assert_eq!(
383 serde_json::to_string(&RootThinkingMode::Ultra).unwrap(),
384 "\"ultra\""
385 );
386 assert_eq!(
387 serde_json::from_str::<RootThinkingMode>("\"standard\"").unwrap(),
388 RootThinkingMode::Standard
389 );
390 assert!(serde_json::from_str::<RootThinkingMode>("\"max\"").is_err());
391 assert!(serde_json::from_str::<ReasoningEffort>("\"ultra\"").is_err());
392 }
393
394 #[test]
395 fn canonical_and_legacy_thinking_selectors_share_one_boolean_request() {
396 assert_eq!(RootThinkingMode::resolve_selection(None, None), Ok(None));
397 for enabled in [false, true] {
398 let mode = RootThinkingMode::from_enabled(enabled);
399 assert_eq!(
400 RootThinkingMode::resolve_selection(Some(mode), None),
401 Ok(Some(enabled))
402 );
403 assert_eq!(
404 RootThinkingMode::resolve_selection(None, Some(enabled)),
405 Ok(Some(enabled))
406 );
407 assert_eq!(
408 RootThinkingMode::resolve_selection(Some(mode), Some(enabled)),
409 Ok(Some(enabled))
410 );
411 assert_eq!(
412 RootThinkingMode::resolve_selection(Some(mode), Some(!enabled)),
413 Err(RootThinkingModeConflict)
414 );
415 }
416 }
417
418 #[test]
419 fn root_selection_is_persistent_and_child_does_not_inherit_it() {
420 let mut root = Session::new("root", "model");
421 root.set_root_orchestration_only(true).unwrap();
422 assert_eq!(root.root_tool_authority_revision, 1);
423 let reloaded: Session =
424 serde_json::from_str(&serde_json::to_string(&root).unwrap()).unwrap();
425 assert!(reloaded.root_orchestration_only_enabled());
426 let mut child = Session::new_child_of("child", &reloaded, "model", "child");
427 assert!(!child.root_orchestration_only_enabled());
428 assert_eq!(child.root_tool_authority_revision, 0);
429 assert_eq!(
430 child.set_root_orchestration_only(false),
431 Err(RootToolAuthorityError::NotRoot)
432 );
433 assert!(child.allows_model_tool_execution("Bash"));
434 }
435
436 #[test]
437 fn root_allowlist_is_exact_and_denies_unknown_tools() {
438 let mut root = Session::new("root", "model");
439 root.set_root_orchestration_only(true).unwrap();
440 for tool in ROOT_ORCHESTRATION_TOOLS {
441 assert!(root.allows_model_tool_execution(tool), "{tool}");
442 }
443 for tool in [
444 "exec_command",
445 "write_stdin",
446 "Bash",
447 "Edit",
448 "Write",
449 "WebFetch",
450 "workflow_run",
451 "mcp__x__read",
452 "read_file",
453 "default::Read",
454 ] {
455 assert!(!root.allows_model_tool_execution(tool), "{tool}");
456 }
457 assert_eq!(root.root_tool_authority_revision, 1);
458 root.set_root_orchestration_only(true).unwrap();
459 assert_eq!(root.root_tool_authority_revision, 1);
460 root.set_root_orchestration_only(false).unwrap();
461 assert_eq!(root.root_tool_authority_revision, 2);
462 assert!(root.allows_model_tool_execution("Bash"));
463 }
464
465 #[test]
466 fn durable_refresh_adopts_tightening_and_rejects_stale_loosen() {
467 let mut running = Session::new("root", "model");
468 let mut durable = running.clone();
469 durable.set_root_orchestration_only(true).unwrap();
470 running.adopt_root_tool_authority_from(&durable).unwrap();
471 assert!(!running.allows_model_tool_execution("Bash"));
472 assert_eq!(
473 durable.adopt_root_tool_authority_from(&Session::new("root", "model")),
474 Err(RootToolAuthorityError::StaleSnapshot)
475 );
476 durable.set_root_orchestration_only(false).unwrap();
477 running.adopt_root_tool_authority_from(&durable).unwrap();
478 assert!(running.allows_model_tool_execution("Bash"));
479 }
480
481 #[test]
482 fn durable_refresh_rejects_a_different_birth_identity_or_lineage() {
483 let running = Session::new("root", "model");
484 let mut other_birth = running.clone();
485 other_birth.created_at += chrono::Duration::microseconds(1);
486 assert_eq!(
487 running.clone().adopt_root_tool_authority_from(&other_birth),
488 Err(RootToolAuthorityError::StaleSnapshot)
489 );
490
491 let mut other_identity = running.clone();
492 other_identity.authority_identity = super::super::SessionAuthorityIdentity::Supervisor {
493 incarnation_id: uuid::Uuid::new_v4(),
494 };
495 assert_eq!(
496 running
497 .clone()
498 .adopt_root_tool_authority_from(&other_identity),
499 Err(RootToolAuthorityError::StaleSnapshot)
500 );
501
502 let mut other_lineage = running.clone();
503 other_lineage.root_session_id = "another-root".into();
504 assert_eq!(
505 running
506 .clone()
507 .adopt_root_tool_authority_from(&other_lineage),
508 Err(RootToolAuthorityError::StaleSnapshot)
509 );
510 other_lineage = running.clone();
511 other_lineage.spawn_depth = 1;
512 assert_eq!(
513 running
514 .clone()
515 .adopt_root_tool_authority_from(&other_lineage),
516 Err(RootToolAuthorityError::StaleSnapshot)
517 );
518
519 let mut legacy = running.clone();
521 legacy.root_session_id.clear();
522 legacy.adopt_root_tool_authority_from(&running).unwrap();
523 }
524
525 #[test]
526 fn root_selection_rejects_legacy_plan_or_workflow_that_needs_other_tools() {
527 let mut root = Session::new("root", "model");
528 let runtime = AgentRuntimeState {
529 plan_mode: Some(PlanModeState {
530 entered_at: chrono::Utc::now(),
531 pre_permission_mode: "default".to_string(),
532 plan_file_path: None,
533 status: PlanModeStatus::Exploring,
534 }),
535 ..AgentRuntimeState::default()
536 };
537 root.agent_runtime_state = Some(runtime);
538 assert_eq!(
539 root.set_root_orchestration_only(true),
540 Err(RootToolAuthorityError::LegacyPlanActive)
541 );
542 root.agent_runtime_state = None;
543 root.set_selected_skill_ids(vec!["workflow".to_string()]);
544 assert_eq!(
545 root.set_root_orchestration_only(true),
546 Err(RootToolAuthorityError::WorkflowSelected)
547 );
548 root.clear_selected_skill_ids();
549 root.set_root_orchestration_only(true).unwrap();
550 root.set_selected_skill_ids(vec!["workflow".to_string()]);
551 assert_eq!(
552 root.validate_root_orchestration_compatibility(),
553 Err(RootToolAuthorityError::WorkflowSelected)
554 );
555 }
556
557 #[test]
558 fn live_authority_adoption_resets_prior_model_context_epoch() {
559 for enabled in [false, true] {
560 let mut running = Session::new("root", "model");
561 if !enabled {
562 running.set_root_orchestration_only(true).unwrap();
563 }
564 running.model_context_state = Some(ModelContextState {
565 prefix_epoch: 4,
566 cache_scope_sha256: Some("prior-provider-scope".to_string()),
567 ..ModelContextState::default()
568 });
569 let mut durable = running.clone();
570 durable.set_root_orchestration_only(enabled).unwrap();
571 running.adopt_root_tool_authority_from(&durable).unwrap();
572 let context = running.model_context_state.as_ref().unwrap();
573 assert_eq!(context.prefix_epoch, 5);
574 assert!(context.cache_scope_sha256.is_none());
575 assert_eq!(
576 context.last_reset_reason,
577 Some(ModelContextResetReason::CacheScopeChanged)
578 );
579 }
580 }
581
582 #[test]
583 fn live_authority_adoption_preserves_all_missed_mode_resets() {
584 let mut running = Session::new("root", "model");
585 running.model_context_state = Some(ModelContextState {
586 prefix_epoch: 4,
587 cache_scope_sha256: Some("old scope".into()),
588 ..Default::default()
589 });
590 running
591 .activate_provider_transcript_route(
592 super::super::provider_transcript::ProviderFamily::OpenAi,
593 super::super::provider_transcript::ProviderProtocol::OpenAiResponsesV1,
594 &"a".repeat(64),
595 )
596 .unwrap();
597 let mut durable = running.clone();
598 durable.set_root_orchestration_only(true).unwrap();
599 durable
602 .model_context_state
603 .as_mut()
604 .unwrap()
605 .cache_scope_sha256 = Some("new scope".into());
606 durable.set_root_orchestration_only(false).unwrap();
607 assert_eq!(running.root_thinking_mode(), durable.root_thinking_mode());
608 running.adopt_root_tool_authority_from(&durable).unwrap();
609 assert_eq!(running.model_context_state, durable.model_context_state);
610 assert_eq!(
611 running.model_context_state.as_ref().unwrap().prefix_epoch,
612 6
613 );
614 assert_eq!(running.provider_transcript, durable.provider_transcript);
615 running
617 .model_context_state
618 .as_mut()
619 .unwrap()
620 .cache_scope_sha256 = Some("owner scope".into());
621 let owner_context = running.model_context_state.clone();
622 durable.root_mode_transition_epoch += 1;
623 running.adopt_root_tool_authority_from(&durable).unwrap();
624 assert_eq!(running.model_context_state, owner_context);
625 }
626}