use crate::orchestrator::job::{Job, JobId, JobResult};
use crate::{Subagent, SubagentSpec};
use async_trait::async_trait;
use harness_core::{Model, SubagentStatus, Task, Tool, World};
use std::sync::Arc;
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum JobError {
#[error("job runner: {0}")]
Run(String),
#[error("job blocked: {0}")]
Blocked(String),
}
#[async_trait(?Send)]
pub trait JobRunner {
async fn run(&self, job: &Job, deps: &[(JobId, JobResult)]) -> Result<JobResult, JobError>;
}
pub type WorldFactory = Arc<dyn Fn() -> World + Send + Sync>;
pub struct SubagentJobRunner {
model: Arc<dyn Model>,
tools: Vec<Arc<dyn Tool>>,
world_factory: WorldFactory,
max_iters: u32,
}
impl SubagentJobRunner {
pub fn new(model: Arc<dyn Model>, root: impl Into<std::path::PathBuf>) -> Self {
let root = root.into();
Self {
model,
tools: Vec::new(),
world_factory: Arc::new(move || harness_context::default_world(root.clone())),
max_iters: 12,
}
}
pub fn with_tool(mut self, t: Arc<dyn Tool>) -> Self {
self.tools.push(t);
self
}
pub fn with_max_iters(mut self, n: u32) -> Self {
self.max_iters = n;
self
}
pub fn with_world_factory(mut self, f: WorldFactory) -> Self {
self.world_factory = f;
self
}
fn task_description(&self, job: &Job, deps: &[(JobId, JobResult)]) -> String {
job_prompt(job, deps)
}
}
pub fn job_prompt(job: &Job, deps: &[(JobId, JobResult)]) -> String {
let mut lead = String::new();
if let Some(f) = &job.feedback {
lead.push_str(&format!("Your previous answer was rejected: {f}\n"));
}
if let Some(prev) = &job.prior_attempt {
lead.push_str(&format!(
"What you answered last time (do not repeat it — improve on it):\n{}\n\n",
prev.trim()
));
}
if deps.is_empty() {
return format!("{lead}{}", job.prompt);
}
let mut d = lead;
d.push_str("Results from upstream jobs you depend on:\n");
for (id, r) in deps {
d.push_str(&format!("--- {id} ---\n{}\n", r.text.trim()));
}
d.push_str("\nYour task:\n");
d.push_str(&job.prompt);
d
}
#[async_trait(?Send)]
impl JobRunner for SubagentJobRunner {
async fn run(&self, job: &Job, deps: &[(JobId, JobResult)]) -> Result<JobResult, JobError> {
let mut world = (self.world_factory)();
let mut spec = SubagentSpec::new(
job.id.clone(),
Task {
description: self.task_description(job, deps),
source: None,
deadline: None,
},
)
.with_max_iters(self.max_iters);
for t in &self.tools {
spec = spec.with_tool(t.clone());
}
let sub = Subagent::new(harness_core::DynModel(self.model.clone()), spec);
let report = sub
.run(&mut world)
.await
.map_err(|e| JobError::Run(e.to_string()))?;
match report.status {
SubagentStatus::Done | SubagentStatus::DoneWithConcerns => Ok(JobResult {
text: report.text.unwrap_or_default(),
input_tokens: report.usage.input_tokens as u64,
output_tokens: report.usage.output_tokens as u64,
}),
SubagentStatus::Blocked => Err(JobError::Blocked(
report
.text
.unwrap_or_else(|| "sub-agent budget exhausted".into()),
)),
SubagentStatus::NeedsContext => Err(JobError::Blocked(
report
.text
.unwrap_or_else(|| "sub-agent needs more context".into()),
)),
_ => Err(JobError::Blocked("sub-agent did not complete".into())),
}
}
}