1use agent_core::orchestration::{
2 CatalogSnapshot, CompletionGate, DelegationPolicy, WorkerRegistry, WorkerRole, WorkerTerminal,
3 WorkerWritePolicy,
4};
5use agent_core::prompt::QualifiedModelId;
6use serde::Serialize;
7use std::collections::{BTreeMap, HashMap, HashSet};
8use std::sync::Mutex;
9
10#[derive(Debug, Clone, Eq, PartialEq)]
15pub enum DelegationTreeDenied {
16 DepthLimit { max_depth: u16 },
18 DuplicateWorker,
20 UnknownParent,
22}
23
24impl std::fmt::Display for DelegationTreeDenied {
25 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
26 match self {
27 Self::DepthLimit { max_depth } => write!(
28 f,
29 "subagent nesting limit reached: workers may nest {max_depth} level(s) deep \
30 (subagent.max_depth). Do this work directly instead of delegating further."
31 ),
32 Self::DuplicateWorker => write!(f, "this subagent handle is already registered"),
33 Self::UnknownParent => write!(
34 f,
35 "the parent worker of this delegation has already finished or been cancelled"
36 ),
37 }
38 }
39}
40
41#[derive(Debug, Default)]
42struct DelegationTreeState {
43 depth_by_worker: BTreeMap<String, u16>,
44 parent_by_worker: BTreeMap<String, String>,
45 descendants: usize,
46}
47
48pub struct OrchestrationRuntime {
50 inner: Mutex<Inner>,
51 max_depth: u16,
52 tree: Mutex<DelegationTreeState>,
53}
54struct Inner {
55 registry: WorkerRegistry,
56 handles: HashMap<String, agent_core::orchestration::WorkerHandle>,
57}
58
59#[derive(Clone, Debug, Serialize)]
60pub struct AuthorizedWorkerModel {
61 pub model: QualifiedModelId,
62 pub selection_source: SelectionSource,
63 pub network_attempted: bool,
64 pub policy_digest: String,
65 pub catalog_snapshot_id: String,
66 pub catalog_digest: String,
67 pub correlation_id: String,
68 pub cross_provider_grant_id: Option<String>,
69}
70#[derive(Clone, Copy, Debug, Serialize)]
71#[serde(rename_all = "snake_case")]
72pub enum SelectionSource {
73 ForegroundInheritance,
74 ExplicitRequest,
75}
76impl SelectionSource {
77 pub fn as_str(self) -> &'static str {
78 match self {
79 Self::ForegroundInheritance => "foreground_inheritance",
80 Self::ExplicitRequest => "explicit_request",
81 }
82 }
83}
84#[derive(Clone, Debug, Serialize)]
85pub struct DispatchDenial {
86 pub kind: &'static str,
87 pub code: &'static str,
88 pub requested_model: Option<String>,
89 pub foreground_model: String,
90 pub selection_source: SelectionSource,
91 pub network_attempted: bool,
92 pub remediation: &'static str,
93}
94impl std::fmt::Display for DispatchDenial {
95 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
96 write!(
97 f,
98 "{}",
99 serde_json::to_string(self).unwrap_or_else(|_| self.code.into())
100 )
101 }
102}
103impl std::error::Error for DispatchDenial {}
104
105pub fn canonical_foreground_identity(raw: &str) -> Result<QualifiedModelId, String> {
111 if let Ok(model) = QualifiedModelId::parse(raw) {
112 return Ok(model);
113 }
114 let route = crate::runtime::openai::resolve_route(raw)
115 .ok_or_else(|| format!("foreground model is unresolved: {raw}"))?;
116 QualifiedModelId::parse(format!("{}/{}", route.provider, route.model))
117 .map_err(|e| e.to_string())
118}
119
120pub fn validate_user_authorizable_model(raw: &str) -> Result<QualifiedModelId, String> {
125 let model = QualifiedModelId::parse(raw)
126 .map_err(|_| "authorization denied: invalid qualified model".to_string())?;
127 let known = if model.provider() == "anthropic" {
128 agent_core::models::KNOWN_MODELS
129 .iter()
130 .any(|(id, _)| *id == model.model())
131 } else if model.provider() == "openai-codex" {
132 crate::runtime::openai::catalog::codex_static_catalog_models()
133 .iter()
134 .any(|entry| entry.runtime_id() == model.as_str())
135 } else if model.provider() == "xai-auth" {
136 crate::runtime::openai::catalog::xai_model(model.model()).is_some()
137 } else if model.provider() == "github-copilot" {
138 crate::runtime::openai::catalog::github_copilot_runtime_model(model.model()).is_some()
139 } else if model.provider() == "google-gemini" {
140 crate::runtime::openai::catalog::google_gemini_model(model.model()).is_some()
141 } else {
142 crate::runtime::openai::registry::providers()
143 .iter()
144 .find(|provider| provider.key == model.provider())
145 .is_some_and(|provider| {
146 provider
147 .models
148 .iter()
149 .any(|(id, _, _)| *id == model.model())
150 })
151 };
152 if !known || crate::runtime::openai::resolve_route(model.as_str()).is_none() {
153 return Err(format!(
154 "authorization denied: '{}' is not a known routable model",
155 model.as_str()
156 ));
157 }
158 Ok(model)
159}
160
161const OPENROUTER_WORKER_MODELS: &[&str] = &[
164 "openrouter/deepseek/deepseek-v4-pro",
165 "openrouter/moonshotai/kimi-k2.7-code",
166 "openrouter/z-ai/glm-5.2",
167];
168
169fn manifestless_worker_choices(foreground: &QualifiedModelId) -> Vec<QualifiedModelId> {
171 let source = if foreground.provider() == "openrouter" {
172 OPENROUTER_WORKER_MODELS
173 } else {
174 &[]
175 };
176 let mut choices: Vec<_> = source
177 .iter()
178 .filter_map(|value| QualifiedModelId::parse(*value).ok())
179 .collect();
180 choices.push(foreground.clone());
181 choices.sort();
182 choices.dedup();
183 choices
184}
185
186impl OrchestrationRuntime {
187 pub fn trusted_catalog(
192 foreground: &QualifiedModelId,
193 _manifest_references: impl IntoIterator<Item = QualifiedModelId>,
194 ) -> Result<CatalogSnapshot, &'static str> {
195 use crate::runtime::openai::catalog;
196 use agent_core::orchestration::CatalogEntry;
197
198 let route = crate::runtime::openai::resolve_route(foreground.as_str())
199 .ok_or("foreground model is unresolved")?;
200 if route.provider != foreground.provider() {
201 return Err("foreground provider did not resolve exactly");
202 }
203
204 let mut descriptors = catalog::codex_static_catalog_models();
205 descriptors.extend(catalog::xai_static_catalog_models());
206 descriptors.extend(catalog::copilot_static_catalog_models());
207 descriptors.extend(catalog::google_gemini_static_catalog_models());
208 let mut entries: Vec<_> = descriptors
209 .into_iter()
210 .filter_map(|descriptor| QualifiedModelId::parse(descriptor.runtime_id()).ok())
211 .map(|model| CatalogEntry {
212 model,
213 available: true,
214 worker_eligible: true,
215 })
216 .collect();
217 entries.extend(
218 manifestless_worker_choices(foreground)
219 .into_iter()
220 .map(|model| CatalogEntry {
221 model,
222 available: true,
223 worker_eligible: true,
224 }),
225 );
226 entries.push(CatalogEntry {
230 model: foreground.clone(),
231 available: true,
232 worker_eligible: true,
233 });
234 Ok(CatalogSnapshot::from_entries(entries))
235 }
236
237 pub fn from_limits(
240 foreground: QualifiedModelId,
241 limits: crate::config::SubagentLimits,
242 ) -> Result<Self, &'static str> {
243 Self::baseline(foreground, limits.max_concurrent, limits.max_total)?
244 .with_max_depth(limits.max_depth)
245 }
246
247 pub fn baseline(
250 foreground: QualifiedModelId,
251 concurrent: usize,
252 total: usize,
253 ) -> Result<Self, &'static str> {
254 let choices = manifestless_worker_choices(&foreground);
255 let catalog = Self::trusted_catalog(&foreground, std::iter::empty())?;
256 let mut policy =
257 DelegationPolicy::provider_baseline(foreground, catalog, choices, concurrent, total)?;
258 policy.recycle_reconciled_workers = true;
259 Ok(Self::new(policy))
260 }
261
262 pub fn new(policy: DelegationPolicy) -> Self {
263 Self {
264 inner: Mutex::new(Inner {
265 registry: WorkerRegistry::new(policy),
266 handles: HashMap::new(),
267 }),
268 max_depth: crate::config::SubagentLimits::default().max_depth,
269 tree: Mutex::new(DelegationTreeState::default()),
270 }
271 }
272
273 pub fn with_max_depth(mut self, max_depth: u16) -> Result<Self, &'static str> {
276 if max_depth == 0 {
277 return Err("invalid subagent nesting depth");
278 }
279 self.max_depth = max_depth;
280 Ok(self)
281 }
282
283 pub fn reserve_delegation(
289 &self,
290 worker_id: &str,
291 parent: Option<&str>,
292 ) -> Result<u16, DelegationTreeDenied> {
293 let mut tree = self.tree.lock().unwrap();
294 if tree.depth_by_worker.contains_key(worker_id) {
295 return Err(DelegationTreeDenied::DuplicateWorker);
296 }
297 let depth = match parent {
298 None => 1,
299 Some(parent_id) => tree
300 .depth_by_worker
301 .get(parent_id)
302 .copied()
303 .ok_or(DelegationTreeDenied::UnknownParent)?
304 .saturating_add(1),
305 };
306 if depth > self.max_depth {
307 return Err(DelegationTreeDenied::DepthLimit {
308 max_depth: self.max_depth,
309 });
310 }
311 let parent_key = parent.unwrap_or("<root>");
312 tree.depth_by_worker.insert(worker_id.to_string(), depth);
313 tree.parent_by_worker
314 .insert(worker_id.to_string(), parent_key.to_string());
315 tree.descendants += 1;
316 Ok(depth)
317 }
318
319 pub fn release_delegation(&self, worker_id: &str, _parent: Option<&str>) {
325 let mut tree = self.tree.lock().unwrap();
326 let mut stack = vec![worker_id.to_string()];
327 while let Some(current) = stack.pop() {
328 let children = tree
329 .parent_by_worker
330 .iter()
331 .filter_map(|(child, parent)| (parent == ¤t).then_some(child.clone()))
332 .collect::<Vec<_>>();
333 stack.extend(children);
334 if tree.depth_by_worker.remove(¤t).is_some() {
335 tree.descendants = tree.descendants.saturating_sub(1);
336 tree.parent_by_worker.remove(¤t);
337 }
338 }
339 }
340
341 pub fn delegation_descendants(&self) -> usize {
342 self.tree.lock().unwrap().descendants
343 }
344 pub fn preflight(&self, model: &str) -> Result<(), String> {
345 let model = QualifiedModelId::parse(model)
346 .map_err(|_| "delegation denied: invalid qualified model".to_string())?;
347 self.inner
348 .lock()
349 .unwrap()
350 .registry
351 .validate_dispatch(&model)
352 .map_err(|e| format!("delegation denied: {}", e.code()))
353 }
354
355 pub fn grant_worker_model(&self, model: &str) -> Result<(), String> {
361 let model = QualifiedModelId::parse(model)
362 .map_err(|_| "grant denied: invalid qualified model".to_string())?;
363 self.inner
364 .lock()
365 .unwrap()
366 .registry
367 .grant_worker_model(model)
368 .map_err(|error| format!("grant denied: {error}"))
369 }
370
371 pub fn ultracode_readiness(&self, model: &str) -> Result<(usize, usize), String> {
375 let model = QualifiedModelId::parse(model)
376 .map_err(|_| "delegation denied: invalid qualified model".to_string())?;
377 let inner = self.inner.lock().unwrap();
378 if inner.registry.foreground_model() != &model {
379 return Err("delegation denied: model is not exact foreground".into());
380 }
381 inner
382 .registry
383 .validate_dispatch(&model)
384 .map_err(|e| format!("delegation denied: {}", e.code()))?;
385 let policy = inner.registry.policy();
386 if policy.max_concurrent_workers == 0 || policy.max_total_workers == 0 {
387 return Err("delegation denied: invalid worker limits".into());
388 }
389 Ok((policy.max_concurrent_workers, policy.max_total_workers))
390 }
391
392 pub fn resolve_and_authorize(
395 &self,
396 runtime_handle: &str,
397 requested: Option<&str>,
398 ) -> Result<AuthorizedWorkerModel, DispatchDenial> {
399 let mut inner = self.inner.lock().unwrap();
400 let foreground = inner.registry.foreground_model().clone();
401 let selection_source = if requested.is_some() {
402 SelectionSource::ExplicitRequest
403 } else {
404 SelectionSource::ForegroundInheritance
405 };
406 let model = match requested {
407 Some(value) => QualifiedModelId::parse(value).map_err(|_| DispatchDenial {
408 kind: "dispatch_denied",
409 code: "invalid_qualified_model",
410 requested_model: None,
411 foreground_model: foreground.as_str().into(),
412 selection_source,
413 network_attempted: false,
414 remediation: "Omit model to inherit foreground or select an exact session choice.",
415 })?,
416 None => foreground.clone(),
417 };
418 if inner.handles.contains_key(runtime_handle) {
419 return Err(DispatchDenial {
420 kind: "dispatch_denied",
421 code: "duplicate_runtime_handle",
422 requested_model: Some(model.as_str().into()),
423 foreground_model: foreground.as_str().into(),
424 selection_source,
425 network_attempted: false,
426 remediation: "Use a new worker handle.",
427 });
428 }
429 let policy_digest = inner.registry.policy().digest();
430 let catalog_snapshot_id = inner.registry.policy().catalog_snapshot_id().to_owned();
431 let catalog_digest = inner.registry.policy().catalog_digest().to_owned();
432 let cross_provider_grant_id = inner
433 .registry
434 .policy()
435 .authorize(&model)
436 .ok()
437 .flatten()
438 .map(str::to_owned);
439 let handle = inner
440 .registry
441 .authorize_dispatch(&model, WorkerRole::Implementer, WorkerWritePolicy::ReadOnly)
442 .map_err(|error| DispatchDenial {
443 kind: "dispatch_denied",
444 code: error.code(),
445 requested_model: Some(model.as_str().into()),
446 foreground_model: foreground.as_str().into(),
447 selection_source,
448 network_attempted: false,
449 remediation: match error.typed_code() {
450 agent_core::orchestration::DispatchFailureCode::ConcurrencyLimit =>
451 if inner.registry.policy().recycle_reconciled_workers {
452 "Wait for running workers to finish; collect and reconcile their results. The limit is subagent.max_concurrent in config. Changing model does not release capacity."
453 } else {
454 "Wait for running workers to finish; collect and reconcile their results. The prompt manifest's delegation policy sets this limit. Changing model does not release capacity."
455 },
456 agent_core::orchestration::DispatchFailureCode::TotalWorkerLimit =>
457 if inner.registry.policy().recycle_reconciled_workers {
458 "Collect finished workers with reconciled=true to release outstanding-worker capacity (subagent.max_total in config). Changing model does not release capacity."
459 } else {
460 "The explicit delegation policy cumulative worker budget is exhausted. Changing model or collecting workers does not renew it."
461 },
462 _ => "Omit model to inherit foreground, select an exact session choice, or trust the model mid-session (favorite it in the models picker).",
463 },
464 })?;
465 inner.handles.insert(runtime_handle.to_owned(), handle);
466 Ok(AuthorizedWorkerModel {
467 model,
468 selection_source,
469 network_attempted: false,
470 policy_digest,
471 catalog_snapshot_id,
472 catalog_digest,
473 correlation_id: runtime_handle.to_owned(),
474 cross_provider_grant_id,
475 })
476 }
477
478 pub fn mark_starting(&self, runtime_handle: &str) -> Result<(), String> {
481 let mut inner = self.inner.lock().unwrap();
482 let handle = inner
483 .handles
484 .get(runtime_handle)
485 .cloned()
486 .ok_or_else(|| "worker lifecycle unavailable".to_string())?;
487 inner
488 .registry
489 .mark_starting(&handle)
490 .map_err(|_| "worker lifecycle unavailable".into())
491 }
492 pub fn mark_running(&self, runtime_handle: &str) -> Result<(), String> {
493 let mut inner = self.inner.lock().unwrap();
494 let handle = inner
495 .handles
496 .get(runtime_handle)
497 .cloned()
498 .ok_or_else(|| "worker lifecycle unavailable".to_string())?;
499 inner
500 .registry
501 .mark_running(&handle)
502 .map_err(|_| "worker lifecycle unavailable".into())
503 }
504
505 pub fn rollback(&self, runtime_handle: &str) {
506 let mut inner = self.inner.lock().unwrap();
507 if let Some(handle) = inner.handles.get(runtime_handle).cloned() {
508 let _ = inner.registry.rollback_dispatch(&handle);
509 inner.handles.remove(runtime_handle);
510 }
511 }
512
513 pub fn effective_choices(&self) -> Vec<String> {
514 self.inner
515 .lock()
516 .unwrap()
517 .registry
518 .policy()
519 .effective_choices()
520 .iter()
521 .map(|model| model.as_str().to_owned())
522 .collect()
523 }
524
525 pub fn foreground_model(&self) -> String {
526 self.inner
527 .lock()
528 .unwrap()
529 .registry
530 .foreground_model()
531 .as_str()
532 .to_owned()
533 }
534 pub fn authorize_with_policy(
535 &self,
536 runtime_handle: &str,
537 model: &str,
538 role: WorkerRole,
539 writes: WorkerWritePolicy,
540 ) -> Result<(), String> {
541 let model = QualifiedModelId::parse(model)
542 .map_err(|_| "delegation denied: invalid qualified model".to_string())?;
543 let mut inner = self.inner.lock().unwrap();
544 if inner.handles.contains_key(runtime_handle) {
545 return Err("delegation denied: duplicate runtime handle".to_string());
546 }
547 let handle = inner
548 .registry
549 .authorize_dispatch(&model, role, writes)
550 .map_err(|e| format!("delegation denied: {}", e.code()))?;
551 inner.registry.mark_starting(&handle).map_err(|error| {
552 let _ = inner.registry.rollback_dispatch(&handle);
553 error.to_string()
554 })?;
555 if let Err(error) = inner.registry.mark_running(&handle) {
556 let _ = inner.registry.rollback_dispatch(&handle);
557 return Err(error.to_string());
558 }
559 inner.handles.insert(runtime_handle.to_string(), handle);
560 Ok(())
561 }
562 pub fn authorize(&self, runtime_handle: &str, model: &str) -> Result<(), String> {
563 self.authorize_with_policy(
564 runtime_handle,
565 model,
566 WorkerRole::Implementer,
567 WorkerWritePolicy::ReadOnly,
568 )
569 }
570 pub fn poll(&self, id: &str, fingerprint: &str) -> Result<(), String> {
571 let mut inner = self.inner.lock().unwrap();
572 let h = inner
573 .handles
574 .get(id)
575 .cloned()
576 .ok_or_else(|| "unknown worker".to_string())?;
577 inner.registry.poll(&h, fingerprint).map_err(str::to_string)
578 }
579 pub fn steer(&self, id: &str) -> Result<(), String> {
580 let mut inner = self.inner.lock().unwrap();
581 let h = inner
582 .handles
583 .get(id)
584 .cloned()
585 .ok_or_else(|| "unknown worker".to_string())?;
586 inner.registry.steer(&h).map_err(str::to_string)
587 }
588 pub fn terminal_and_collect(&self, id: &str, terminal: WorkerTerminal) -> Result<(), String> {
589 let mut inner = self.inner.lock().unwrap();
590 let h = inner
591 .handles
592 .get(id)
593 .cloned()
594 .ok_or_else(|| "unknown worker".to_string())?;
595 inner
596 .registry
597 .mark_terminal(&h, terminal)
598 .map_err(str::to_string)?;
599 inner.registry.collect(&h).map_err(str::to_string)
600 }
601 pub fn finish_one_shot(&self, id: &str, terminal: WorkerTerminal) -> Result<(), String> {
602 self.terminal_and_collect(id, terminal)?;
603 self.reconcile(id)?;
604 self.retire_reconciled(&[id.to_owned()].into_iter().collect());
606 Ok(())
607 }
608 pub fn reconcile(&self, id: &str) -> Result<(), String> {
609 let mut inner = self.inner.lock().unwrap();
610 let h = inner
611 .handles
612 .get(id)
613 .cloned()
614 .ok_or_else(|| "unknown worker".to_string())?;
615 inner.registry.reconcile(&h).map_err(str::to_string)
616 }
617 pub fn retire_reconciled(&self, removed_handles: &HashSet<String>) {
620 let mut inner = self.inner.lock().unwrap();
621 for id in removed_handles {
622 if let Some(handle) = inner.handles.get(id).cloned() {
623 if inner.registry.retire_reconciled(&handle) {
624 inner.handles.remove(id);
625 }
626 }
627 }
628 }
629
630 pub fn unreconciled_runtime_handles(&self) -> HashSet<String> {
633 let inner = self.inner.lock().unwrap();
634 let unreconciled = inner.registry.all_unreconciled_ids();
639 if unreconciled.is_empty() {
640 return HashSet::new();
641 }
642 inner
643 .handles
644 .iter()
645 .filter(|(_, handle)| unreconciled.iter().any(|id| id == handle.id()))
646 .map(|(runtime_id, _)| runtime_id.clone())
647 .collect()
648 }
649
650 pub fn is_unreconciled(&self, runtime_handle: &str) -> bool {
652 self.unreconciled_runtime_handles().contains(runtime_handle)
653 }
654
655 pub fn completion_gate(&self) -> CompletionGate {
656 let inner = self.inner.lock().unwrap();
660 match inner.registry.completion_gate() {
661 CompletionGate::Allowed => CompletionGate::Allowed,
662 CompletionGate::Warning { workers } => CompletionGate::Warning {
663 workers: Self::map_policy_ids_to_runtime(&inner.handles, workers),
664 },
665 CompletionGate::Blocked { workers } => CompletionGate::Blocked {
666 workers: Self::map_policy_ids_to_runtime(&inner.handles, workers),
667 },
668 }
669 }
670
671 fn map_policy_ids_to_runtime(
674 handles: &HashMap<String, agent_core::orchestration::WorkerHandle>,
675 policy_ids: Vec<String>,
676 ) -> Vec<String> {
677 let reverse: HashMap<&str, &str> = handles
678 .iter()
679 .map(|(runtime_handle, policy_handle)| (policy_handle.id(), runtime_handle.as_str()))
680 .collect();
681 policy_ids
682 .into_iter()
683 .map(|policy_id| {
684 reverse
685 .get(policy_id.as_str())
686 .map(|runtime| (*runtime).to_owned())
687 .unwrap_or(policy_id)
688 })
689 .collect()
690 }
691 pub fn check_foreground_write(&self, path: &str) -> agent_core::orchestration::ScopeDecision {
692 self.inner
693 .lock()
694 .unwrap()
695 .registry
696 .check_foreground_write(path)
697 }
698 pub fn telemetry_json(&self) -> String {
699 serde_json::to_string(self.inner.lock().unwrap().registry.telemetry())
700 .unwrap_or_else(|_| "[]".into())
701 }
702 pub fn enforcement_mode(&self) -> agent_core::orchestration::EnforcementMode {
703 self.inner.lock().unwrap().registry.mode()
704 }
705}
706
707#[cfg(test)]
708mod tests {
709 use super::*;
710 use std::sync::Arc;
711 fn model(s: &str) -> QualifiedModelId {
712 QualifiedModelId::parse(s).unwrap()
713 }
714 #[test]
719 fn legacy_bare_claude_foreground_canonicalizes_via_router() {
720 let id = canonical_foreground_identity("claude-fable-5").unwrap();
721 assert_eq!(id.as_str(), "anthropic/claude-fable-5");
722 OrchestrationRuntime::baseline(id, 8, 64).unwrap();
724 }
725
726 #[test]
727 fn baseline_openrouter_catalog_authorizes_exact_chinese_worker_stack() {
728 let deepseek = model("openrouter/deepseek/deepseek-v4-pro");
729 let glm = model("openrouter/z-ai/glm-5.2");
730 let kimi = model("openrouter/moonshotai/kimi-k2.7-code");
731 let rt = OrchestrationRuntime::baseline(deepseek.clone(), 3, 8).unwrap();
732
733 assert_eq!(
734 rt.effective_choices(),
735 vec![
736 deepseek.as_str().to_owned(),
737 kimi.as_str().to_owned(),
738 glm.as_str().to_owned(),
739 ]
740 );
741 assert_eq!(
742 rt.resolve_and_authorize("sa_glm", Some(glm.as_str()))
743 .unwrap()
744 .model,
745 glm
746 );
747 assert_eq!(
748 rt.resolve_and_authorize("sa_kimi", Some(kimi.as_str()))
749 .unwrap()
750 .model,
751 kimi
752 );
753 }
754
755 #[test]
756 fn manifestless_non_openrouter_baseline_remains_foreground_only() {
757 let foreground = model("anthropic/claude-fable-5");
758 let rt = OrchestrationRuntime::baseline(foreground.clone(), 3, 8).unwrap();
759 assert_eq!(rt.effective_choices(), vec![foreground.as_str().to_owned()]);
760 let denied = rt
761 .resolve_and_authorize(
762 "sa_openrouter",
763 Some("openrouter/moonshotai/kimi-k2.7-code"),
764 )
765 .unwrap_err();
766 assert!(matches!(
767 denied.code,
768 "provider_not_allowed" | "catalog_model_unknown"
769 ));
770 }
771
772 #[test]
777 fn baseline_honors_mid_session_user_grant() {
778 let foreground = model("anthropic/claude-fable-5");
779 let requested = "openai-codex/gpt-5.6-sol";
780 let rt = OrchestrationRuntime::baseline(foreground.clone(), 3, 8).unwrap();
781 assert_eq!(rt.effective_choices(), vec![foreground.as_str().to_owned()]);
782 let denied = rt
783 .resolve_and_authorize("sa_denied", Some(requested))
784 .unwrap_err();
785 assert!(matches!(
786 denied.code,
787 "provider_not_allowed" | "catalog_model_unknown"
788 ));
789 assert!(!denied.network_attempted);
790
791 rt.grant_worker_model(requested).unwrap();
792
793 assert!(rt.effective_choices().contains(&requested.to_owned()));
794 let authorized = rt
795 .resolve_and_authorize("sa_granted", Some(requested))
796 .unwrap();
797 assert_eq!(authorized.model.as_str(), requested);
798 assert_eq!(
799 authorized.cross_provider_grant_id.as_deref(),
800 Some("session-user-grant-openai-codex/gpt-5.6-sol")
801 );
802 assert!(!authorized.network_attempted);
803 rt.rollback("sa_granted");
804 assert_eq!(rt.completion_gate(), CompletionGate::Allowed);
805 }
806
807 #[test]
809 fn mid_session_grant_rejects_invalid_qualified_model() {
810 let foreground = model("anthropic/claude-fable-5");
811 let rt = OrchestrationRuntime::baseline(foreground.clone(), 3, 8).unwrap();
812 assert!(rt.grant_worker_model("").is_err());
813 assert!(rt.grant_worker_model("not-qualified").is_err());
814 assert_eq!(rt.effective_choices(), vec![foreground.as_str().to_owned()]);
815 }
816
817 #[test]
818 fn qualified_foreground_identity_passes_through_unchanged() {
819 for raw in [
820 "openai-codex/gpt-5.6-sol",
821 "anthropic/claude-fable-5",
822 "openrouter/z-ai/glm-5.1",
823 ] {
824 assert_eq!(canonical_foreground_identity(raw).unwrap().as_str(), raw);
825 }
826 }
827
828 #[test]
829 fn unroutable_foreground_identity_fails_closed() {
830 assert!(canonical_foreground_identity("foo/").is_err());
833 assert!(canonical_foreground_identity("").is_err());
834 }
835
836 #[test]
837 fn trusted_catalog_rejects_unresolved_foreground_and_ignores_manifest_candidates() {
838 let unresolved = model("invented/foreground");
839 assert!(OrchestrationRuntime::trusted_catalog(
840 &unresolved,
841 [model("anthropic/claude-manifest-injected")]
842 )
843 .is_err());
844
845 let foreground = model("openai-codex/gpt-5.1-codex-mini");
846 let catalog = OrchestrationRuntime::trusted_catalog(
847 &foreground,
848 [model("anthropic/claude-manifest-injected")],
849 )
850 .unwrap();
851 assert!(catalog.contains(&foreground));
852 assert!(!catalog.contains(&model("anthropic/claude-manifest-injected")));
853 }
854
855 #[test]
856 fn authorization_reserves_without_claiming_worker_is_running() {
857 let foreground = model("anthropic/claude-foreground");
858 let rt = OrchestrationRuntime::new(DelegationPolicy::enforced(
859 foreground.clone(),
860 [foreground.clone()],
861 1,
862 1,
863 ));
864 rt.resolve_and_authorize("sa_reserved", None).unwrap();
865 assert!(rt.poll("sa_reserved", "not-started").is_err());
866 rt.mark_starting("sa_reserved").unwrap();
867 assert!(rt.poll("sa_reserved", "not-running").is_err());
868 rt.rollback("sa_reserved");
869 assert_eq!(rt.completion_gate(), CompletionGate::Allowed);
870 }
871
872 #[test]
873 fn one_shot_uses_the_full_lifecycle_before_terminal_collection() {
874 let foreground = model("anthropic/foreground");
875 let rt = OrchestrationRuntime::new(DelegationPolicy::enforced(
876 foreground.clone(),
877 [foreground],
878 1,
879 1,
880 ));
881 rt.resolve_and_authorize("sa_one_shot", None).unwrap();
882 rt.mark_starting("sa_one_shot").unwrap();
883 rt.mark_running("sa_one_shot").unwrap();
884 rt.finish_one_shot("sa_one_shot", WorkerTerminal::Completed)
885 .unwrap();
886 assert_eq!(rt.completion_gate(), CompletionGate::Allowed);
887 let telemetry = rt.telemetry_json();
888 for event in [
889 "worker.dispatch_allowed",
890 "worker.starting",
891 "worker.running",
892 "worker.terminal",
893 "worker.collected",
894 "worker.reconciled",
895 ] {
896 assert!(telemetry.contains(event), "missing {event}: {telemetry}");
897 }
898 }
899
900 #[test]
901 fn authorization_happens_before_a_worker_is_registered() {
902 let rt = OrchestrationRuntime::new(DelegationPolicy::enforced(
903 model("anthropic/foreground"),
904 [model("anthropic/worker")],
905 1,
906 1,
907 ));
908 assert!(rt.authorize("sa_1", "openrouter/worker").is_err());
909 assert_eq!(rt.completion_gate(), CompletionGate::Allowed);
910 rt.authorize("sa_2", "anthropic/worker").unwrap();
911 assert_eq!(rt.completion_gate(), CompletionGate::Allowed);
913 rt.poll("sa_2", "same").unwrap();
914 rt.steer("sa_2").unwrap();
915 rt.terminal_and_collect("sa_2", WorkerTerminal::Completed)
916 .unwrap();
917 assert!(matches!(
919 rt.completion_gate(),
920 CompletionGate::Blocked { .. }
921 ));
922 rt.reconcile("sa_2").unwrap();
923 assert_eq!(rt.completion_gate(), CompletionGate::Allowed);
924 }
925
926 #[test]
929 fn delegation_tree_bounds_depth_and_rejects_duplicate_handles() {
930 let foreground = model("anthropic/foreground");
931 let rt = OrchestrationRuntime::new(DelegationPolicy::enforced(
932 foreground.clone(),
933 [foreground],
934 8,
935 16,
936 ))
937 .with_max_depth(2)
938 .unwrap();
939
940 assert_eq!(rt.reserve_delegation("a", None), Ok(1));
941 assert_eq!(
942 rt.reserve_delegation("a", None),
943 Err(DelegationTreeDenied::DuplicateWorker),
944 "registration/spawn retry cannot double-reserve one handle"
945 );
946 assert_eq!(rt.reserve_delegation("b", Some("a")), Ok(2));
947 assert_eq!(
948 rt.reserve_delegation("too-deep", Some("b")),
949 Err(DelegationTreeDenied::DepthLimit { max_depth: 2 })
950 );
951 for i in 0..20 {
953 assert_eq!(
954 rt.reserve_delegation(&format!("sibling-{i}"), Some("a")),
955 Ok(2)
956 );
957 }
958 assert_eq!(rt.delegation_descendants(), 22);
959 rt.release_delegation("a", None);
960 assert_eq!(
961 rt.delegation_descendants(),
962 0,
963 "a parent takes its subtree with it"
964 );
965 assert!(OrchestrationRuntime::new(DelegationPolicy::enforced(
966 model("anthropic/foreground"),
967 [model("anthropic/foreground")],
968 1,
969 1,
970 ))
971 .with_max_depth(0)
972 .is_err());
973 }
974
975 #[test]
976 fn depth_denial_names_the_config_key() {
977 let message = DelegationTreeDenied::DepthLimit { max_depth: 3 }.to_string();
978 assert!(message.contains("3 level"), "{message}");
979 assert!(message.contains("subagent.max_depth"), "{message}");
980 assert!(
981 !message.contains("DepthLimit"),
982 "no Debug names in user text: {message}"
983 );
984 }
985
986 #[test]
988 fn delegation_release_is_idempotent_under_completion_cancel_race() {
989 let foreground = model("anthropic/foreground");
990 let rt = Arc::new(OrchestrationRuntime::new(DelegationPolicy::enforced(
991 foreground.clone(),
992 [foreground],
993 4,
994 8,
995 )));
996 rt.reserve_delegation("child", None).unwrap();
997 let a = Arc::clone(&rt);
998 let b = Arc::clone(&rt);
999 let t1 = std::thread::spawn(move || a.release_delegation("child", None));
1000 let t2 = std::thread::spawn(move || b.release_delegation("child", None));
1001 t1.join().unwrap();
1002 t2.join().unwrap();
1003 assert_eq!(rt.delegation_descendants(), 0);
1004 }
1005
1006 #[test]
1007 fn parent_exit_releases_entire_live_descendant_subtree() {
1008 let foreground = model("anthropic/foreground");
1009 let rt = OrchestrationRuntime::new(DelegationPolicy::enforced(
1010 foreground.clone(),
1011 [foreground],
1012 8,
1013 16,
1014 ));
1015 rt.reserve_delegation("parent", None).unwrap();
1016 rt.reserve_delegation("child", Some("parent")).unwrap();
1017 rt.reserve_delegation("grandchild", Some("child")).unwrap();
1018 assert_eq!(rt.delegation_descendants(), 3);
1019 rt.release_delegation("parent", None);
1020 assert_eq!(rt.delegation_descendants(), 0);
1021 assert_eq!(
1022 rt.reserve_delegation("orphan", Some("child")),
1023 Err(DelegationTreeDenied::UnknownParent)
1024 );
1025 }
1026
1027 #[test]
1030 fn parent_cancel_racing_child_completion_releases_every_lease() {
1031 let foreground = model("anthropic/foreground");
1032 let rt = Arc::new(OrchestrationRuntime::new(DelegationPolicy::enforced(
1033 foreground.clone(),
1034 [foreground],
1035 8,
1036 16,
1037 )));
1038 rt.reserve_delegation("parent", None).unwrap();
1039 rt.reserve_delegation("child", Some("parent")).unwrap();
1040 rt.reserve_delegation("grandchild", Some("child")).unwrap();
1041 let cancel = Arc::clone(&rt);
1042 let complete = Arc::clone(&rt);
1043 let t1 = std::thread::spawn(move || cancel.release_delegation("parent", None));
1044 let t2 = std::thread::spawn(move || complete.release_delegation("child", Some("parent")));
1045 t1.join().unwrap();
1046 t2.join().unwrap();
1047
1048 assert_eq!(rt.delegation_descendants(), 0);
1049 assert_eq!(
1050 rt.reserve_delegation("orphan", Some("grandchild")),
1051 Err(DelegationTreeDenied::UnknownParent)
1052 );
1053 assert_eq!(rt.reserve_delegation("replacement", None), Ok(1));
1054 rt.release_delegation("replacement", None);
1055 assert_eq!(rt.delegation_descendants(), 0);
1056 }
1057
1058 #[test]
1062 fn descendant_lineage_cannot_mint_or_broaden_session_model_grants() {
1063 let foreground = model("anthropic/foreground");
1064 let rt = OrchestrationRuntime::new(DelegationPolicy::enforced(
1065 foreground.clone(),
1066 [foreground],
1067 4,
1068 8,
1069 ));
1070 rt.grant_worker_model("openai-codex/gpt-5.6-sol").unwrap();
1071 rt.reserve_delegation("parent", None).unwrap();
1072 rt.reserve_delegation("child", Some("parent")).unwrap();
1073
1074 assert!(rt.preflight("openai-codex/gpt-5.6-sol").is_ok());
1075 assert!(
1076 rt.preflight("openai-codex/gpt-5.6-luna").is_err(),
1077 "a child lineage must not broaden one exact session grant to a sibling model"
1078 );
1079 assert!(
1080 rt.preflight("openrouter/ungranted").is_err(),
1081 "tree lineage must never mint a cross-provider grant"
1082 );
1083 rt.release_delegation("parent", None);
1084 assert_eq!(rt.delegation_descendants(), 0);
1085 }
1086
1087 #[test]
1090 fn completion_gate_reports_runtime_handles_not_policy_ids() {
1091 let rt = OrchestrationRuntime::new(DelegationPolicy::enforced(
1092 model("anthropic/foreground"),
1093 [model("anthropic/worker")],
1094 2,
1095 2,
1096 ));
1097 rt.authorize("sa_alpha", "anthropic/worker").unwrap();
1098 rt.authorize("sa_beta", "anthropic/worker").unwrap();
1099 rt.terminal_and_collect("sa_alpha", WorkerTerminal::Completed)
1101 .unwrap();
1102 rt.terminal_and_collect("sa_beta", WorkerTerminal::Completed)
1103 .unwrap();
1104
1105 match rt.completion_gate() {
1106 CompletionGate::Blocked { workers } => {
1107 assert_eq!(
1108 workers,
1109 vec!["sa_alpha".to_string(), "sa_beta".to_string()],
1110 "blocked IDs must be runtime handles in deterministic policy order"
1111 );
1112 for id in &workers {
1113 assert!(
1114 id.starts_with("sa_"),
1115 "blocked id must be a subagent_collect handle, got {id}"
1116 );
1117 assert!(
1118 !id.starts_with("worker-"),
1119 "blocked id must never leak policy WorkerHandle, got {id}"
1120 );
1121 }
1122 }
1123 other => panic!("expected Blocked with two sa_* handles, got {other:?}"),
1124 }
1125
1126 rt.reconcile("sa_alpha").unwrap();
1128 match rt.completion_gate() {
1129 CompletionGate::Blocked { workers } => {
1130 assert_eq!(workers, vec!["sa_beta".to_string()]);
1131 }
1132 other => panic!("expected only sa_beta still blocked, got {other:?}"),
1133 }
1134 }
1135}
1136
1137#[cfg(test)]
1138mod capacity_recovery_tests {
1139 use super::*;
1140 fn baseline(concurrent: usize, total: usize) -> OrchestrationRuntime {
1141 OrchestrationRuntime::baseline(
1142 QualifiedModelId::parse("anthropic/claude-fable-5").unwrap(),
1143 concurrent,
1144 total,
1145 )
1146 .unwrap()
1147 }
1148 #[test]
1149 fn manifestless_long_session_reuses_capacity_without_replacing_policy() {
1150 let rt = baseline(8, 64);
1151 let choices = rt.effective_choices();
1152 for n in 0..150 {
1153 let id = format!("sa_{n}");
1154 rt.resolve_and_authorize(&id, None).unwrap();
1155 rt.mark_starting(&id).unwrap();
1156 rt.mark_running(&id).unwrap();
1157 rt.terminal_and_collect(&id, WorkerTerminal::Completed)
1158 .unwrap();
1159 rt.reconcile(&id).unwrap();
1160 rt.reconcile(&id).unwrap();
1161 }
1162 assert_eq!(rt.effective_choices(), choices);
1163 assert_eq!(rt.completion_gate(), CompletionGate::Allowed);
1164 }
1165 #[test]
1166 fn retirement_only_touches_explicit_reconciled_ids() {
1167 let rt = baseline(8, 64);
1168 for id in ["old", "new", "uncollected"] {
1169 rt.authorize(id, &rt.foreground_model()).unwrap();
1170 rt.terminal_and_collect(id, WorkerTerminal::Completed)
1171 .unwrap();
1172 }
1173 rt.reconcile("old").unwrap();
1174 rt.reconcile("new").unwrap();
1175 rt.retire_reconciled(&["old".into(), "uncollected".into()].into_iter().collect());
1176 let inner = rt.inner.lock().unwrap();
1177 assert!(!inner.handles.contains_key("old"));
1178 assert!(inner.handles.contains_key("new"));
1179 assert!(inner.handles.contains_key("uncollected"));
1180 assert_eq!(inner.registry.total_dispatched(), 3);
1181 }
1182
1183 #[test]
1184 fn fanout_is_bounded_by_policy_concurrency_not_a_tree_cap() {
1185 let limits = crate::config::SubagentLimits {
1186 max_concurrent: 16,
1187 max_total: 64,
1188 max_depth: 4,
1189 };
1190 let rt = OrchestrationRuntime::from_limits(
1191 QualifiedModelId::parse("anthropic/claude-fable-5").unwrap(),
1192 limits,
1193 )
1194 .unwrap();
1195 assert_eq!(
1196 rt.ultracode_readiness("anthropic/claude-fable-5"),
1197 Ok((16, 64))
1198 );
1199 for i in 0..16 {
1201 let id = format!("sa_{i}");
1202 assert_eq!(rt.reserve_delegation(&id, None), Ok(1), "worker {i}");
1203 rt.resolve_and_authorize(&id, None).unwrap();
1204 }
1205 assert_eq!(
1206 rt.reserve_delegation("sa_16", None),
1207 Ok(1),
1208 "the 17th root child is not refused by the tree"
1209 );
1210 let denial = rt.resolve_and_authorize("sa_16", None).unwrap_err();
1211 assert_eq!(denial.code, "concurrency_limit");
1212 assert!(
1213 denial.remediation.contains("subagent.max_concurrent"),
1214 "{}",
1215 denial.remediation
1216 );
1217 assert!(denial.remediation.contains("Changing model does not"));
1218 }
1219
1220 #[test]
1221 fn capacity_returns_only_after_reconcile_and_concurrency_still_applies() {
1222 let rt = baseline(1, 2);
1223 rt.authorize("a", &rt.foreground_model()).unwrap();
1224 let denial = rt.resolve_and_authorize("b", None).unwrap_err();
1225 assert_eq!(denial.code, "concurrency_limit");
1226 assert!(denial.remediation.contains("Changing model does not"));
1227 rt.terminal_and_collect("a", WorkerTerminal::Failed)
1228 .unwrap();
1229 rt.authorize("b", &rt.foreground_model()).unwrap();
1230 rt.terminal_and_collect("b", WorkerTerminal::TimedOut)
1231 .unwrap();
1232 assert_eq!(
1233 rt.resolve_and_authorize("c", None).unwrap_err().code,
1234 "total_limit"
1235 );
1236 rt.reconcile("a").unwrap();
1237 rt.resolve_and_authorize("c", None).unwrap();
1238 assert!(matches!(
1239 rt.completion_gate(),
1240 CompletionGate::Blocked { .. }
1241 ));
1242 }
1243}