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