1use 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
81struct 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#[derive(Clone)]
102pub struct RootAgentAuthority {
103 registry: Weak<Registry>,
104}
105
106#[derive(Clone, Copy, Debug, Eq, Error, PartialEq)]
108pub enum AuthorityError {
109 #[error("subagent runtime is closed")]
111 RuntimeClosed,
112 #[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 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 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#[derive(Clone)]
1600pub struct Subagents {
1601 pub(crate) registry: Arc<Registry>,
1602}
1603
1604#[derive(Clone)]
1609pub struct WeakSubagents {
1610 pub(crate) registry: Weak<Registry>,
1611}
1612
1613impl Subagents {
1614 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 pub fn downgrade(&self) -> WeakSubagents {
1628 WeakSubagents {
1629 registry: Arc::downgrade(&self.registry),
1630 }
1631 }
1632
1633 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 pub fn root_agent_authority(&self) -> RootAgentAuthority {
1660 self.downgrade().root_agent_authority()
1661 }
1662
1663 pub fn set_max_concurrency(&self, limit: usize) {
1668 self.registry.set_max_concurrency(limit);
1669 }
1670
1671 pub fn set_thinking(&self, thinking: Thinking) {
1673 self.registry.set_agent_thinking(thinking);
1674 }
1675
1676 pub fn set_fast_mode(&self, enabled: bool) {
1678 self.registry.set_agent_fast_mode(enabled);
1679 }
1680
1681 pub async fn cancel_all(&self, root_session_id: &str) {
1685 self.registry.cancel_all(root_session_id).await;
1686 }
1687
1688 pub async fn close_all(&self, root_session_id: &str) {
1692 drop(self.registry.close_all(root_session_id).await);
1693 }
1694
1695 pub fn runtime_id(&self) -> SubagentRuntimeId {
1697 self.registry.id
1698 }
1699}
1700
1701impl WeakSubagents {
1702 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(®istry),
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(®istry, &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 ®istry,
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(®istry, &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(®istry, "main", None, Arc::clone(&sender_called))
2373 .await;
2374 let (target, _target_session) =
2375 insert_pending_runtime_session(®istry, "main", None, Arc::clone(&target_called))
2376 .await;
2377 mark_reusable(®istry, "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(®istry, "main", None, Arc::new(Notify::new())).await;
2413 let (target, _target_session) =
2414 insert_pending_runtime_session(®istry, "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(®istry, "main", None, Arc::new(Notify::new())).await;
2437 let (target, _target_session) =
2438 insert_pending_runtime_session(®istry, "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(®istry, "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 ®istry,
2464 "main",
2465 Some(parent),
2466 Arc::clone(&child_called),
2467 )
2468 .await;
2469 mark_reusable(®istry, "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(®istry, "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(®istry, "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(®istry, "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(®istry, "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(®istry, "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(®istry, "main", None, Arc::new(Notify::new())).await;
2636 let (target, _target_session) =
2637 insert_pending_runtime_session(®istry, "main", None, Arc::new(Notify::new())).await;
2638 mark_reusable(®istry, "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(®istry, "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}