Skip to main content

harness_loop/
subagent.rs

1//! Subagent: an isolated agent loop spawned from a parent.
2//!
3//! Per DESIGN.md ยง8, a subagent has:
4//! - independent `Context`
5//! - restricted tool / sensor / guide set
6//! - its own iteration budget
7//!
8//! A subagent reports back via [`SubagentStatus`] (the Superpowers
9//! convention: Done / DoneWithConcerns / Blocked / NeedsContext).
10
11use crate::{AgentLoop, Outcome};
12use harness_core::{Guide, HarnessError, Model, Sensor, SubagentStatus, Task, Tool, World};
13use std::sync::Arc;
14
15/// What a subagent needs to run.
16pub struct SubagentSpec {
17    pub name: String,
18    pub task: Task,
19    pub tools: Vec<Arc<dyn Tool>>,
20    pub guides: Vec<Arc<dyn Guide>>,
21    pub sensors: Vec<Arc<dyn Sensor>>,
22    pub max_iters: u32,
23}
24
25impl SubagentSpec {
26    pub fn new(name: impl Into<String>, task: Task) -> Self {
27        Self {
28            name: name.into(),
29            task,
30            tools: Vec::new(),
31            guides: Vec::new(),
32            sensors: Vec::new(),
33            max_iters: 12,
34        }
35    }
36
37    pub fn with_tool(mut self, t: Arc<dyn Tool>) -> Self {
38        self.tools.push(t);
39        self
40    }
41
42    pub fn with_guide(mut self, g: Arc<dyn Guide>) -> Self {
43        self.guides.push(g);
44        self
45    }
46
47    pub fn with_sensor(mut self, s: Arc<dyn Sensor>) -> Self {
48        self.sensors.push(s);
49        self
50    }
51
52    pub fn with_max_iters(mut self, n: u32) -> Self {
53        self.max_iters = n;
54        self
55    }
56}
57
58/// A subagent's structured report back to the parent.
59#[derive(Debug, Clone)]
60pub struct SubagentReport {
61    pub name: String,
62    pub status: SubagentStatus,
63    pub text: Option<String>,
64    pub iters: u32,
65    /// Token usage accumulated by the subagent's loop. Lets callers
66    /// (e.g. `harness-loop-engine`'s `TokenBudget`) account for spend
67    /// across maker/checker turns.
68    pub usage: harness_core::Usage,
69}
70
71/// Bind a `Model` to a `SubagentSpec` and run it.
72pub struct Subagent<M: Model> {
73    pub spec: SubagentSpec,
74    pub loop_: AgentLoop<M>,
75}
76
77impl<M: Model> Subagent<M> {
78    pub fn new(model: M, spec: SubagentSpec) -> Self {
79        let mut loop_ = AgentLoop::new(model);
80        for t in &spec.tools {
81            loop_ = loop_.with_tool(t.clone());
82        }
83        for g in &spec.guides {
84            loop_ = loop_.with_guide(g.clone());
85        }
86        for s in &spec.sensors {
87            loop_ = loop_.with_sensor(s.clone());
88        }
89        Self { spec, loop_ }
90    }
91
92    pub async fn run(self, world: &mut World) -> Result<SubagentReport, HarnessError> {
93        let name = self.spec.name.clone();
94        let max = self.spec.max_iters;
95        let task = self.spec.task.clone();
96        let outcome = self.loop_.run_with_max_iters(task, world, max).await?;
97        let report = match outcome {
98            Outcome::Done {
99                text, iters, usage, ..
100            } => SubagentReport {
101                name,
102                status: SubagentStatus::Done,
103                text,
104                iters,
105                usage,
106            },
107            Outcome::BudgetExhausted {
108                iters,
109                last_text,
110                usage,
111                ..
112            } => SubagentReport {
113                name,
114                status: SubagentStatus::Blocked,
115                text: last_text,
116                iters,
117                usage,
118            },
119        };
120        tracing::info!(
121            subagent = %report.name,
122            status = ?report.status,
123            iters = report.iters,
124            "subagent completed"
125        );
126        Ok(report)
127    }
128}