Skip to main content

harness_rs_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::{
13    Guide, HarnessError, Model, Sensor, SubagentStatus, Task, Tool, World,
14};
15use std::sync::Arc;
16
17/// What a subagent needs to run.
18pub struct SubagentSpec {
19    pub name:      String,
20    pub task:      Task,
21    pub tools:     Vec<Arc<dyn Tool>>,
22    pub guides:    Vec<Arc<dyn Guide>>,
23    pub sensors:   Vec<Arc<dyn Sensor>>,
24    pub max_iters: u32,
25}
26
27impl SubagentSpec {
28    pub fn new(name: impl Into<String>, task: Task) -> Self {
29        Self {
30            name: name.into(),
31            task,
32            tools:   Vec::new(),
33            guides:  Vec::new(),
34            sensors: Vec::new(),
35            max_iters: 12,
36        }
37    }
38
39    pub fn with_tool(mut self, t: Arc<dyn Tool>) -> Self {
40        self.tools.push(t);
41        self
42    }
43
44    pub fn with_guide(mut self, g: Arc<dyn Guide>) -> Self {
45        self.guides.push(g);
46        self
47    }
48
49    pub fn with_sensor(mut self, s: Arc<dyn Sensor>) -> Self {
50        self.sensors.push(s);
51        self
52    }
53
54    pub fn with_max_iters(mut self, n: u32) -> Self {
55        self.max_iters = n;
56        self
57    }
58}
59
60/// A subagent's structured report back to the parent.
61#[derive(Debug, Clone)]
62pub struct SubagentReport {
63    pub name:   String,
64    pub status: SubagentStatus,
65    pub text:   Option<String>,
66    pub iters:  u32,
67}
68
69/// Bind a `Model` to a `SubagentSpec` and run it.
70pub struct Subagent<M: Model> {
71    pub spec:   SubagentSpec,
72    pub loop_:  AgentLoop<M>,
73}
74
75impl<M: Model> Subagent<M> {
76    pub fn new(model: M, spec: SubagentSpec) -> Self {
77        let mut loop_ = AgentLoop::new(model);
78        for t in &spec.tools   { loop_ = loop_.with_tool(t.clone()); }
79        for g in &spec.guides  { loop_ = loop_.with_guide(g.clone()); }
80        for s in &spec.sensors { loop_ = loop_.with_sensor(s.clone()); }
81        Self { spec, loop_ }
82    }
83
84    pub async fn run(self, world: &mut World) -> Result<SubagentReport, HarnessError> {
85        let name = self.spec.name.clone();
86        let max  = self.spec.max_iters;
87        let task = self.spec.task.clone();
88        let outcome = self.loop_.run_with_max_iters(task, world, max).await?;
89        let report = match outcome {
90            Outcome::Done { text, iters } => SubagentReport {
91                name,
92                status: SubagentStatus::Done,
93                text,
94                iters,
95            },
96            Outcome::BudgetExhausted { iters } => SubagentReport {
97                name,
98                status: SubagentStatus::Blocked,
99                text: None,
100                iters,
101            },
102        };
103        tracing::info!(
104            subagent = %report.name,
105            status = ?report.status,
106            iters = report.iters,
107            "subagent completed"
108        );
109        Ok(report)
110    }
111}