use std::collections::HashMap;
use std::sync::Arc;
use serde_json::{Map, Value, json};
use super::agent_loop::guarded_hooks;
use super::runtime::{
Budget, CloseOptions, Def, OnCloseFn, OnSpawnFn, Options, Runtime, TaskResult,
};
use crate::client::{BeforeToolEvent, Hooks, OnMetricFn, RunResult, WaitForFn};
use crate::tool::{Tool, ToolContext, ToolResult, ToolSource};
pub type Guardrail = Arc<dyn Fn(&BeforeToolEvent) -> String + Send + Sync>;
pub type VerifyFn = Arc<dyn Fn(&RunResult) -> (bool, String) + Send + Sync>;
#[derive(Clone, Default)]
pub struct Completion {
pub verify: Option<VerifyFn>,
pub max_attempts: usize,
}
impl Completion {
pub fn new(
verify: impl Fn(&RunResult) -> (bool, String) + Send + Sync + 'static,
max_attempts: usize,
) -> Self {
Completion {
verify: Some(Arc::new(verify)),
max_attempts,
}
}
}
#[derive(Clone, Default)]
pub struct Spec {
pub does: String,
pub tools: Vec<Tool>,
pub soul: String,
pub soul_file: String,
pub team: Vec<Agent>,
pub budget: Option<Budget>,
pub model: String,
pub wait_for: Option<WaitForFn>,
pub on_spawn: Option<OnSpawnFn>,
pub on_close: Option<OnCloseFn>,
pub hooks: Option<Hooks>,
pub on_metric: Option<OnMetricFn>,
pub guardrails: Vec<Guardrail>,
pub completion: Option<Completion>,
}
#[derive(Clone)]
pub struct Agent {
pub name: String,
pub spec: Spec,
}
impl std::fmt::Debug for Agent {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Agent")
.field("name", &self.name)
.finish_non_exhaustive()
}
}
pub fn new(name: impl Into<String>, spec: Spec) -> Agent {
Agent {
name: name.into(),
spec,
}
}
pub fn harness(s: Spec) -> Spec {
s
}
pub type AsToolOptionsFn = Arc<dyn Fn(&ToolContext) -> Options + Send + Sync>;
impl Agent {
pub fn registry(&self) -> HashMap<String, Def> {
let mut acc = HashMap::new();
self.registry_into(&mut acc);
acc
}
fn registry_into(&self, acc: &mut HashMap<String, Def>) {
if acc.contains_key(&self.name) {
return;
}
let sp = &self.spec;
let mut soul = sp.soul.clone();
if !sp.soul_file.is_empty()
&& let Ok(s) = std::fs::read_to_string(&sp.soul_file)
{
soul = s;
}
let model = if sp.model.is_empty() {
"inherit".to_string()
} else {
sp.model.clone()
};
acc.insert(
self.name.clone(),
Def {
name: self.name.clone(),
does: sp.does.clone(),
soul,
model,
tools: sp.tools.clone(),
team: sp.team.iter().map(|t| t.name.clone()).collect(),
budget: sp.budget,
wait_for: sp.wait_for.clone(),
on_spawn: sp.on_spawn.clone(),
on_close: sp.on_close.clone(),
hooks: guarded_hooks(sp),
completion: sp.completion.clone(),
on_metric: sp.on_metric.clone(),
},
);
for t in &sp.team {
t.registry_into(acc);
}
}
pub async fn run(&self, mut opts: Options, prompt: &str) -> (TaskResult, Runtime) {
opts.registry = self.registry();
let rt = Runtime::new(opts);
let root = rt.root();
let h = match rt.spawn(&root, &self.name, None) {
Ok(h) => h,
Err(e) => {
return (
TaskResult {
text: e,
is_error: true,
status: "error".into(),
..Default::default()
},
rt,
);
}
};
rt.wake(&h, prompt);
let r = rt.wait(&h, None).await;
if r.status != "pending" {
rt.close(&root, None).await;
}
(r, rt)
}
pub fn as_tool(&self, opts: Options) -> Tool {
self.as_tool_with(Arc::new(move |_| opts.clone()))
}
pub fn as_tool_with(&self, per: AsToolOptionsFn) -> Tool {
let agent = self.clone();
Tool {
concurrency_safe: None,
name: self.name.clone(),
description: self.spec.does.clone(),
input_schema: json!({
"type": "object",
"properties": {"prompt": {"type": "string"}},
"required": ["prompt"],
}),
source: ToolSource::Custom,
hints: None,
execute: Arc::new(move |args, ctx| {
let agent = agent.clone();
let per = per.clone();
Box::pin(async move {
let prompt = args.get("prompt").and_then(Value::as_str).unwrap_or("");
let r = agent.run_cancellable(&ctx, per(&ctx), prompt).await;
let mut meta = Map::new();
meta.insert("agent".into(), json!(agent.name));
meta.insert("turns".into(), json!(r.turns));
meta.insert("totalTokens".into(), json!(r.total_tokens));
if r.status == "cancelled" {
return ToolResult {
output: format!("cancelled: {}", r.text),
is_error: true,
metadata: Some(meta),
..Default::default()
};
}
ToolResult {
output: r.text,
is_error: r.is_error,
metadata: Some(meta),
..Default::default()
}
})
}),
}
}
async fn run_cancellable(
&self,
ctx: &ToolContext,
mut opts: Options,
prompt: &str,
) -> TaskResult {
let cancelled = || TaskResult {
text: "context canceled".into(),
is_error: true,
status: "cancelled".into(),
..Default::default()
};
if ctx.cancel.is_cancelled() {
return cancelled();
}
opts.registry = self.registry();
let rt = Runtime::new(opts);
let root = rt.root();
let h = match rt.spawn(&root, &self.name, None) {
Ok(h) => h,
Err(e) => {
return TaskResult {
text: e,
is_error: true,
status: "error".into(),
..Default::default()
};
}
};
let stop = tokio_util::sync::CancellationToken::new();
let watcher = {
let (rt, root, stop, cancel) =
(rt.clone(), root.clone(), stop.clone(), ctx.cancel.clone());
tokio::spawn(async move {
tokio::select! {
_ = cancel.cancelled() => {
rt.close(&root, Some(CloseOptions { force: true, reason: "cancelled".into() })).await;
}
_ = stop.cancelled() => {}
}
})
};
rt.wake(&h, prompt);
let r = rt.wait(&h, None).await;
stop.cancel();
let _ = watcher.await;
if ctx.cancel.is_cancelled() {
let mut c = cancelled();
c.turns = r.turns;
c.total_tokens = r.total_tokens;
return c;
}
if r.status != "pending" {
rt.close(&root, None).await;
}
r
}
}