use std::collections::BTreeMap;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Arc;
use serde::{Deserialize, Serialize};
use crate::error::{Error, Result};
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct NamedAgentDefinition {
pub name: String,
pub system_prompt: String,
pub tools: Option<Vec<String>>,
pub model: Option<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BackgroundPromptsPolicy {
AutoPolicy,
Parent,
}
impl BackgroundPromptsPolicy {
pub fn parse(s: &str) -> Option<Self> {
match s {
"auto_policy" => Some(BackgroundPromptsPolicy::AutoPolicy),
"parent" => Some(BackgroundPromptsPolicy::Parent),
_ => None,
}
}
pub fn as_str(self) -> &'static str {
match self {
BackgroundPromptsPolicy::AutoPolicy => "auto_policy",
BackgroundPromptsPolicy::Parent => "parent",
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct SubagentLineage {
pub child_agent_id: String,
pub parent_session_id: Option<String>,
pub parent_tool_use_id: String,
pub depth: usize,
pub agent_type: Option<String>,
pub task: String,
pub background: bool,
pub spawned_at_ms: i64,
pub model: String,
}
impl SubagentLineage {
pub fn to_lineage_map(&self) -> BTreeMap<String, String> {
let mut m = BTreeMap::new();
if let Some(p) = &self.parent_session_id {
m.insert("parent_thread_id".to_string(), p.clone());
m.insert("parent_session_id".to_string(), p.clone());
}
m.insert("depth".to_string(), self.depth.to_string());
if let Some(t) = &self.agent_type {
m.insert("agent_role".to_string(), t.clone());
}
m.insert(
"thread_source".to_string(),
"supercode_native_spawn".to_string(),
);
m
}
}
#[derive(Debug, Clone)]
pub struct QueuedApproval {
pub child_agent_id: String,
pub tool: String,
pub subject: Option<String>,
pub queued_at_ms: i64,
}
pub struct ParentQueueApprovalHandler {
pub child_agent_id: String,
pub queue: Arc<std::sync::Mutex<Vec<QueuedApproval>>>,
}
impl crate::permissions::PermissionsApprovalHandler for ParentQueueApprovalHandler {
fn ask(
&self,
req: &crate::permissions::ApprovalRequest,
) -> crate::permissions::ApprovalOutcome {
let record = QueuedApproval {
child_agent_id: self.child_agent_id.clone(),
tool: req.tool.to_string(),
subject: req.subject.map(String::from),
queued_at_ms: now_ms(),
};
if let Ok(mut q) = self.queue.lock() {
q.push(record);
}
crate::permissions::ApprovalOutcome::Deny
}
}
fn now_ms() -> i64 {
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_millis() as i64)
.unwrap_or(0)
}
pub fn check_depth(current_depth: usize, max_depth: usize) -> Result<()> {
if current_depth >= max_depth {
return Err(Error::SubagentDepthExceeded {
max_depth,
attempted_depth: current_depth + 1,
});
}
Ok(())
}
pub struct ConcurrencyGuard(Arc<AtomicUsize>);
impl Drop for ConcurrencyGuard {
fn drop(&mut self) {
self.0.fetch_sub(1, Ordering::SeqCst);
}
}
pub fn try_acquire(gauge: &Arc<AtomicUsize>, max_concurrent: usize) -> Option<ConcurrencyGuard> {
let mut current = gauge.load(Ordering::SeqCst);
loop {
if current >= max_concurrent {
return None;
}
match gauge.compare_exchange(current, current + 1, Ordering::SeqCst, Ordering::SeqCst) {
Ok(_) => return Some(ConcurrencyGuard(gauge.clone())),
Err(actual) => current = actual,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn background_prompts_policy_round_trips_through_its_schema_string() {
for p in [
BackgroundPromptsPolicy::AutoPolicy,
BackgroundPromptsPolicy::Parent,
] {
assert_eq!(BackgroundPromptsPolicy::parse(p.as_str()), Some(p));
}
assert_eq!(BackgroundPromptsPolicy::parse("bogus"), None);
assert_eq!(BackgroundPromptsPolicy::parse(""), None);
}
#[test]
fn check_depth_allows_up_to_the_cap_and_refuses_past_it() {
assert!(check_depth(0, 2).is_ok());
assert!(check_depth(1, 2).is_ok());
let err = check_depth(2, 2).unwrap_err();
match err {
Error::SubagentDepthExceeded {
max_depth,
attempted_depth,
} => {
assert_eq!(max_depth, 2);
assert_eq!(attempted_depth, 3);
}
other => panic!("expected SubagentDepthExceeded, got {other:?}"),
}
}
#[test]
fn check_depth_zero_cap_refuses_every_spawn() {
assert!(check_depth(0, 0).is_err());
}
#[test]
fn try_acquire_enforces_the_cap_and_release_frees_a_slot() {
let gauge = Arc::new(AtomicUsize::new(0));
let g1 = try_acquire(&gauge, 2).expect("first slot free");
let g2 = try_acquire(&gauge, 2).expect("second slot free");
assert!(
try_acquire(&gauge, 2).is_none(),
"cap of 2 must refuse a third concurrent holder"
);
drop(g1);
let g3 = try_acquire(&gauge, 2).expect("a released slot must be reusable");
drop(g2);
drop(g3);
assert_eq!(gauge.load(Ordering::SeqCst), 0);
}
#[test]
fn try_acquire_zero_cap_never_grants_a_slot() {
let gauge = Arc::new(AtomicUsize::new(0));
assert!(try_acquire(&gauge, 0).is_none());
}
#[test]
fn lineage_to_map_carries_parent_thread_id_alias_and_depth() {
let rec = SubagentLineage {
child_agent_id: "agent-1".to_string(),
parent_session_id: Some("sess-a".to_string()),
parent_tool_use_id: "call_1".to_string(),
depth: 1,
agent_type: Some("researcher".to_string()),
task: "look into X".to_string(),
background: false,
spawned_at_ms: 1_700_000_000_000,
model: "vendor/model-a".to_string(),
};
let m = rec.to_lineage_map();
assert_eq!(m.get("parent_thread_id"), Some(&"sess-a".to_string()));
assert_eq!(m.get("parent_session_id"), Some(&"sess-a".to_string()));
assert_eq!(m.get("depth"), Some(&"1".to_string()));
assert_eq!(m.get("agent_role"), Some(&"researcher".to_string()));
assert_eq!(
m.get("thread_source"),
Some(&"supercode_native_spawn".to_string())
);
}
#[test]
fn parent_queue_handler_never_hangs_and_records_the_request() {
use crate::permissions::{ApprovalOutcome, ApprovalRequest, PermissionsApprovalHandler};
let queue = Arc::new(std::sync::Mutex::new(Vec::new()));
let handler = ParentQueueApprovalHandler {
child_agent_id: "agent-bg-1".to_string(),
queue: queue.clone(),
};
let args = serde_json::json!({});
let outcome = handler.ask(&ApprovalRequest {
tool: "bash",
subject: Some("rm -rf /tmp/x"),
raw_args: &args,
});
assert_eq!(outcome, ApprovalOutcome::Deny);
let recorded = queue.lock().unwrap();
assert_eq!(recorded.len(), 1);
assert_eq!(recorded[0].child_agent_id, "agent-bg-1");
assert_eq!(recorded[0].tool, "bash");
assert_eq!(recorded[0].subject.as_deref(), Some("rm -rf /tmp/x"));
}
#[test]
fn lineage_round_trips_through_json() {
let rec = SubagentLineage {
child_agent_id: "agent-2".to_string(),
parent_session_id: None,
parent_tool_use_id: "call_9".to_string(),
depth: 0,
agent_type: None,
task: "t".to_string(),
background: true,
spawned_at_ms: 42,
model: "vendor/model-b".to_string(),
};
let json = serde_json::to_string(&rec).unwrap();
let back: SubagentLineage = serde_json::from_str(&json).unwrap();
assert_eq!(rec, back);
}
}