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