use async_trait::async_trait;
use atomr_agents_core::{InvokeCtx, Result, Value};
use serde::{Deserialize, Serialize};
use crate::descriptor::ToolDescriptor;
use crate::r#trait::Tool;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum ToolReturn {
Content(Value),
ContentAndArtifact { content: Value, artifact: Value },
Command(ToolControl),
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum ToolControl {
Handoff { target: String, payload: Value },
Done(Value),
Update(Vec<(String, Value)>),
}
#[async_trait]
pub trait RichTool: Send + Sync + 'static {
fn descriptor(&self) -> &ToolDescriptor;
async fn invoke_rich(&self, args: Value, ctx: &InvokeCtx) -> Result<ToolReturn>;
}
#[async_trait]
impl<T: RichTool> Tool for T {
fn descriptor(&self) -> &ToolDescriptor {
RichTool::descriptor(self)
}
async fn invoke(&self, args: Value, ctx: &InvokeCtx) -> Result<Value> {
match RichTool::invoke_rich(self, args, ctx).await? {
ToolReturn::Content(v) => Ok(v),
ToolReturn::ContentAndArtifact { content, .. } => Ok(content),
ToolReturn::Command(c) => Ok(serde_json::to_value(c).unwrap_or(Value::Null)),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::descriptor::ToolSchema;
use atomr_agents_core::{
CallCtx, InvokeCtx, IterationBudget, MoneyBudget, TimeBudget, TokenBudget, ToolId,
};
use std::time::Duration;
struct Handoff {
d: ToolDescriptor,
}
#[async_trait]
impl RichTool for Handoff {
fn descriptor(&self) -> &ToolDescriptor {
&self.d
}
async fn invoke_rich(&self, _args: Value, _ctx: &InvokeCtx) -> Result<ToolReturn> {
Ok(ToolReturn::Command(ToolControl::Handoff {
target: "specialist".into(),
payload: serde_json::json!({"why": "complex"}),
}))
}
}
fn ictx() -> InvokeCtx {
InvokeCtx {
call: CallCtx {
agent_id: None,
tokens: TokenBudget::new(1000),
time: TimeBudget::new(Duration::from_secs(5)),
money: MoneyBudget::from_usd(0.10),
iterations: IterationBudget::new(5),
trace: vec![],
},
tool_call_id: "t1".into(),
raw_args: Value::Null,
}
}
#[tokio::test]
async fn rich_tool_acts_as_plain_tool() {
let t = Handoff {
d: ToolDescriptor {
id: ToolId::from("handoff"),
name: "handoff".into(),
description: "delegate to specialist".into(),
schema: ToolSchema::empty_object(),
},
};
let v = Tool::invoke(&t, Value::Null, &ictx()).await.unwrap();
assert!(v.is_object()); }
#[tokio::test]
async fn rich_invoke_returns_command() {
let t = Handoff {
d: ToolDescriptor {
id: ToolId::from("handoff"),
name: "handoff".into(),
description: "delegate".into(),
schema: ToolSchema::empty_object(),
},
};
let r = t.invoke_rich(Value::Null, &ictx()).await.unwrap();
assert!(matches!(r, ToolReturn::Command(ToolControl::Handoff { .. })));
}
}