Skip to main content

tact_subagents/
runtime.rs

1//! Async child-agent sessions, turns, and lifecycle orchestration.
2
3use super::{
4    capacity::{Capacity, TurnCapacity},
5    harness::{self, HarnessHandle},
6    message::MessageThreads,
7    model::{
8        AgentDescriptor, AgentId, AgentMessage, AgentMessageUpdate, AgentStatus, AgentThread,
9        AgentUpdate, MessageDeliveryState, MessageDisposition, MessageId, MessagePriority,
10        MessagePurpose, MessageSender, ScopedAgentUpdate, SubagentRuntimeId, ThreadId,
11    },
12    task_tree::TaskTree,
13};
14use futures_util::future::join_all;
15use jsonschema::Validator;
16use nanocodex::{AgentEvents, Model, Nanocodex, NanocodexError, Thinking};
17use serde::Serialize;
18use serde_json::Value;
19use std::{
20    collections::HashMap,
21    sync::{Arc, Mutex, OnceLock, Weak},
22    time::Duration,
23};
24use thiserror::Error;
25use tokio::{
26    sync::{mpsc, oneshot, watch},
27    task::JoinHandle,
28    time::{Instant, timeout_at},
29};
30
31pub(super) struct ChildSession {
32    pub(super) descriptor: AgentDescriptor,
33    pub(super) event_task: Option<JoinHandle<()>>,
34    pub(super) harness: Option<HarnessHandle>,
35    pub(super) harness_task: Option<JoinHandle<()>>,
36    pub(super) status: AgentStatus,
37    pub(super) active: bool,
38    pub(super) output_validator: Validator,
39    pub(super) next_turn_token: u64,
40    pub(super) active_turn_token: Option<u64>,
41    pub(super) steering: bool,
42    pub(super) submitted_output: Option<Value>,
43    pub(super) last_output: Option<Value>,
44}
45
46pub(super) struct OutputContract {
47    validator: Validator,
48    schema: String,
49}
50
51impl OutputContract {
52    pub(super) fn compile(schema: &Value) -> std::io::Result<Self> {
53        let validator = jsonschema::validator_for(schema)
54            .map_err(|error| std::io::Error::other(format!("invalid output_schema: {error}")))?;
55        let schema = serde_json::to_string_pretty(schema)
56            .map_err(|error| std::io::Error::other(format!("could not render schema: {error}")))?;
57        Ok(Self { validator, schema })
58    }
59}
60
61pub(super) fn completion_instructions(schema: &str, turn_token: u64) -> String {
62    format!(
63        "Your contractual result is not prose. Before finishing, use Code Mode to call \
64         `await tools.submit_result({{ turn_token: {turn_token}, output: ... }})` exactly once \
65         with a JSON value matching the output schema below. If validation rejects the value, \
66         correct it and retry. A turn that ends without an accepted result fails.\n\nOutput \
67         schema:\n{schema}"
68    )
69}
70
71pub(crate) struct Registry {
72    id: SubagentRuntimeId,
73    state: tokio::sync::Mutex<RegistryState>,
74    pub(super) updates: mpsc::UnboundedSender<ScopedAgentUpdate>,
75    revision: watch::Sender<u64>,
76    capacity: Capacity,
77    message_lock: tokio::sync::Mutex<()>,
78    agent_factory: OnceLock<AgentFactory>,
79}
80
81/// Owns the clean-agent recipe and the settings inherited by the next spawn.
82/// Per-agent tool factories hold only a weak registry reference, keeping this ownership acyclic.
83struct AgentFactory {
84    build: Box<AgentBuilder>,
85    settings: Mutex<AgentSettings>,
86}
87
88type AgentBuilder =
89    dyn Fn(Model, Thinking, bool) -> Result<(Nanocodex, AgentEvents), NanocodexError> + Send + Sync;
90
91#[derive(Clone, Copy)]
92struct AgentSettings {
93    thinking: Thinking,
94    fast_mode: bool,
95}
96
97/// Identifies whether a tool call belongs to a coordinating root agent.
98///
99/// Clean subagents are registered by session before they can execute a turn. Root sessions and
100/// user-created forks are intentionally absent from that map and retain mutation authority.
101#[derive(Clone)]
102pub struct RootAgentAuthority {
103    registry: Weak<Registry>,
104}
105
106/// Failure to establish root-session authority.
107#[derive(Clone, Copy, Debug, Eq, Error, PartialEq)]
108pub enum AuthorityError {
109    /// The owning subagent runtime has already closed.
110    #[error("subagent runtime is closed")]
111    RuntimeClosed,
112    /// The caller is a registered child session.
113    #[error("operation is only available to root agents")]
114    ChildSession,
115}
116
117#[derive(Default)]
118pub(super) struct RegistryState {
119    root_by_session: HashMap<String, String>,
120    scopes: HashMap<String, AgentScope>,
121}
122
123#[derive(Default)]
124struct AgentScope {
125    topology: TaskTree,
126    sessions: HashMap<AgentId, ChildSession>,
127    messages: MessageThreads,
128}
129
130pub(super) struct AgentReservation {
131    pub(super) root_session_id: String,
132    pub(super) id: AgentId,
133    pub(super) parent: Option<AgentId>,
134}
135
136pub(super) struct CloseRequest {
137    pub(super) root_session_id: String,
138    pub(super) ids: Vec<AgentId>,
139    pub(super) harnesses: Vec<HarnessHandle>,
140    pub(super) status_updates: Vec<(AgentId, AgentStatus)>,
141}
142
143pub(super) struct ClosedSessions {
144    pub(super) summaries: Vec<AgentSummary>,
145    pub(super) harness_tasks: Vec<JoinHandle<()>>,
146    pub(super) event_tasks: Vec<JoinHandle<()>>,
147}
148
149#[derive(Clone, Serialize)]
150pub(super) struct AgentSummary {
151    pub(super) agent_id: AgentId,
152    pub(super) model: Model,
153    pub(super) role: String,
154    pub(super) task: String,
155    pub(super) parent_agent_id: Option<AgentId>,
156    pub(super) status: AgentStatus,
157    #[serde(skip_serializing_if = "Option::is_none")]
158    pub(super) last_output: Option<Value>,
159}
160
161#[derive(Serialize)]
162pub(super) struct AgentDirectoryEntry {
163    pub(super) agent_id: AgentId,
164    pub(super) model: Model,
165    pub(super) role: String,
166    pub(super) task: String,
167    pub(super) parent_agent_id: Option<AgentId>,
168    pub(super) status: AgentStatus,
169    pub(super) can_message: bool,
170    pub(super) can_manage: bool,
171}
172
173#[derive(Debug, Serialize)]
174pub(super) struct MessageReceipt {
175    pub(super) message_id: MessageId,
176    pub(super) thread_id: ThreadId,
177    pub(super) from: MessageSender,
178    pub(super) to_agent_id: AgentId,
179    pub(super) disposition: MessageDisposition,
180}
181
182struct PreparedMessage {
183    root_session_id: String,
184    message: AgentMessage,
185    harness: HarnessHandle,
186}
187
188pub(super) struct DelegationChange {
189    target: AgentId,
190    previous_task: String,
191}
192
193pub(super) struct TurnSteer {
194    id: AgentId,
195    previous_token: u64,
196    token: u64,
197}
198
199impl TurnSteer {
200    pub(super) const fn token(&self) -> u64 {
201        self.token
202    }
203}
204
205impl RegistryState {
206    fn submit_result(
207        &mut self,
208        session_id: &str,
209        turn_token: u64,
210        output: Value,
211    ) -> std::io::Result<()> {
212        let root_session_id = self.root_session_id(session_id).to_owned();
213        let scope = self
214            .scopes
215            .get_mut(&root_session_id)
216            .ok_or_else(|| std::io::Error::other("submit_result is only available to subagents"))?;
217        let id = scope
218            .topology
219            .agent_for_session(session_id)
220            .ok_or_else(|| std::io::Error::other("submit_result is only available to subagents"))?;
221        let session = scope
222            .sessions
223            .get_mut(&id)
224            .ok_or_else(|| std::io::Error::other("subagent session disappeared"))?;
225        if !session.active {
226            return Err(std::io::Error::other(
227                "submit_result is only available during an active subagent turn",
228            ));
229        }
230        if session.steering {
231            return Err(std::io::Error::other(
232                "the subagent turn is being steered; retry submit_result",
233            ));
234        }
235        if session.active_turn_token != Some(turn_token) {
236            return Err(std::io::Error::other(
237                "submit_result used a stale or unknown turn_token",
238            ));
239        }
240        if session.submitted_output.is_some() {
241            return Err(std::io::Error::other(
242                "submit_result already accepted one result for this turn",
243            ));
244        }
245        let errors = session
246            .output_validator
247            .iter_errors(&output)
248            .take(4)
249            .map(|error| error.to_string())
250            .collect::<Vec<_>>();
251        if !errors.is_empty() {
252            return Err(std::io::Error::other(format!(
253                "submitted output does not match the required schema: {}",
254                errors.join("; ")
255            )));
256        }
257        session.submitted_output = Some(output);
258        Ok(())
259    }
260
261    fn begin_turn_steer(&mut self, root_session_id: &str, id: AgentId) -> Option<TurnSteer> {
262        let session = self
263            .scopes
264            .get_mut(root_session_id)?
265            .sessions
266            .get_mut(&id)?;
267        if !session.active || session.steering || session.submitted_output.is_some() {
268            return None;
269        }
270        let previous_token = session.active_turn_token?;
271        let token = session.next_turn_token.checked_add(1)?;
272        session.next_turn_token = token;
273        session.active_turn_token = Some(token);
274        session.steering = true;
275        Some(TurnSteer {
276            id,
277            previous_token,
278            token,
279        })
280    }
281
282    fn finish_turn_steer(&mut self, root_session_id: &str, steer: TurnSteer, committed: bool) {
283        let Some(session) = self
284            .scopes
285            .get_mut(root_session_id)
286            .and_then(|scope| scope.sessions.get_mut(&steer.id))
287        else {
288            return;
289        };
290        if session.active_turn_token != Some(steer.token) {
291            return;
292        }
293        if !committed {
294            session.active_turn_token = Some(steer.previous_token);
295        }
296        session.steering = false;
297    }
298
299    fn reserve_for(&mut self, session_id: &str) -> std::io::Result<AgentReservation> {
300        let root_session_id = self.root_session_id(session_id).to_owned();
301        let parent = self
302            .scopes
303            .get(&root_session_id)
304            .and_then(|scope| scope.topology.agent_for_session(session_id));
305        if let Some(parent) = parent {
306            let parent_session = self
307                .scopes
308                .get(&root_session_id)
309                .and_then(|scope| scope.sessions.get(&parent))
310                .ok_or_else(|| std::io::Error::other("subagent parent disappeared"))?;
311            if matches!(
312                parent_session.status,
313                AgentStatus::Closing | AgentStatus::Closed
314            ) {
315                return Err(std::io::Error::other(format!(
316                    "agent {parent} is closing and cannot spawn children"
317                )));
318            }
319        }
320        self.reserve(&root_session_id, parent)
321    }
322
323    fn reserve(
324        &mut self,
325        session_id: &str,
326        parent: Option<AgentId>,
327    ) -> std::io::Result<AgentReservation> {
328        let root_session_id = self.root_session_id(session_id).to_owned();
329        let id = self.scope_mut(&root_session_id).topology.reserve(parent)?;
330        Ok(AgentReservation {
331            root_session_id,
332            id,
333            parent,
334        })
335    }
336
337    fn insert(
338        &mut self,
339        root_session_id: String,
340        id: AgentId,
341        session_id: String,
342        session: ChildSession,
343    ) -> std::io::Result<()> {
344        if let Some(parent) = session.descriptor.parent {
345            let parent_session = self
346                .scopes
347                .get(&root_session_id)
348                .and_then(|scope| scope.sessions.get(&parent))
349                .ok_or_else(|| std::io::Error::other(format!("unknown parent agent {parent}")))?;
350            if matches!(
351                parent_session.status,
352                AgentStatus::Closing | AgentStatus::Closed
353            ) {
354                return Err(std::io::Error::other(format!(
355                    "agent {parent} stopped while spawning child {id}"
356                )));
357            }
358        }
359        self.scope_mut(&root_session_id).topology.insert(
360            id,
361            session_id.clone(),
362            session.descriptor.parent,
363        )?;
364        self.root_by_session
365            .insert(session_id, root_session_id.clone());
366        self.scope_mut(&root_session_id)
367            .sessions
368            .insert(id, session);
369        Ok(())
370    }
371
372    fn harness_in_scope(
373        &self,
374        root_session_id: &str,
375        id: AgentId,
376    ) -> std::io::Result<HarnessHandle> {
377        self.scopes
378            .get(root_session_id)
379            .and_then(|scope| scope.sessions.get(&id))
380            .and_then(|session| session.harness.clone())
381            .ok_or_else(|| std::io::Error::other(format!("agent {id} is closed")))
382    }
383
384    fn directory(
385        &self,
386        session_id: &str,
387        include_completed: bool,
388        include_self: bool,
389    ) -> Vec<AgentDirectoryEntry> {
390        let root_session_id = self.root_session_id(session_id);
391        let Some(scope) = self.scopes.get(root_session_id) else {
392            return Vec::new();
393        };
394        let caller = scope.topology.agent_for_session(session_id);
395        let mut ids = scope.topology.ids();
396        ids.sort_unstable();
397        ids.into_iter()
398            .filter(|id| include_self || caller != Some(*id))
399            .filter_map(|id| {
400                let session = scope.sessions.get(&id)?;
401                if !include_completed
402                    && !matches!(session.status, AgentStatus::Pending | AgentStatus::Running)
403                {
404                    return None;
405                }
406                let can_message = caller != Some(id)
407                    && !matches!(
408                        session.status,
409                        AgentStatus::Pending | AgentStatus::Closing | AgentStatus::Closed
410                    );
411                let can_manage = can_message && scope.topology.authorize(session_id, id).is_ok();
412                Some(AgentDirectoryEntry {
413                    agent_id: id,
414                    model: session.descriptor.model,
415                    role: bounded_summary(&session.descriptor.role),
416                    task: bounded_summary(&session.descriptor.task),
417                    parent_agent_id: session.descriptor.parent,
418                    status: session.status.clone(),
419                    can_message,
420                    can_manage,
421                })
422            })
423            .collect()
424    }
425
426    fn prepare_message(
427        &mut self,
428        session_id: &str,
429        to: AgentId,
430        priority: MessagePriority,
431        purpose: MessagePurpose,
432        in_reply_to: Option<MessageId>,
433        body: String,
434    ) -> std::io::Result<PreparedMessage> {
435        let root_session_id = self.root_session_id(session_id).to_owned();
436        let scope = self
437            .scopes
438            .get_mut(&root_session_id)
439            .ok_or_else(|| std::io::Error::other(format!("unknown agent_id {to}")))?;
440        let from = scope
441            .topology
442            .agent_for_session(session_id)
443            .map_or(MessageSender::Root, |agent_id| MessageSender::Agent {
444                agent_id,
445            });
446        if from.agent_id() == Some(to) {
447            return Err(std::io::Error::other("agents cannot message themselves"));
448        }
449        if purpose == MessagePurpose::Delegate {
450            scope.topology.authorize(session_id, to)?;
451        }
452        let target = scope
453            .sessions
454            .get(&to)
455            .ok_or_else(|| std::io::Error::other(format!("unknown agent_id {to}")))?;
456        if matches!(target.status, AgentStatus::Pending) {
457            return Err(std::io::Error::other(format!(
458                "agent {to} has not started and cannot receive messages yet"
459            )));
460        }
461        if matches!(target.status, AgentStatus::Closing | AgentStatus::Closed) {
462            return Err(std::io::Error::other(format!(
463                "agent {to} is {:?} and cannot receive messages",
464                target.status
465            )));
466        }
467        let harness = target
468            .harness
469            .clone()
470            .ok_or_else(|| std::io::Error::other(format!("agent {to} is closed")))?;
471        let message = scope
472            .messages
473            .prepare(from, to, priority, purpose, in_reply_to, body)?;
474        Ok(PreparedMessage {
475            root_session_id,
476            message,
477            harness,
478        })
479    }
480
481    fn commit_message(
482        &mut self,
483        root_session_id: &str,
484        message: AgentMessage,
485    ) -> std::io::Result<AgentThread> {
486        let scope = self
487            .scopes
488            .get_mut(root_session_id)
489            .ok_or_else(|| std::io::Error::other("subagent scope disappeared"))?;
490        Ok(scope.messages.commit(message))
491    }
492
493    fn rollback_message(&mut self, root_session_id: &str, id: MessageId) {
494        if let Some(scope) = self.scopes.get_mut(root_session_id) {
495            scope.messages.rollback(id);
496        }
497    }
498
499    fn begin_delegation(
500        &mut self,
501        root_session_id: &str,
502        id: MessageId,
503    ) -> Option<(DelegationChange, AgentDescriptor)> {
504        let scope = self.scopes.get_mut(root_session_id)?;
505        let message = scope.messages.message(id)?;
506        if message.purpose != MessagePurpose::Delegate {
507            return None;
508        }
509        let target = scope.sessions.get_mut(&message.to)?;
510        let previous_task = std::mem::replace(&mut target.descriptor.task, message.body);
511        Some((
512            DelegationChange {
513                target: message.to,
514                previous_task,
515            },
516            target.descriptor.clone(),
517        ))
518    }
519
520    fn rollback_delegation(
521        &mut self,
522        root_session_id: &str,
523        change: DelegationChange,
524    ) -> Option<AgentDescriptor> {
525        let target = self
526            .scopes
527            .get_mut(root_session_id)?
528            .sessions
529            .get_mut(&change.target)?;
530        target.descriptor.task = change.previous_task;
531        Some(target.descriptor.clone())
532    }
533
534    fn thread_for_message(&self, root_session_id: &str, id: MessageId) -> Option<AgentThread> {
535        self.scopes
536            .get(root_session_id)
537            .and_then(|scope| scope.messages.thread_for_message(id))
538    }
539
540    fn mark_message_admitted(
541        &mut self,
542        root_session_id: &str,
543        id: MessageId,
544        disposition: MessageDisposition,
545    ) {
546        if let Some(scope) = self.scopes.get_mut(root_session_id) {
547            scope.messages.mark_admitted(id, disposition);
548        }
549    }
550
551    fn mark_message_terminal(&mut self, root_session_id: &str, id: MessageId) {
552        if let Some(scope) = self.scopes.get_mut(root_session_id) {
553            scope.messages.mark_terminal(id);
554        }
555    }
556
557    fn summaries(&self, session_id: &str, ids: &[AgentId]) -> std::io::Result<Vec<AgentSummary>> {
558        let root_session_id = self.root_session_id(session_id);
559        for &id in ids {
560            self.authorize(session_id, id)?;
561        }
562        self.summaries_in_scope(root_session_id, ids)
563    }
564
565    fn summaries_in_scope(
566        &self,
567        root_session_id: &str,
568        ids: &[AgentId],
569    ) -> std::io::Result<Vec<AgentSummary>> {
570        let scope = self
571            .scopes
572            .get(root_session_id)
573            .ok_or_else(|| std::io::Error::other("subagent scope disappeared"))?;
574        ids.iter()
575            .map(|id| {
576                scope
577                    .sessions
578                    .get(id)
579                    .map(ChildSession::summary)
580                    .ok_or_else(|| std::io::Error::other(format!("unknown agent_id {id}")))
581            })
582            .collect()
583    }
584
585    fn request_interrupt(
586        &mut self,
587        session_id: &str,
588        id: AgentId,
589    ) -> std::io::Result<(String, Vec<AgentId>, Vec<HarnessHandle>)> {
590        let root_session_id = self.authorize(session_id, id)?;
591        let ids = self.subtree_shutdown_order(&root_session_id, id)?;
592        let harnesses = self.harnesses(&root_session_id, &ids, false)?;
593        Ok((root_session_id, ids, harnesses))
594    }
595
596    fn request_close(&mut self, session_id: &str, id: AgentId) -> std::io::Result<CloseRequest> {
597        let root_session_id = self.authorize(session_id, id)?;
598        let ids = self.subtree_shutdown_order(&root_session_id, id)?;
599        let harnesses = self.harnesses(&root_session_id, &ids, true)?;
600        let status_updates = ids
601            .iter()
602            .copied()
603            .map(|id| (id, AgentStatus::Closing))
604            .collect();
605        Ok(CloseRequest {
606            root_session_id,
607            ids,
608            harnesses,
609            status_updates,
610        })
611    }
612
613    fn request_close_all(&mut self, session_id: &str) -> std::io::Result<CloseRequest> {
614        let root_session_id = self.root_session_id(session_id).to_owned();
615        let Some(scope) = self.scopes.get(&root_session_id) else {
616            return Ok(CloseRequest {
617                root_session_id,
618                ids: Vec::new(),
619                harnesses: Vec::new(),
620                status_updates: Vec::new(),
621            });
622        };
623        let ids = scope.topology.all_postorder();
624        let harnesses = self.harnesses(&root_session_id, &ids, true)?;
625        let status_updates = ids
626            .iter()
627            .copied()
628            .map(|id| (id, AgentStatus::Closing))
629            .collect();
630        Ok(CloseRequest {
631            root_session_id,
632            ids,
633            harnesses,
634            status_updates,
635        })
636    }
637
638    fn request_interrupt_all(
639        &mut self,
640        session_id: &str,
641    ) -> (String, Vec<AgentId>, Vec<HarnessHandle>) {
642        let root_session_id = self.root_session_id(session_id).to_owned();
643        let ids = self
644            .scopes
645            .get(&root_session_id)
646            .map(|scope| scope.topology.ids())
647            .unwrap_or_default();
648        let harnesses = self
649            .harnesses(&root_session_id, &ids, false)
650            .unwrap_or_default();
651        (root_session_id, ids, harnesses)
652    }
653
654    fn harnesses(
655        &mut self,
656        root_session_id: &str,
657        ids: &[AgentId],
658        closing: bool,
659    ) -> std::io::Result<Vec<HarnessHandle>> {
660        let scope = self
661            .scopes
662            .get_mut(root_session_id)
663            .ok_or_else(|| std::io::Error::other("subagent scope disappeared"))?;
664        let mut harnesses = Vec::new();
665        for id in ids {
666            let session = scope
667                .sessions
668                .get_mut(id)
669                .ok_or_else(|| std::io::Error::other(format!("unknown agent_id {id}")))?;
670            if closing {
671                session.status = AgentStatus::Closing;
672            }
673            harnesses.extend(session.harness.iter().cloned());
674        }
675        Ok(harnesses)
676    }
677
678    fn finish_close(
679        &mut self,
680        root_session_id: &str,
681        ids: &[AgentId],
682    ) -> std::io::Result<ClosedSessions> {
683        let scope = self
684            .scopes
685            .get_mut(root_session_id)
686            .ok_or_else(|| std::io::Error::other("subagent scope disappeared"))?;
687        let mut harness_tasks = Vec::new();
688        let mut event_tasks = Vec::new();
689        for id in ids {
690            let session = scope
691                .sessions
692                .get_mut(id)
693                .ok_or_else(|| std::io::Error::other(format!("unknown agent_id {id}")))?;
694            if session.active {
695                return Err(std::io::Error::other(format!(
696                    "agent {id} is still running"
697                )));
698            }
699            session.harness = None;
700            harness_tasks.extend(session.harness_task.take());
701            event_tasks.extend(session.event_task.take());
702            session.status = AgentStatus::Closed;
703        }
704        let summaries = ids
705            .iter()
706            .filter_map(|id| scope.sessions.get(id).map(ChildSession::summary))
707            .collect();
708        Ok(ClosedSessions {
709            summaries,
710            harness_tasks,
711            event_tasks,
712        })
713    }
714
715    fn all_inactive(&self, root_session_id: &str, ids: &[AgentId]) -> std::io::Result<bool> {
716        let scope = self
717            .scopes
718            .get(root_session_id)
719            .ok_or_else(|| std::io::Error::other("subagent scope disappeared"))?;
720        Ok(ids.iter().all(|id| {
721            scope
722                .sessions
723                .get(id)
724                .is_some_and(|session| !session.active)
725        }))
726    }
727
728    fn subtree_shutdown_order(
729        &self,
730        root_session_id: &str,
731        id: AgentId,
732    ) -> std::io::Result<Vec<AgentId>> {
733        self.scopes
734            .get(root_session_id)
735            .ok_or_else(|| std::io::Error::other("subagent scope disappeared"))?
736            .topology
737            .subtree_postorder(id)
738    }
739
740    fn authorize(&self, session_id: &str, id: AgentId) -> std::io::Result<String> {
741        let root_session_id = self.root_session_id(session_id);
742        self.scopes
743            .get(root_session_id)
744            .ok_or_else(|| std::io::Error::other(format!("unknown agent_id {id}")))?
745            .topology
746            .authorize(session_id, id)?;
747        Ok(root_session_id.to_owned())
748    }
749
750    fn root_session_id<'a>(&'a self, session_id: &'a str) -> &'a str {
751        self.root_by_session
752            .get(session_id)
753            .map_or(session_id, String::as_str)
754    }
755
756    fn scope_mut(&mut self, root_session_id: &str) -> &mut AgentScope {
757        self.scopes.entry(root_session_id.to_owned()).or_default()
758    }
759}
760
761const AGENT_STOP_TIMEOUT: Duration = Duration::from_secs(30);
762
763impl Registry {
764    pub(super) fn new(
765        updates: mpsc::UnboundedSender<ScopedAgentUpdate>,
766        max_concurrency: usize,
767    ) -> Self {
768        let (revision, _) = watch::channel(0);
769        Self {
770            id: SubagentRuntimeId::next(),
771            state: tokio::sync::Mutex::new(RegistryState::default()),
772            updates,
773            revision,
774            capacity: Capacity::new(max_concurrency),
775            message_lock: tokio::sync::Mutex::new(()),
776            agent_factory: OnceLock::new(),
777        }
778    }
779
780    pub(crate) fn set_agent_factory<F>(
781        &self,
782        thinking: Thinking,
783        fast_mode: bool,
784        factory: F,
785    ) -> Result<(), NanocodexError>
786    where
787        F: Fn(Model, Thinking, bool) -> Result<(Nanocodex, AgentEvents), NanocodexError>
788            + Send
789            + Sync
790            + 'static,
791    {
792        self.agent_factory
793            .set(AgentFactory {
794                build: Box::new(factory),
795                settings: Mutex::new(AgentSettings {
796                    thinking,
797                    fast_mode,
798                }),
799            })
800            .map_err(|_| {
801                NanocodexError::InvalidRequest("subagent factory is already configured".to_owned())
802            })
803    }
804
805    pub(super) fn spawn_agent(
806        &self,
807        model: Model,
808    ) -> Result<(Nanocodex, AgentEvents), NanocodexError> {
809        let factory = self.agent_factory.get().ok_or_else(|| {
810            NanocodexError::InvalidRequest("subagent factory is not configured".to_owned())
811        })?;
812        let settings = *factory
813            .settings
814            .lock()
815            .expect("subagent settings lock should not be poisoned");
816        (factory.build)(model, settings.thinking, settings.fast_mode)
817    }
818
819    fn set_agent_thinking(&self, thinking: Thinking) {
820        if let Some(factory) = self.agent_factory.get() {
821            factory
822                .settings
823                .lock()
824                .expect("subagent settings lock should not be poisoned")
825                .thinking = thinking;
826        }
827    }
828
829    fn set_agent_fast_mode(&self, fast_mode: bool) {
830        if let Some(factory) = self.agent_factory.get() {
831            factory
832                .settings
833                .lock()
834                .expect("subagent settings lock should not be poisoned")
835                .fast_mode = fast_mode;
836        }
837    }
838
839    pub(super) fn reserve_turn(&self) -> std::io::Result<TurnCapacity> {
840        self.capacity.reserve()
841    }
842
843    pub(super) fn set_max_concurrency(&self, limit: usize) {
844        self.capacity.set_limit(limit);
845    }
846
847    async fn is_root_session(&self, session_id: &str) -> bool {
848        !self
849            .state
850            .lock()
851            .await
852            .root_by_session
853            .contains_key(session_id)
854    }
855
856    pub(super) async fn reserve(&self, session_id: &str) -> std::io::Result<AgentReservation> {
857        self.state.lock().await.reserve_for(session_id)
858    }
859
860    pub(super) async fn submit_result(
861        &self,
862        session_id: &str,
863        turn_token: u64,
864        output: Value,
865    ) -> std::io::Result<()> {
866        self.state
867            .lock()
868            .await
869            .submit_result(session_id, turn_token, output)
870    }
871
872    pub(super) async fn begin_turn_steer(
873        &self,
874        root_session_id: &str,
875        id: AgentId,
876    ) -> Option<TurnSteer> {
877        self.state
878            .lock()
879            .await
880            .begin_turn_steer(root_session_id, id)
881    }
882
883    pub(super) async fn finish_turn_steer(
884        &self,
885        root_session_id: &str,
886        steer: TurnSteer,
887        committed: bool,
888    ) {
889        self.state
890            .lock()
891            .await
892            .finish_turn_steer(root_session_id, steer, committed);
893    }
894
895    pub(super) async fn insert(
896        self: &Arc<Self>,
897        root_session_id: String,
898        descriptor: AgentDescriptor,
899        agent: Nanocodex,
900        event_task: JoinHandle<()>,
901        contract: OutputContract,
902    ) -> std::io::Result<()> {
903        let OutputContract { validator, schema } = contract;
904        let (harness, harness_task) = harness::spawn(
905            root_session_id.clone(),
906            descriptor.id,
907            agent,
908            self.capacity.clone(),
909            Arc::downgrade(self),
910            schema,
911        );
912        self.state.lock().await.insert(
913            root_session_id,
914            descriptor.id,
915            descriptor.session_id.clone(),
916            ChildSession {
917                descriptor,
918                event_task: Some(event_task),
919                harness: Some(harness),
920                harness_task: Some(harness_task),
921                status: AgentStatus::Pending,
922                active: false,
923                output_validator: validator,
924                next_turn_token: 0,
925                active_turn_token: None,
926                steering: false,
927                submitted_output: None,
928                last_output: None,
929            },
930        )?;
931        self.changed();
932        Ok(())
933    }
934
935    pub(super) async fn launch_initial_turn(
936        self: &Arc<Self>,
937        root_session_id: &str,
938        id: AgentId,
939        prompt: String,
940        capacity: TurnCapacity,
941    ) -> std::io::Result<()> {
942        let harness = self
943            .state
944            .lock()
945            .await
946            .harness_in_scope(root_session_id, id)?;
947        harness.start(prompt, capacity).await
948    }
949
950    pub(super) async fn harness_turn_started(
951        &self,
952        root_session_id: &str,
953        id: AgentId,
954    ) -> Option<u64> {
955        let token = {
956            let mut state = self.state.lock().await;
957            let session = state
958                .scopes
959                .get_mut(root_session_id)
960                .and_then(|scope| scope.sessions.get_mut(&id))?;
961            if !session.status.can_start_turn() || session.active {
962                None
963            } else {
964                let token = session.next_turn_token.checked_add(1)?;
965                session.next_turn_token = token;
966                session.active_turn_token = Some(token);
967                session.active = true;
968                session.steering = false;
969                session.submitted_output = None;
970                session.status = AgentStatus::Running;
971                Some(token)
972            }
973        };
974        if token.is_some() {
975            self.send(
976                root_session_id,
977                AgentUpdate::Status {
978                    id,
979                    status: AgentStatus::Running,
980                },
981            );
982            self.changed();
983        }
984        token
985    }
986
987    pub(super) async fn harness_turn_start_failed(
988        &self,
989        root_session_id: &str,
990        id: AgentId,
991        error: String,
992    ) {
993        let status = {
994            let mut state = self.state.lock().await;
995            let Some(session) = state
996                .scopes
997                .get_mut(root_session_id)
998                .and_then(|scope| scope.sessions.get_mut(&id))
999            else {
1000                return;
1001            };
1002            session.active = false;
1003            session.active_turn_token = None;
1004            session.steering = false;
1005            session.submitted_output = None;
1006            if !matches!(session.status, AgentStatus::Closing | AgentStatus::Closed) {
1007                session.status = AgentStatus::Failed { error };
1008            }
1009            session.status.clone()
1010        };
1011        self.send(root_session_id, AgentUpdate::Status { id, status });
1012        self.changed();
1013    }
1014
1015    pub(super) async fn harness_turn_finished(
1016        &self,
1017        root_session_id: &str,
1018        id: AgentId,
1019        result: nanocodex::agent::Result<nanocodex::TurnResult>,
1020    ) {
1021        let status = {
1022            let mut state = self.state.lock().await;
1023            let Some(session) = state
1024                .scopes
1025                .get_mut(root_session_id)
1026                .and_then(|scope| scope.sessions.get_mut(&id))
1027            else {
1028                return;
1029            };
1030            if !session.active {
1031                return;
1032            }
1033            session.active = false;
1034            session.active_turn_token = None;
1035            session.steering = false;
1036            let submitted_output = session.submitted_output.take();
1037            if matches!(session.status, AgentStatus::Closing | AgentStatus::Closed) {
1038                session.status.clone()
1039            } else {
1040                match result {
1041                    Ok(_) => complete_session(session, submitted_output),
1042                    Err(NanocodexError::TurnCancelled) => AgentStatus::Interrupted,
1043                    Err(error) => AgentStatus::Failed {
1044                        error: error.to_string(),
1045                    },
1046                }
1047            }
1048            .clone_into(&mut session.status);
1049            session.status.clone()
1050        };
1051        self.send(root_session_id, AgentUpdate::Status { id, status });
1052        self.changed();
1053    }
1054
1055    pub(super) async fn harness_closed(&self, root_session_id: &str, id: AgentId) {
1056        let changed = {
1057            let mut state = self.state.lock().await;
1058            let Some(session) = state
1059                .scopes
1060                .get_mut(root_session_id)
1061                .and_then(|scope| scope.sessions.get_mut(&id))
1062            else {
1063                return;
1064            };
1065            if matches!(session.status, AgentStatus::Closed) {
1066                false
1067            } else {
1068                session.active = false;
1069                session.active_turn_token = None;
1070                session.steering = false;
1071                session.submitted_output = None;
1072                session.status = AgentStatus::Closed;
1073                true
1074            }
1075        };
1076        if changed {
1077            self.send(
1078                root_session_id,
1079                AgentUpdate::Status {
1080                    id,
1081                    status: AgentStatus::Closed,
1082                },
1083            );
1084            self.changed();
1085        }
1086    }
1087
1088    async fn runtime_closed(&self, root_session_id: &str, id: AgentId) {
1089        let harness = {
1090            let state = self.state.lock().await;
1091            state
1092                .scopes
1093                .get(root_session_id)
1094                .and_then(|scope| scope.sessions.get(&id))
1095                .filter(|session| {
1096                    !matches!(session.status, AgentStatus::Closing | AgentStatus::Closed)
1097                })
1098                .and_then(|session| session.harness.clone())
1099        };
1100        let Some(harness) = harness else {
1101            self.harness_closed(root_session_id, id).await;
1102            return;
1103        };
1104        drop(harness.close().await);
1105    }
1106
1107    pub(super) fn send(&self, root_session_id: &str, update: AgentUpdate) {
1108        let _ = send_update(&self.updates, root_session_id, update);
1109    }
1110
1111    pub(super) async fn directory(
1112        &self,
1113        session_id: &str,
1114        include_completed: bool,
1115        include_self: bool,
1116    ) -> Vec<AgentDirectoryEntry> {
1117        self.state
1118            .lock()
1119            .await
1120            .directory(session_id, include_completed, include_self)
1121    }
1122
1123    pub(super) async fn send_message(
1124        &self,
1125        session_id: &str,
1126        to: AgentId,
1127        priority: MessagePriority,
1128        purpose: MessagePurpose,
1129        in_reply_to: Option<MessageId>,
1130        body: String,
1131    ) -> std::io::Result<MessageReceipt> {
1132        let _message_guard = self.message_lock.lock().await;
1133        let prepared = self.state.lock().await.prepare_message(
1134            session_id,
1135            to,
1136            priority,
1137            purpose,
1138            in_reply_to,
1139            body,
1140        )?;
1141        let delivery = prepared
1142            .harness
1143            .enqueue_delivery(prepared.message.clone())?;
1144        self.state
1145            .lock()
1146            .await
1147            .commit_message(&prepared.root_session_id, prepared.message.clone())?;
1148        let disposition = match delivery.release().await {
1149            Ok(disposition) => disposition,
1150            Err(error) => {
1151                self.state
1152                    .lock()
1153                    .await
1154                    .rollback_message(&prepared.root_session_id, prepared.message.id);
1155                return Err(error);
1156            }
1157        };
1158        Ok(MessageReceipt {
1159            message_id: prepared.message.id,
1160            thread_id: prepared.message.thread_id,
1161            from: prepared.message.from,
1162            to_agent_id: prepared.message.to,
1163            disposition,
1164        })
1165    }
1166
1167    pub(super) async fn message_admitted(
1168        &self,
1169        root_session_id: &str,
1170        id: MessageId,
1171        disposition: MessageDisposition,
1172    ) {
1173        let thread = {
1174            let mut state = self.state.lock().await;
1175            let thread = state.thread_for_message(root_session_id, id);
1176            state.mark_message_admitted(root_session_id, id, disposition);
1177            thread
1178        };
1179        let Some(thread) = thread else {
1180            return;
1181        };
1182        self.send(
1183            root_session_id,
1184            AgentUpdate::Message(AgentMessageUpdate {
1185                message_id: id,
1186                thread,
1187                delivery: MessageDeliveryState::Admitted { disposition },
1188            }),
1189        );
1190        self.changed();
1191    }
1192
1193    pub(super) async fn message_rejected(&self, root_session_id: &str, id: MessageId) {
1194        self.state
1195            .lock()
1196            .await
1197            .rollback_message(root_session_id, id);
1198    }
1199
1200    pub(super) async fn message_delivered(
1201        &self,
1202        root_session_id: &str,
1203        id: MessageId,
1204        disposition: MessageDisposition,
1205    ) {
1206        self.publish_message_state(
1207            root_session_id,
1208            id,
1209            MessageDeliveryState::Delivered { disposition },
1210        )
1211        .await;
1212    }
1213
1214    pub(super) async fn begin_message_delivery(
1215        &self,
1216        root_session_id: &str,
1217        id: MessageId,
1218    ) -> Option<DelegationChange> {
1219        let (change, descriptor) = self
1220            .state
1221            .lock()
1222            .await
1223            .begin_delegation(root_session_id, id)?;
1224        self.send(root_session_id, AgentUpdate::Added(descriptor));
1225        self.changed();
1226        Some(change)
1227    }
1228
1229    pub(super) async fn rollback_message_delivery(
1230        &self,
1231        root_session_id: &str,
1232        change: DelegationChange,
1233    ) {
1234        let descriptor = self
1235            .state
1236            .lock()
1237            .await
1238            .rollback_delegation(root_session_id, change);
1239        if let Some(descriptor) = descriptor {
1240            self.send(root_session_id, AgentUpdate::Added(descriptor));
1241            self.changed();
1242        }
1243    }
1244
1245    pub(super) async fn message_failed(&self, root_session_id: &str, id: MessageId, error: String) {
1246        self.publish_message_state(root_session_id, id, MessageDeliveryState::Failed { error })
1247            .await;
1248    }
1249
1250    async fn publish_message_state(
1251        &self,
1252        root_session_id: &str,
1253        message_id: MessageId,
1254        delivery: MessageDeliveryState,
1255    ) {
1256        let thread = self
1257            .state
1258            .lock()
1259            .await
1260            .thread_for_message(root_session_id, message_id);
1261        let Some(thread) = thread else {
1262            return;
1263        };
1264        self.send(
1265            root_session_id,
1266            AgentUpdate::Message(AgentMessageUpdate {
1267                message_id,
1268                thread,
1269                delivery,
1270            }),
1271        );
1272        self.changed();
1273        self.state
1274            .lock()
1275            .await
1276            .mark_message_terminal(root_session_id, message_id);
1277    }
1278
1279    pub(super) async fn wait(
1280        &self,
1281        session_id: &str,
1282        ids: &[AgentId],
1283        duration: Duration,
1284    ) -> std::io::Result<(Vec<AgentSummary>, bool)> {
1285        if ids.is_empty() {
1286            return Err(std::io::Error::other("agent_ids must not be empty"));
1287        }
1288        let mut revision = self.revision.subscribe();
1289        let deadline = Instant::now() + duration;
1290        loop {
1291            let summaries = self.state.lock().await.summaries(session_id, ids)?;
1292            if summaries
1293                .iter()
1294                .any(|summary| summary.status.is_wait_terminal())
1295            {
1296                return Ok((summaries, false));
1297            }
1298            if timeout_at(deadline, revision.changed()).await.is_err() {
1299                let summaries = self.state.lock().await.summaries(session_id, ids)?;
1300                return Ok((summaries, true));
1301            }
1302        }
1303    }
1304
1305    pub(super) async fn interrupt(
1306        &self,
1307        session_id: &str,
1308        id: AgentId,
1309    ) -> std::io::Result<Vec<AgentSummary>> {
1310        let _message_guard = self.message_lock.lock().await;
1311        let (root_session_id, ids, harnesses) = {
1312            let mut state = self.state.lock().await;
1313            state.request_interrupt(session_id, id)?
1314        };
1315        self.changed();
1316        let deadline = Instant::now() + AGENT_STOP_TIMEOUT;
1317        self.interrupt_harnesses(&root_session_id, &ids, harnesses, deadline)
1318            .await?;
1319        self.state
1320            .lock()
1321            .await
1322            .summaries_in_scope(&root_session_id, &ids)
1323    }
1324
1325    pub(super) async fn close(
1326        &self,
1327        session_id: &str,
1328        id: AgentId,
1329    ) -> std::io::Result<Vec<AgentSummary>> {
1330        let _message_guard = self.message_lock.lock().await;
1331        let CloseRequest {
1332            root_session_id,
1333            ids,
1334            harnesses,
1335            status_updates,
1336        } = {
1337            let mut state = self.state.lock().await;
1338            state.request_close(session_id, id)?
1339        };
1340        for (id, status) in status_updates {
1341            self.send(&root_session_id, AgentUpdate::Status { id, status });
1342        }
1343        self.changed();
1344        self.stop_and_close(root_session_id, ids, harnesses).await
1345    }
1346
1347    async fn close_all(&self, session_id: &str) -> std::io::Result<Vec<AgentSummary>> {
1348        let _message_guard = self.message_lock.lock().await;
1349        let CloseRequest {
1350            root_session_id,
1351            ids,
1352            harnesses,
1353            status_updates,
1354        } = {
1355            let mut state = self.state.lock().await;
1356            state.request_close_all(session_id)?
1357        };
1358        for (id, status) in status_updates {
1359            self.send(&root_session_id, AgentUpdate::Status { id, status });
1360        }
1361        self.changed();
1362        self.stop_and_close(root_session_id, ids, harnesses).await
1363    }
1364
1365    async fn stop_and_close(
1366        &self,
1367        root_session_id: String,
1368        ids: Vec<AgentId>,
1369        harnesses: Vec<HarnessHandle>,
1370    ) -> std::io::Result<Vec<AgentSummary>> {
1371        if ids.is_empty() {
1372            return Ok(Vec::new());
1373        }
1374        let deadline = Instant::now() + AGENT_STOP_TIMEOUT;
1375        let closing_result = self.close_harnesses(harnesses, deadline).await;
1376        self.wait_until_inactive(&root_session_id, &ids, deadline)
1377            .await?;
1378        // Closing an already-finished driver can race with its natural shutdown.
1379        // Once every harness reports inactive, command errors no longer identify
1380        // a live resource and must not prevent task handles from being joined.
1381        drop(closing_result);
1382        let ClosedSessions {
1383            summaries,
1384            harness_tasks,
1385            event_tasks,
1386        } = self
1387            .state
1388            .lock()
1389            .await
1390            .finish_close(&root_session_id, &ids)?;
1391        for summary in &summaries {
1392            self.send(
1393                &root_session_id,
1394                AgentUpdate::Status {
1395                    id: summary.agent_id,
1396                    status: AgentStatus::Closed,
1397                },
1398            );
1399        }
1400        self.changed();
1401        self.wait_for_tasks(harness_tasks, deadline, "subagent harnesses")
1402            .await?;
1403        self.wait_for_tasks(event_tasks, deadline, "subagent event streams")
1404            .await?;
1405        Ok(summaries)
1406    }
1407
1408    async fn cancel_all(&self, session_id: &str) {
1409        let _message_guard = self.message_lock.lock().await;
1410        let (root_session_id, ids, harnesses) = {
1411            let mut state = self.state.lock().await;
1412            state.request_interrupt_all(session_id)
1413        };
1414        self.changed();
1415        let deadline = Instant::now() + AGENT_STOP_TIMEOUT;
1416        drop(
1417            self.interrupt_harnesses(&root_session_id, &ids, harnesses, deadline)
1418                .await,
1419        );
1420    }
1421
1422    async fn interrupt_harnesses(
1423        &self,
1424        root_session_id: &str,
1425        ids: &[AgentId],
1426        harnesses: Vec<HarnessHandle>,
1427        deadline: Instant,
1428    ) -> std::io::Result<()> {
1429        let interruption = async move {
1430            let results = join_all(
1431                harnesses
1432                    .into_iter()
1433                    .map(|harness| async move { harness.interrupt().await }),
1434            )
1435            .await;
1436            first_error(results)
1437        };
1438        let interruption_result = timeout_at(deadline, interruption).await.map_err(|_| {
1439            std::io::Error::new(
1440                std::io::ErrorKind::TimedOut,
1441                "timed out interrupting subagent harnesses",
1442            )
1443        })?;
1444        self.wait_until_inactive(root_session_id, ids, deadline)
1445            .await?;
1446        drop(interruption_result);
1447        Ok(())
1448    }
1449
1450    async fn close_harnesses(
1451        &self,
1452        harnesses: Vec<HarnessHandle>,
1453        deadline: Instant,
1454    ) -> std::io::Result<()> {
1455        let closing = async move {
1456            let results = join_all(
1457                harnesses
1458                    .into_iter()
1459                    .map(|harness| async move { harness.close().await }),
1460            )
1461            .await;
1462            first_error(results)
1463        };
1464        timeout_at(deadline, closing).await.map_err(|_| {
1465            std::io::Error::new(
1466                std::io::ErrorKind::TimedOut,
1467                "timed out closing subagent harnesses",
1468            )
1469        })?
1470    }
1471
1472    async fn wait_for_tasks(
1473        &self,
1474        mut tasks: Vec<JoinHandle<()>>,
1475        deadline: Instant,
1476        description: &str,
1477    ) -> std::io::Result<()> {
1478        if tasks.is_empty() {
1479            return Ok(());
1480        }
1481        let completion = join_all(tasks.iter_mut());
1482        match timeout_at(deadline, completion).await {
1483            Ok(results) => results
1484                .into_iter()
1485                .find_map(Result::err)
1486                .map_or(Ok(()), |error| {
1487                    Err(std::io::Error::other(format!(
1488                        "{description} failed during shutdown: {error}"
1489                    )))
1490                }),
1491            Err(_) => {
1492                for task in tasks {
1493                    task.abort();
1494                }
1495                Err(std::io::Error::new(
1496                    std::io::ErrorKind::TimedOut,
1497                    format!("timed out waiting for {description} to close"),
1498                ))
1499            }
1500        }
1501    }
1502
1503    async fn wait_until_inactive(
1504        &self,
1505        root_session_id: &str,
1506        ids: &[AgentId],
1507        deadline: Instant,
1508    ) -> std::io::Result<()> {
1509        let mut revision = self.revision.subscribe();
1510        loop {
1511            if self.state.lock().await.all_inactive(root_session_id, ids)? {
1512                return Ok(());
1513            }
1514            timeout_at(deadline, revision.changed())
1515                .await
1516                .map_err(|_| {
1517                    std::io::Error::new(
1518                        std::io::ErrorKind::TimedOut,
1519                        "timed out waiting for subagent turns to stop",
1520                    )
1521                })?
1522                .map_err(|_| std::io::Error::other("subagent runtime is closed"))?;
1523        }
1524    }
1525
1526    fn changed(&self) {
1527        self.revision.send_modify(|revision| {
1528            *revision = revision.wrapping_add(1);
1529        });
1530    }
1531}
1532
1533impl RootAgentAuthority {
1534    /// Rejects callers that belong to a child session.
1535    ///
1536    /// # Errors
1537    ///
1538    /// Returns an error when the runtime has closed or `session_id` belongs to a registered child.
1539    pub async fn require_root(&self, session_id: &str) -> Result<(), AuthorityError> {
1540        let registry = self
1541            .registry
1542            .upgrade()
1543            .ok_or(AuthorityError::RuntimeClosed)?;
1544        if registry.is_root_session(session_id).await {
1545            return Ok(());
1546        }
1547        Err(AuthorityError::ChildSession)
1548    }
1549}
1550
1551fn complete_session(session: &mut ChildSession, output: Option<Value>) -> AgentStatus {
1552    let Some(output) = output else {
1553        return AgentStatus::Failed {
1554            error: "subagent turn ended without a valid submit_result call".to_owned(),
1555        };
1556    };
1557    session.last_output = Some(output.clone());
1558    AgentStatus::Completed { output }
1559}
1560
1561fn first_error(results: Vec<std::io::Result<()>>) -> std::io::Result<()> {
1562    results.into_iter().find(Result::is_err).unwrap_or(Ok(()))
1563}
1564
1565fn bounded_summary(value: &str) -> String {
1566    const MAX_BYTES: usize = 160;
1567    if value.len() <= MAX_BYTES {
1568        return value.to_owned();
1569    }
1570    let end = value
1571        .char_indices()
1572        .map(|(index, _)| index)
1573        .take_while(|index| *index <= MAX_BYTES)
1574        .last()
1575        .unwrap_or_default();
1576    value[..end].to_owned()
1577}
1578
1579impl ChildSession {
1580    pub(super) fn summary(&self) -> AgentSummary {
1581        let last_output = if matches!(self.status, AgentStatus::Completed { .. }) {
1582            None
1583        } else {
1584            self.last_output.clone()
1585        };
1586        AgentSummary {
1587            agent_id: self.descriptor.id,
1588            model: self.descriptor.model,
1589            role: self.descriptor.role.clone(),
1590            task: self.descriptor.task.clone(),
1591            parent_agent_id: self.descriptor.parent,
1592            status: self.status.clone(),
1593            last_output,
1594        }
1595    }
1596}
1597
1598/// Owns the child sessions, shared capacity, and tool state for one agent runtime.
1599#[derive(Clone)]
1600pub struct Subagents {
1601    pub(crate) registry: Arc<Registry>,
1602}
1603
1604/// A non-owning handle used by factories installed into child sessions.
1605///
1606/// Keeping tool factories weak prevents the runtime from retaining itself through the child-agent
1607/// factory it owns.
1608#[derive(Clone)]
1609pub struct WeakSubagents {
1610    pub(crate) registry: Weak<Registry>,
1611}
1612
1613impl Subagents {
1614    /// Creates an isolated runtime and its typed update stream.
1615    ///
1616    /// The receiver carries model events as well as lifecycle changes. Keep it alive and drain it
1617    /// continuously for the lifetime of the runtime. Dropping it ends event forwarding and makes
1618    /// later lifecycle changes unobservable. A zero concurrency limit creates the runtime but
1619    /// rejects child turns until the limit is raised.
1620    pub fn new(max_concurrency: usize) -> (Self, mpsc::UnboundedReceiver<ScopedAgentUpdate>) {
1621        let (updates, receiver) = mpsc::unbounded_channel();
1622        let registry = Arc::new(Registry::new(updates, max_concurrency));
1623        (Self { registry }, receiver)
1624    }
1625
1626    /// Returns a non-owning handle for tool factories and other runtime-owned callbacks.
1627    pub fn downgrade(&self) -> WeakSubagents {
1628        WeakSubagents {
1629            registry: Arc::downgrade(&self.registry),
1630        }
1631    }
1632
1633    /// Configures how clean child sessions are constructed.
1634    ///
1635    /// The factory must return a new session and its event stream on every call. The runtime
1636    /// supplies the requested model and the current inherited settings. A runtime accepts exactly
1637    /// one factory; a second call returns [`NanocodexError::InvalidRequest`].
1638    ///
1639    /// # Errors
1640    ///
1641    /// Returns [`NanocodexError::InvalidRequest`] if a factory is already configured.
1642    pub fn set_agent_factory<F>(
1643        &self,
1644        thinking: Thinking,
1645        fast_mode: bool,
1646        factory: F,
1647    ) -> Result<(), NanocodexError>
1648    where
1649        F: Fn(Model, Thinking, bool) -> Result<(Nanocodex, AgentEvents), NanocodexError>
1650            + Send
1651            + Sync
1652            + 'static,
1653    {
1654        self.registry
1655            .set_agent_factory(thinking, fast_mode, factory)
1656    }
1657
1658    /// Returns a root-session authority checker for application-owned tools.
1659    pub fn root_agent_authority(&self) -> RootAgentAuthority {
1660        self.downgrade().root_agent_authority()
1661    }
1662
1663    /// Changes the maximum number of concurrently active child turns.
1664    ///
1665    /// Lowering the limit does not cancel active turns. New reservations fail until the active
1666    /// count falls below the new limit.
1667    pub fn set_max_concurrency(&self, limit: usize) {
1668        self.registry.set_max_concurrency(limit);
1669    }
1670
1671    /// Changes the reasoning effort inherited by newly created child sessions.
1672    pub fn set_thinking(&self, thinking: Thinking) {
1673        self.registry.set_agent_thinking(thinking);
1674    }
1675
1676    /// Changes the fast-mode setting inherited by newly created child sessions.
1677    pub fn set_fast_mode(&self, enabled: bool) {
1678        self.registry.set_agent_fast_mode(enabled);
1679    }
1680
1681    /// Attempts to interrupt every active child without closing reusable sessions.
1682    ///
1683    /// Shutdown is best effort and bounded. Individual child failures are reflected in updates.
1684    pub async fn cancel_all(&self, root_session_id: &str) {
1685        self.registry.cancel_all(root_session_id).await;
1686    }
1687
1688    /// Attempts to close and join every child owned by a root session.
1689    ///
1690    /// Shutdown is best effort and bounded. Individual child failures are reflected in updates.
1691    pub async fn close_all(&self, root_session_id: &str) {
1692        drop(self.registry.close_all(root_session_id).await);
1693    }
1694
1695    /// Returns the identity used to reject updates from a replaced runtime.
1696    pub fn runtime_id(&self) -> SubagentRuntimeId {
1697        self.registry.id
1698    }
1699}
1700
1701impl WeakSubagents {
1702    /// Returns a root-session authority checker that does not keep the runtime alive.
1703    pub fn root_agent_authority(&self) -> RootAgentAuthority {
1704        RootAgentAuthority {
1705            registry: self.registry.clone(),
1706        }
1707    }
1708}
1709
1710#[cfg(test)]
1711fn channel(
1712    max_concurrency: usize,
1713) -> (
1714    Arc<Registry>,
1715    Subagents,
1716    mpsc::UnboundedReceiver<ScopedAgentUpdate>,
1717) {
1718    let (subagents, updates) = Subagents::new(max_concurrency);
1719    (Arc::clone(&subagents.registry), subagents, updates)
1720}
1721
1722pub(super) fn forward_events(
1723    root_session_id: String,
1724    id: AgentId,
1725    mut events: AgentEvents,
1726    start: oneshot::Receiver<()>,
1727    registry: Weak<Registry>,
1728    updates: mpsc::UnboundedSender<ScopedAgentUpdate>,
1729) -> JoinHandle<()> {
1730    tokio::spawn(async move {
1731        if start.await.is_err() {
1732            return;
1733        }
1734        while let Some(event) = events.recv().await {
1735            if !send_update(&updates, &root_session_id, AgentUpdate::Event { id, event }) {
1736                return;
1737            }
1738        }
1739        if let Some(registry) = registry.upgrade() {
1740            registry.runtime_closed(&root_session_id, id).await;
1741        }
1742    })
1743}
1744
1745fn send_update(
1746    updates: &mpsc::UnboundedSender<ScopedAgentUpdate>,
1747    root_session_id: &str,
1748    update: AgentUpdate,
1749) -> bool {
1750    updates
1751        .send(ScopedAgentUpdate {
1752            root_session_id: root_session_id.to_owned(),
1753            update,
1754        })
1755        .is_ok()
1756}
1757
1758#[cfg(test)]
1759mod tests {
1760    use super::{
1761        AgentDescriptor, AgentId, AgentStatus, AuthorityError, ChildSession, OutputContract,
1762        Registry, RegistryState, RootAgentAuthority, Subagents, complete_session,
1763        completion_instructions, forward_events,
1764    };
1765    use crate::{
1766        AgentUpdate, MessageDeliveryState, MessageDisposition, MessagePriority, MessagePurpose,
1767    };
1768    use nanocodex::{
1769        Model, Nanocodex, NanocodexError, OpenAi, Thinking,
1770        oai::{
1771            ResponseError,
1772            tower::{ResponsesAttempt, ResponsesServiceResponse},
1773        },
1774    };
1775    use serde_json::json;
1776    use std::{
1777        future::{Pending, pending},
1778        result::Result as StdResult,
1779        sync::{Arc, Mutex},
1780        task::{Context, Poll},
1781        time::Duration,
1782    };
1783    use tokio::{
1784        sync::{Notify, mpsc, oneshot},
1785        time::timeout,
1786    };
1787    use tower::Service;
1788
1789    #[test]
1790    fn agent_factory_receives_the_declared_model() {
1791        let (updates, _receiver) = mpsc::unbounded_channel();
1792        let registry = Registry::new(updates, 1);
1793        let seen = Arc::new(Mutex::new(None));
1794        let captured = Arc::clone(&seen);
1795        registry
1796            .set_agent_factory(
1797                Thinking::Medium,
1798                false,
1799                move |model, thinking, fast_mode| {
1800                    *captured.lock().unwrap() = Some((model, thinking, fast_mode));
1801                    Err(NanocodexError::InvalidRequest(
1802                        "stop after capture".to_owned(),
1803                    ))
1804                },
1805            )
1806            .unwrap();
1807        registry.set_agent_thinking(Thinking::Low);
1808        registry.set_agent_fast_mode(true);
1809
1810        assert!(registry.spawn_agent(Model::Luna).is_err());
1811        assert_eq!(
1812            *seen.lock().unwrap(),
1813            Some((Model::Luna, Thinking::Low, true))
1814        );
1815    }
1816
1817    #[derive(Clone)]
1818    struct PendingService {
1819        called: Arc<Notify>,
1820    }
1821
1822    impl Service<ResponsesAttempt> for PendingService {
1823        type Response = ResponsesServiceResponse;
1824        type Error = ResponseError;
1825        type Future = Pending<StdResult<Self::Response, Self::Error>>;
1826
1827        fn poll_ready(&mut self, _context: &mut Context<'_>) -> Poll<StdResult<(), Self::Error>> {
1828            Poll::Ready(Ok(()))
1829        }
1830
1831        fn call(&mut self, _request: ResponsesAttempt) -> Self::Future {
1832            self.called.notify_one();
1833            pending()
1834        }
1835    }
1836
1837    fn pending_agent(called: Arc<Notify>) -> (Nanocodex, nanocodex::AgentEvents) {
1838        let openai = OpenAi::builder("test-key")
1839            .service(move || PendingService {
1840                called: Arc::clone(&called),
1841            })
1842            .build()
1843            .unwrap();
1844        Nanocodex::builder(openai).build().unwrap()
1845    }
1846
1847    fn test_contract() -> OutputContract {
1848        OutputContract {
1849            validator: jsonschema::validator_for(&json!({})).unwrap(),
1850            schema: "{}".to_owned(),
1851        }
1852    }
1853
1854    #[tokio::test]
1855    async fn root_agent_authority_rejects_registered_child_sessions() {
1856        let (updates, _updates_receiver) = mpsc::unbounded_channel();
1857        let registry = Arc::new(Registry::new(updates, 1));
1858        registry
1859            .state
1860            .lock()
1861            .await
1862            .root_by_session
1863            .insert("child".to_owned(), "root".to_owned());
1864        let guard = RootAgentAuthority {
1865            registry: Arc::downgrade(&registry),
1866        };
1867
1868        assert!(guard.require_root("root").await.is_ok());
1869        assert!(guard.require_root("fork").await.is_ok());
1870        assert_eq!(
1871            guard.require_root("child").await,
1872            Err(AuthorityError::ChildSession)
1873        );
1874        drop(registry);
1875        assert_eq!(
1876            guard.require_root("root").await,
1877            Err(AuthorityError::RuntimeClosed)
1878        );
1879    }
1880
1881    #[tokio::test]
1882    async fn runtime_owned_agent_factory_does_not_keep_the_runtime_alive() {
1883        let (subagents, mut updates) = Subagents::new(1);
1884        let weak = subagents.downgrade();
1885        let factory_weak = weak.clone();
1886        subagents
1887            .set_agent_factory(Thinking::Medium, false, move |_, _, _| {
1888                let _ = &factory_weak;
1889                Err(NanocodexError::InvalidRequest("unused factory".to_owned()))
1890            })
1891            .unwrap();
1892
1893        drop(subagents);
1894
1895        assert!(weak.registry.upgrade().is_none());
1896        assert!(updates.recv().await.is_none());
1897        assert_eq!(
1898            weak.root_agent_authority().require_root("root").await,
1899            Err(AuthorityError::RuntimeClosed)
1900        );
1901    }
1902
1903    #[test]
1904    fn output_contract_renders_the_schema_for_every_turn() {
1905        let schema = json!({
1906            "type": "object",
1907            "properties": { "report": { "type": "string" } },
1908            "required": ["report"]
1909        });
1910
1911        let contract = OutputContract::compile(&schema).unwrap();
1912        let instructions = completion_instructions(&contract.schema, 7);
1913
1914        assert!(instructions.contains("tools.submit_result"));
1915        assert!(instructions.contains("turn_token: 7"));
1916        assert!(instructions.contains("exactly once"));
1917        assert!(instructions.contains("\"report\""));
1918        assert!(contract.validator.is_valid(&json!({ "report": "done" })));
1919    }
1920
1921    async fn insert_runtime_session(
1922        registry: &Arc<Registry>,
1923        reservation: &super::AgentReservation,
1924        parent: Option<AgentId>,
1925        agent: Nanocodex,
1926        events: nanocodex::AgentEvents,
1927    ) -> String {
1928        let session_id = events.request_id().to_owned();
1929        let descriptor = AgentDescriptor {
1930            id: reservation.id,
1931            session_id: session_id.clone(),
1932            model: Model::Sol,
1933            role: format!("agent-{}", reservation.id),
1934            task: "wait forever".to_owned(),
1935            parent,
1936        };
1937        let (start_events, events_ready) = oneshot::channel();
1938        let event_task = forward_events(
1939            reservation.root_session_id.clone(),
1940            reservation.id,
1941            events,
1942            events_ready,
1943            Arc::downgrade(registry),
1944            registry.updates.clone(),
1945        );
1946        registry
1947            .insert(
1948                reservation.root_session_id.clone(),
1949                descriptor,
1950                agent,
1951                event_task,
1952                test_contract(),
1953            )
1954            .await
1955            .unwrap();
1956        start_events.send(()).unwrap();
1957        session_id
1958    }
1959
1960    async fn insert_pending_runtime_session(
1961        registry: &Arc<Registry>,
1962        root_session_id: &str,
1963        parent: Option<AgentId>,
1964        called: Arc<Notify>,
1965    ) -> (AgentId, String) {
1966        let reservation = registry.reserve(root_session_id).await.unwrap();
1967        let id = reservation.id;
1968        let (agent, events) = pending_agent(called);
1969        let session_id =
1970            insert_runtime_session(registry, &reservation, parent, agent, events).await;
1971        (id, session_id)
1972    }
1973
1974    async fn next_message_update(
1975        updates: &mut tokio::sync::mpsc::UnboundedReceiver<super::ScopedAgentUpdate>,
1976    ) -> crate::AgentMessageUpdate {
1977        timeout(Duration::from_secs(5), async {
1978            loop {
1979                let update = updates
1980                    .recv()
1981                    .await
1982                    .expect("the update channel should remain open");
1983                if let AgentUpdate::Message(message) = update.update {
1984                    return message;
1985                }
1986            }
1987        })
1988        .await
1989        .expect("a message update should arrive")
1990    }
1991
1992    async fn mark_reusable(registry: &Arc<Registry>, root_session_id: &str, id: AgentId) {
1993        registry
1994            .state
1995            .lock()
1996            .await
1997            .scopes
1998            .get_mut(root_session_id)
1999            .unwrap()
2000            .sessions
2001            .get_mut(&id)
2002            .unwrap()
2003            .status = AgentStatus::Completed {
2004            output: json!({ "report": "ready for another turn" }),
2005        };
2006    }
2007
2008    fn test_session(id: AgentId, session_id: &str, parent: Option<AgentId>) -> ChildSession {
2009        let descriptor = AgentDescriptor {
2010            id,
2011            session_id: session_id.to_owned(),
2012            model: Model::Sol,
2013            role: format!("agent-{id}"),
2014            task: "test lifecycle".to_owned(),
2015            parent,
2016        };
2017        ChildSession {
2018            descriptor,
2019            event_task: Some(tokio::spawn(async {})),
2020            harness: None,
2021            harness_task: None,
2022            status: AgentStatus::Pending,
2023            active: false,
2024            output_validator: test_contract().validator,
2025            next_turn_token: 0,
2026            active_turn_token: None,
2027            steering: false,
2028            submitted_output: None,
2029            last_output: None,
2030        }
2031    }
2032
2033    #[tokio::test]
2034    async fn submitted_outputs_are_validated_and_completed_as_json() {
2035        let mut registry = RegistryState::default();
2036        let reservation = registry.reserve("main", None).unwrap();
2037        let mut session = test_session(reservation.id, "child-session", None);
2038        session.active = true;
2039        session.next_turn_token = 1;
2040        session.active_turn_token = Some(1);
2041        session.status = AgentStatus::Running;
2042        session.output_validator = jsonschema::validator_for(&json!({
2043            "type": "object",
2044            "properties": { "answer": { "type": "integer" } },
2045            "required": ["answer"],
2046            "additionalProperties": false
2047        }))
2048        .unwrap();
2049        registry
2050            .insert(
2051                reservation.root_session_id,
2052                reservation.id,
2053                session.descriptor.session_id.clone(),
2054                session,
2055            )
2056            .unwrap();
2057
2058        let invalid = registry.submit_result("child-session", 1, json!({ "answer": "42" }));
2059        assert!(invalid.unwrap_err().to_string().contains("required schema"));
2060        registry
2061            .submit_result("child-session", 1, json!({ "answer": 42 }))
2062            .unwrap();
2063        assert!(
2064            registry
2065                .submit_result("child-session", 1, json!({ "answer": 43 }))
2066                .unwrap_err()
2067                .to_string()
2068                .contains("already accepted")
2069        );
2070
2071        let session = registry
2072            .scopes
2073            .get_mut("main")
2074            .unwrap()
2075            .sessions
2076            .get_mut(&reservation.id)
2077            .unwrap();
2078        let output = session.submitted_output.take();
2079        let status = complete_session(session, output);
2080
2081        assert_eq!(
2082            status,
2083            AgentStatus::Completed {
2084                output: json!({ "answer": 42 })
2085            }
2086        );
2087        assert_eq!(session.last_output, Some(json!({ "answer": 42 })));
2088    }
2089
2090    #[test]
2091    fn root_cannot_submit_a_subagent_result() {
2092        let mut registry = RegistryState::default();
2093
2094        let error = registry.submit_result("main", 1, json!({ "report": "no" }));
2095
2096        assert!(
2097            error
2098                .unwrap_err()
2099                .to_string()
2100                .contains("only available to subagents")
2101        );
2102    }
2103
2104    #[tokio::test]
2105    async fn successful_turn_without_submission_fails_completion() {
2106        let mut session = test_session(AgentId::new(1), "child-session", None);
2107
2108        let status = complete_session(&mut session, None);
2109
2110        assert!(matches!(status, AgentStatus::Failed { error } if error.contains("submit_result")));
2111        assert_eq!(session.last_output, None);
2112    }
2113
2114    #[tokio::test]
2115    async fn submission_from_completed_turn_cannot_satisfy_next_turn() {
2116        let mut registry = RegistryState::default();
2117        let reservation = registry.reserve("main", None).unwrap();
2118        let mut session = test_session(reservation.id, "child-session", None);
2119        session.active = true;
2120        session.next_turn_token = 1;
2121        session.active_turn_token = Some(1);
2122        session.status = AgentStatus::Running;
2123        registry
2124            .insert(
2125                reservation.root_session_id,
2126                reservation.id,
2127                session.descriptor.session_id.clone(),
2128                session,
2129            )
2130            .unwrap();
2131        let stale_output = json!({ "report": "result from the completed turn" });
2132
2133        let session = registry
2134            .scopes
2135            .get_mut("main")
2136            .unwrap()
2137            .sessions
2138            .get_mut(&reservation.id)
2139            .unwrap();
2140        session.active = false;
2141        session.active = true;
2142        session.next_turn_token = 2;
2143        session.active_turn_token = Some(2);
2144        session.status = AgentStatus::Running;
2145
2146        assert!(
2147            registry
2148                .submit_result("child-session", 1, stale_output)
2149                .is_err()
2150        );
2151    }
2152
2153    #[tokio::test]
2154    async fn steering_rotates_the_token_and_stops_after_submission() {
2155        let mut registry = RegistryState::default();
2156        let reservation = registry.reserve("main", None).unwrap();
2157        let mut session = test_session(reservation.id, "child-session", None);
2158        session.active = true;
2159        session.next_turn_token = 1;
2160        session.active_turn_token = Some(1);
2161        session.status = AgentStatus::Running;
2162        registry
2163            .insert(
2164                reservation.root_session_id,
2165                reservation.id,
2166                session.descriptor.session_id.clone(),
2167                session,
2168            )
2169            .unwrap();
2170
2171        let steer = registry.begin_turn_steer("main", reservation.id).unwrap();
2172        assert_eq!(steer.token(), 2);
2173        registry.finish_turn_steer("main", steer, true);
2174        assert!(
2175            registry
2176                .submit_result("child-session", 1, json!({ "report": "stale" }))
2177                .is_err()
2178        );
2179        registry
2180            .submit_result("child-session", 2, json!({ "report": "current" }))
2181            .unwrap();
2182
2183        assert!(registry.begin_turn_steer("main", reservation.id).is_none());
2184    }
2185
2186    #[tokio::test]
2187    async fn closed_agent_summaries_keep_the_last_completed_output() {
2188        let (registry, _control, _updates) = super::channel(32);
2189        let reservation = registry.reserve("main").await.unwrap();
2190        let mut session = test_session(reservation.id, "child-session", None);
2191        session.status = AgentStatus::Completed {
2192            output: json!({ "report": "completed work" }),
2193        };
2194        session.last_output = Some(json!({ "report": "completed work" }));
2195        registry
2196            .state
2197            .lock()
2198            .await
2199            .insert(
2200                reservation.root_session_id.clone(),
2201                reservation.id,
2202                session.descriptor.session_id.clone(),
2203                session,
2204            )
2205            .unwrap();
2206
2207        let summaries = registry.close("main", reservation.id).await.unwrap();
2208
2209        assert_eq!(summaries[0].status, AgentStatus::Closed);
2210        assert_eq!(
2211            summaries[0].last_output,
2212            Some(json!({ "report": "completed work" }))
2213        );
2214    }
2215
2216    #[tokio::test]
2217    async fn interrupt_and_close_stop_recursive_turns_and_preserve_continuation() {
2218        let (registry, _control, _updates) = super::channel(32);
2219        let parent_called = Arc::new(Notify::new());
2220        let child_called = Arc::new(Notify::new());
2221        let sibling_called = Arc::new(Notify::new());
2222
2223        let parent = registry.reserve("main").await.unwrap();
2224        let (parent_agent, parent_events) = pending_agent(Arc::clone(&parent_called));
2225        let parent_session =
2226            insert_runtime_session(&registry, &parent, None, parent_agent, parent_events).await;
2227        registry
2228            .launch_initial_turn(
2229                &parent.root_session_id,
2230                parent.id,
2231                "parent work".to_owned(),
2232                registry.reserve_turn().unwrap(),
2233            )
2234            .await
2235            .unwrap();
2236
2237        let child = registry.reserve(&parent_session).await.unwrap();
2238        let (child_agent, child_events) = pending_agent(Arc::clone(&child_called));
2239        insert_runtime_session(
2240            &registry,
2241            &child,
2242            Some(parent.id),
2243            child_agent,
2244            child_events,
2245        )
2246        .await;
2247        registry
2248            .launch_initial_turn(
2249                &child.root_session_id,
2250                child.id,
2251                "child work".to_owned(),
2252                registry.reserve_turn().unwrap(),
2253            )
2254            .await
2255            .unwrap();
2256
2257        let sibling = registry.reserve("main").await.unwrap();
2258        let (sibling_agent, sibling_events) = pending_agent(Arc::clone(&sibling_called));
2259        insert_runtime_session(&registry, &sibling, None, sibling_agent, sibling_events).await;
2260        registry
2261            .launch_initial_turn(
2262                &sibling.root_session_id,
2263                sibling.id,
2264                "sibling work".to_owned(),
2265                registry.reserve_turn().unwrap(),
2266            )
2267            .await
2268            .unwrap();
2269
2270        timeout(Duration::from_secs(5), parent_called.notified())
2271            .await
2272            .unwrap();
2273        timeout(Duration::from_secs(5), child_called.notified())
2274            .await
2275            .unwrap();
2276        timeout(Duration::from_secs(5), sibling_called.notified())
2277            .await
2278            .unwrap();
2279
2280        let (running, timed_out) = registry
2281            .wait("main", &[parent.id, child.id], Duration::from_millis(1))
2282            .await
2283            .unwrap();
2284        assert!(timed_out);
2285        assert!(
2286            running
2287                .iter()
2288                .all(|summary| summary.status == AgentStatus::Running)
2289        );
2290
2291        let interrupted = registry.interrupt("main", parent.id).await.unwrap();
2292        assert_eq!(
2293            interrupted
2294                .iter()
2295                .map(|summary| (&summary.agent_id, &summary.status))
2296                .collect::<Vec<_>>(),
2297            [
2298                (&child.id, &AgentStatus::Interrupted),
2299                (&parent.id, &AgentStatus::Interrupted),
2300            ]
2301        );
2302        let (finished, timed_out) = registry
2303            .wait("main", &[parent.id, child.id], Duration::from_secs(1))
2304            .await
2305            .unwrap();
2306        assert!(!timed_out);
2307        assert_eq!(finished.len(), 2);
2308        assert_eq!(
2309            registry
2310                .state
2311                .lock()
2312                .await
2313                .summaries("main", &[sibling.id])
2314                .unwrap()[0]
2315                .status,
2316            AgentStatus::Running
2317        );
2318
2319        let receipt = registry
2320            .send_message(
2321                "main",
2322                parent.id,
2323                MessagePriority::Deferred,
2324                MessagePurpose::Delegate,
2325                None,
2326                "continue".to_owned(),
2327            )
2328            .await
2329            .unwrap();
2330        assert_eq!(receipt.disposition, MessageDisposition::Started);
2331        timeout(Duration::from_secs(5), parent_called.notified())
2332            .await
2333            .unwrap();
2334
2335        let closed = registry.close("main", parent.id).await.unwrap();
2336        assert_eq!(
2337            closed
2338                .iter()
2339                .map(|summary| (&summary.agent_id, &summary.status))
2340                .collect::<Vec<_>>(),
2341            [
2342                (&child.id, &AgentStatus::Closed),
2343                (&parent.id, &AgentStatus::Closed),
2344            ]
2345        );
2346        assert_eq!(registry.directory("main", true, false).await.len(), 3);
2347
2348        let all_closed = registry.close_all("main").await.unwrap();
2349        assert_eq!(all_closed.len(), 3);
2350        assert!(
2351            all_closed
2352                .iter()
2353                .all(|summary| summary.status == AgentStatus::Closed)
2354        );
2355        let state = registry.state.lock().await;
2356        assert!(
2357            state.scopes["main"]
2358                .sessions
2359                .values()
2360                .all(|session| session.harness.is_none()
2361                    && session.harness_task.is_none()
2362                    && session.event_task.is_none())
2363        );
2364    }
2365
2366    #[tokio::test]
2367    async fn same_root_agents_can_message_across_sibling_branches() {
2368        let (registry, _control, mut updates) = super::channel(32);
2369        let sender_called = Arc::new(Notify::new());
2370        let target_called = Arc::new(Notify::new());
2371        let (_sender, sender_session) =
2372            insert_pending_runtime_session(&registry, "main", None, Arc::clone(&sender_called))
2373                .await;
2374        let (target, _target_session) =
2375            insert_pending_runtime_session(&registry, "main", None, Arc::clone(&target_called))
2376                .await;
2377        mark_reusable(&registry, "main", target).await;
2378
2379        let receipt = registry
2380            .send_message(
2381                &sender_session,
2382                target,
2383                MessagePriority::Deferred,
2384                MessagePurpose::Coordinate,
2385                None,
2386                "Compare our findings before either of us edits.".to_owned(),
2387            )
2388            .await
2389            .unwrap();
2390
2391        assert_eq!(receipt.disposition, MessageDisposition::Started);
2392        timeout(Duration::from_secs(5), target_called.notified())
2393            .await
2394            .unwrap();
2395        let update = next_message_update(&mut updates).await;
2396        assert_eq!(update.message_id, receipt.message_id);
2397        assert_eq!(update.thread.messages.len(), 1);
2398        assert_eq!(
2399            update.delivery,
2400            MessageDeliveryState::Admitted {
2401                disposition: MessageDisposition::Started,
2402            }
2403        );
2404
2405        registry.close_all("main").await.unwrap();
2406    }
2407
2408    #[tokio::test]
2409    async fn pending_agents_cannot_receive_messages_before_their_initial_turn() {
2410        let (registry, _control, _updates) = super::channel(32);
2411        let (_sender, sender_session) =
2412            insert_pending_runtime_session(&registry, "main", None, Arc::new(Notify::new())).await;
2413        let (target, _target_session) =
2414            insert_pending_runtime_session(&registry, "main", None, Arc::new(Notify::new())).await;
2415
2416        let error = registry
2417            .send_message(
2418                &sender_session,
2419                target,
2420                MessagePriority::Deferred,
2421                MessagePurpose::Coordinate,
2422                None,
2423                "Do not overtake the assigned initial task.".to_owned(),
2424            )
2425            .await
2426            .unwrap_err();
2427
2428        assert!(error.to_string().contains("has not started"));
2429        registry.close_all("main").await.unwrap();
2430    }
2431
2432    #[tokio::test]
2433    async fn sibling_messages_cannot_take_management_authority() {
2434        let (registry, _control, _updates) = super::channel(32);
2435        let (_sender, sender_session) =
2436            insert_pending_runtime_session(&registry, "main", None, Arc::new(Notify::new())).await;
2437        let (target, _target_session) =
2438            insert_pending_runtime_session(&registry, "main", None, Arc::new(Notify::new())).await;
2439
2440        let error = registry
2441            .send_message(
2442                &sender_session,
2443                target,
2444                MessagePriority::Deferred,
2445                MessagePurpose::Delegate,
2446                None,
2447                "Replace the sibling's assigned task.".to_owned(),
2448            )
2449            .await
2450            .unwrap_err();
2451
2452        assert!(error.to_string().contains("only manage its descendants"));
2453        registry.close_all("main").await.unwrap();
2454    }
2455
2456    #[tokio::test]
2457    async fn delegate_messages_replace_assigned_tasks_for_descendants() {
2458        let (registry, _control, _updates) = super::channel(32);
2459        let (parent, parent_session) =
2460            insert_pending_runtime_session(&registry, "main", None, Arc::new(Notify::new())).await;
2461        let child_called = Arc::new(Notify::new());
2462        let (child, _child_session) = insert_pending_runtime_session(
2463            &registry,
2464            "main",
2465            Some(parent),
2466            Arc::clone(&child_called),
2467        )
2468        .await;
2469        mark_reusable(&registry, "main", child).await;
2470
2471        let receipt = registry
2472            .send_message(
2473                &parent_session,
2474                child,
2475                MessagePriority::Deferred,
2476                MessagePurpose::Delegate,
2477                None,
2478                "Own the parser tests and report every uncovered branch.".to_owned(),
2479            )
2480            .await
2481            .unwrap();
2482
2483        assert_eq!(receipt.disposition, MessageDisposition::Started);
2484        timeout(Duration::from_secs(5), child_called.notified())
2485            .await
2486            .unwrap();
2487        let task = registry.state.lock().await.scopes["main"].sessions[&child]
2488            .descriptor
2489            .task
2490            .clone();
2491        assert_eq!(
2492            task,
2493            "Own the parser tests and report every uncovered branch."
2494        );
2495        registry.close_all("main").await.unwrap();
2496    }
2497
2498    #[tokio::test]
2499    async fn urgent_messages_steer_running_agents() {
2500        let (registry, _control, _updates) = super::channel(32);
2501        let (_sender, sender_session) =
2502            insert_pending_runtime_session(&registry, "main", None, Arc::new(Notify::new())).await;
2503        let target_called = Arc::new(Notify::new());
2504        let (target, _target_session) =
2505            insert_pending_runtime_session(&registry, "main", None, Arc::clone(&target_called))
2506                .await;
2507        registry
2508            .launch_initial_turn(
2509                "main",
2510                target,
2511                "Keep working until interrupted.".to_owned(),
2512                registry.reserve_turn().unwrap(),
2513            )
2514            .await
2515            .unwrap();
2516        timeout(Duration::from_secs(5), target_called.notified())
2517            .await
2518            .unwrap();
2519
2520        let receipt = registry
2521            .send_message(
2522                &sender_session,
2523                target,
2524                MessagePriority::Urgent,
2525                MessagePurpose::Finding,
2526                None,
2527                "Stop duplicating the parser investigation.".to_owned(),
2528            )
2529            .await
2530            .unwrap();
2531
2532        assert_eq!(receipt.disposition, MessageDisposition::Steered);
2533        registry.close_all("main").await.unwrap();
2534    }
2535
2536    #[tokio::test]
2537    async fn interruption_marks_queued_messages_as_failed() {
2538        let (registry, _control, mut updates) = super::channel(32);
2539        let (_sender, sender_session) =
2540            insert_pending_runtime_session(&registry, "main", None, Arc::new(Notify::new())).await;
2541        let target_called = Arc::new(Notify::new());
2542        let (target, _target_session) =
2543            insert_pending_runtime_session(&registry, "main", None, Arc::clone(&target_called))
2544                .await;
2545        registry
2546            .launch_initial_turn(
2547                "main",
2548                target,
2549                "Keep working until interrupted.".to_owned(),
2550                registry.reserve_turn().unwrap(),
2551            )
2552            .await
2553            .unwrap();
2554        timeout(Duration::from_secs(5), target_called.notified())
2555            .await
2556            .unwrap();
2557
2558        let receipt = registry
2559            .send_message(
2560                &sender_session,
2561                target,
2562                MessagePriority::Deferred,
2563                MessagePurpose::Question,
2564                None,
2565                "What remains in your investigation?".to_owned(),
2566            )
2567            .await
2568            .unwrap();
2569        assert_eq!(receipt.disposition, MessageDisposition::Queued);
2570        let admitted = next_message_update(&mut updates).await;
2571        assert_eq!(admitted.message_id, receipt.message_id);
2572
2573        registry.interrupt("main", target).await.unwrap();
2574
2575        let failed = next_message_update(&mut updates).await;
2576        assert_eq!(failed.message_id, receipt.message_id);
2577        assert!(matches!(
2578            failed.delivery,
2579            MessageDeliveryState::Failed { .. }
2580        ));
2581        registry.close_all("main").await.unwrap();
2582    }
2583
2584    #[tokio::test]
2585    async fn queued_delegation_changes_the_task_only_when_delivery_starts() {
2586        let (registry, _control, _updates) = super::channel(32);
2587        let target_called = Arc::new(Notify::new());
2588        let (target, _target_session) =
2589            insert_pending_runtime_session(&registry, "main", None, Arc::clone(&target_called))
2590                .await;
2591        registry
2592            .launch_initial_turn(
2593                "main",
2594                target,
2595                "Keep working until interrupted.".to_owned(),
2596                registry.reserve_turn().unwrap(),
2597            )
2598            .await
2599            .unwrap();
2600        timeout(Duration::from_secs(5), target_called.notified())
2601            .await
2602            .unwrap();
2603
2604        let receipt = registry
2605            .send_message(
2606                "main",
2607                target,
2608                MessagePriority::Deferred,
2609                MessagePurpose::Delegate,
2610                None,
2611                "This task must not become current before delivery.".to_owned(),
2612            )
2613            .await
2614            .unwrap();
2615        assert_eq!(receipt.disposition, MessageDisposition::Queued);
2616        let task = registry.state.lock().await.scopes["main"].sessions[&target]
2617            .descriptor
2618            .task
2619            .clone();
2620        assert_eq!(task, "wait forever");
2621
2622        registry.interrupt("main", target).await.unwrap();
2623        let task = registry.state.lock().await.scopes["main"].sessions[&target]
2624            .descriptor
2625            .task
2626            .clone();
2627        assert_eq!(task, "wait forever");
2628        registry.close_all("main").await.unwrap();
2629    }
2630
2631    #[tokio::test]
2632    async fn message_priorities_have_independent_mailbox_bounds() {
2633        let (registry, _control, _updates) = super::channel(0);
2634        let (_sender, sender_session) =
2635            insert_pending_runtime_session(&registry, "main", None, Arc::new(Notify::new())).await;
2636        let (target, _target_session) =
2637            insert_pending_runtime_session(&registry, "main", None, Arc::new(Notify::new())).await;
2638        mark_reusable(&registry, "main", target).await;
2639
2640        for index in 0..crate::harness::DEFERRED_CAPACITY {
2641            let receipt = registry
2642                .send_message(
2643                    &sender_session,
2644                    target,
2645                    MessagePriority::Deferred,
2646                    MessagePurpose::Coordinate,
2647                    None,
2648                    format!("queued message {index}"),
2649                )
2650                .await
2651                .unwrap();
2652            assert_eq!(receipt.disposition, MessageDisposition::Queued);
2653        }
2654        let normal_error = registry
2655            .send_message(
2656                &sender_session,
2657                target,
2658                MessagePriority::Deferred,
2659                MessagePurpose::Coordinate,
2660                None,
2661                "one message too many".to_owned(),
2662            )
2663            .await
2664            .unwrap_err();
2665        assert!(normal_error.to_string().contains("mailbox"));
2666
2667        for index in 0..crate::harness::URGENT_CAPACITY {
2668            let receipt = registry
2669                .send_message(
2670                    &sender_session,
2671                    target,
2672                    MessagePriority::Urgent,
2673                    MessagePurpose::Coordinate,
2674                    None,
2675                    format!("urgent queued message {index}"),
2676                )
2677                .await
2678                .unwrap();
2679            assert_eq!(receipt.disposition, MessageDisposition::Queued);
2680        }
2681        let urgent_error = registry
2682            .send_message(
2683                &sender_session,
2684                target,
2685                MessagePriority::Urgent,
2686                MessagePurpose::Coordinate,
2687                None,
2688                "one urgent message too many".to_owned(),
2689            )
2690            .await
2691            .unwrap_err();
2692        assert!(urgent_error.to_string().contains("mailbox"));
2693        registry.close_all("main").await.unwrap();
2694    }
2695
2696    #[tokio::test]
2697    async fn messages_do_not_cross_root_scopes() {
2698        let (registry, _control, _updates) = super::channel(32);
2699        let (target, _target_session) =
2700            insert_pending_runtime_session(&registry, "main", None, Arc::new(Notify::new())).await;
2701
2702        let error = registry
2703            .send_message(
2704                "other-root",
2705                target,
2706                MessagePriority::Deferred,
2707                MessagePurpose::Coordinate,
2708                None,
2709                "This must not reach the main tree.".to_owned(),
2710            )
2711            .await
2712            .unwrap_err();
2713
2714        assert!(error.to_string().contains("unknown agent_id"));
2715        registry.close_all("main").await.unwrap();
2716    }
2717
2718    fn insert_session(
2719        registry: &mut RegistryState,
2720        root_session_id: &str,
2721        id: AgentId,
2722        session_id: &str,
2723        parent: Option<AgentId>,
2724    ) {
2725        let session = test_session(id, session_id, parent);
2726        registry
2727            .insert(
2728                root_session_id.to_owned(),
2729                id,
2730                session.descriptor.session_id.clone(),
2731                session,
2732            )
2733            .unwrap();
2734    }
2735
2736    #[test]
2737    fn root_sessions_number_subagents_independently() {
2738        let mut registry = RegistryState::default();
2739
2740        let main = registry.reserve("main", None).unwrap();
2741        let fork = registry.reserve("fork", None).unwrap();
2742
2743        assert_eq!(main.id, AgentId::new(1));
2744        assert_eq!(main.root_session_id, "main");
2745        assert_eq!(fork.id, AgentId::new(1));
2746        assert_eq!(fork.root_session_id, "fork");
2747    }
2748
2749    #[test]
2750    fn descendant_sessions_use_their_root_namespace() {
2751        let mut registry = RegistryState::default();
2752        let root = registry.reserve("main", None).unwrap();
2753        registry
2754            .root_by_session
2755            .insert("child".to_owned(), root.root_session_id);
2756
2757        let descendant = registry.reserve("child", None).unwrap();
2758
2759        assert_eq!(descendant.id, AgentId::new(2));
2760        assert_eq!(descendant.root_session_id, "main");
2761    }
2762
2763    #[tokio::test]
2764    async fn child_sessions_automatically_own_new_subagents() {
2765        let mut registry = RegistryState::default();
2766        let parent = registry.reserve("main", None).unwrap();
2767        insert_session(
2768            &mut registry,
2769            &parent.root_session_id,
2770            parent.id,
2771            "parent-session",
2772            None,
2773        );
2774
2775        let child = registry.reserve_for("parent-session").unwrap();
2776
2777        assert_eq!(child.root_session_id, "main");
2778        assert_eq!(child.parent, Some(parent.id));
2779    }
2780
2781    #[tokio::test]
2782    async fn subagents_can_manage_descendants_but_not_siblings_or_ancestors() {
2783        let mut registry = RegistryState::default();
2784        let first = registry.reserve("main", None).unwrap();
2785        insert_session(
2786            &mut registry,
2787            &first.root_session_id,
2788            first.id,
2789            "first-session",
2790            None,
2791        );
2792        let second = registry.reserve("main", None).unwrap();
2793        insert_session(
2794            &mut registry,
2795            &second.root_session_id,
2796            second.id,
2797            "second-session",
2798            None,
2799        );
2800        let child = registry.reserve_for("first-session").unwrap();
2801        insert_session(
2802            &mut registry,
2803            &child.root_session_id,
2804            child.id,
2805            "child-session",
2806            Some(first.id),
2807        );
2808
2809        assert!(registry.summaries("first-session", &[child.id]).is_ok());
2810        assert!(registry.summaries("first-session", &[second.id]).is_err());
2811        assert!(registry.summaries("second-session", &[child.id]).is_err());
2812        assert!(registry.summaries("child-session", &[first.id]).is_err());
2813        assert_eq!(registry.summaries("main", &[child.id]).unwrap().len(), 1);
2814    }
2815
2816    #[tokio::test]
2817    async fn directory_separates_same_tree_messaging_from_management() {
2818        let mut registry = RegistryState::default();
2819        let parent = registry.reserve("main", None).unwrap();
2820        insert_session(
2821            &mut registry,
2822            &parent.root_session_id,
2823            parent.id,
2824            "parent-session",
2825            None,
2826        );
2827        let child = registry.reserve("main", Some(parent.id)).unwrap();
2828        insert_session(
2829            &mut registry,
2830            &child.root_session_id,
2831            child.id,
2832            "child-session",
2833            Some(parent.id),
2834        );
2835        let sibling = registry.reserve("main", None).unwrap();
2836        insert_session(
2837            &mut registry,
2838            &sibling.root_session_id,
2839            sibling.id,
2840            "sibling-session",
2841            None,
2842        );
2843
2844        for session in registry
2845            .scopes
2846            .get_mut("main")
2847            .unwrap()
2848            .sessions
2849            .values_mut()
2850        {
2851            session.status = AgentStatus::Completed {
2852                output: json!({ "report": "ready" }),
2853            };
2854        }
2855
2856        let directory = registry.directory("parent-session", true, false);
2857
2858        assert_eq!(
2859            directory
2860                .iter()
2861                .map(|entry| (entry.agent_id, entry.can_message, entry.can_manage))
2862                .collect::<Vec<_>>(),
2863            [(child.id, true, true), (sibling.id, true, false)]
2864        );
2865    }
2866
2867    #[tokio::test]
2868    async fn child_spawn_is_rejected_when_parent_closes_after_reservation() {
2869        let mut registry = RegistryState::default();
2870        let parent = registry.reserve("main", None).unwrap();
2871        insert_session(
2872            &mut registry,
2873            &parent.root_session_id,
2874            parent.id,
2875            "parent-session",
2876            None,
2877        );
2878        let child = registry.reserve_for("parent-session").unwrap();
2879        registry
2880            .scopes
2881            .get_mut("main")
2882            .unwrap()
2883            .sessions
2884            .get_mut(&parent.id)
2885            .unwrap()
2886            .status = AgentStatus::Closed;
2887        let session = test_session(child.id, "child-session", Some(parent.id));
2888
2889        let result = registry.insert(
2890            child.root_session_id,
2891            child.id,
2892            session.descriptor.session_id.clone(),
2893            session,
2894        );
2895
2896        assert!(result.is_err());
2897    }
2898
2899    #[tokio::test]
2900    async fn subtree_shutdown_order_includes_every_descendant_before_its_parent() {
2901        let mut registry = RegistryState::default();
2902        let parent = registry.reserve("main", None).unwrap();
2903        insert_session(
2904            &mut registry,
2905            &parent.root_session_id,
2906            parent.id,
2907            "parent-session",
2908            None,
2909        );
2910        let child = registry.reserve("parent-session", Some(parent.id)).unwrap();
2911        insert_session(
2912            &mut registry,
2913            &child.root_session_id,
2914            child.id,
2915            "child-session",
2916            Some(parent.id),
2917        );
2918        let grandchild = registry.reserve("child-session", Some(child.id)).unwrap();
2919        insert_session(
2920            &mut registry,
2921            &grandchild.root_session_id,
2922            grandchild.id,
2923            "grandchild-session",
2924            Some(child.id),
2925        );
2926
2927        assert_eq!(
2928            registry.subtree_shutdown_order("main", parent.id).unwrap(),
2929            [grandchild.id, child.id, parent.id]
2930        );
2931    }
2932
2933    #[tokio::test]
2934    async fn root_sessions_cannot_access_each_others_subagents() {
2935        let mut registry = RegistryState::default();
2936        let main = registry.reserve("main", None).unwrap();
2937        let session = test_session(main.id, "main-child", None);
2938        registry
2939            .insert(
2940                main.root_session_id,
2941                main.id,
2942                session.descriptor.session_id.clone(),
2943                session,
2944            )
2945            .unwrap();
2946
2947        assert!(registry.summaries("fork", &[main.id]).is_err());
2948        assert!(registry.reserve("fork", Some(main.id)).is_err());
2949    }
2950}