use crate::bus::{JobQueue, KvStore, Pubsub};
use crate::error::ToolError;
use async_trait::async_trait;
use std::sync::Arc;
#[derive(Clone, Debug)]
pub struct CallerPrincipal(String);
impl CallerPrincipal {
pub fn new(value: impl Into<String>) -> Self {
Self(value.into())
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
}
#[derive(Clone, Debug)]
pub struct ParentAnchor(String);
impl ParentAnchor {
pub fn new(value: impl Into<String>) -> Self {
Self(value.into())
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
}
#[derive(Clone)]
#[non_exhaustive]
pub struct ToolCtx {
pub pubsub: Arc<dyn Pubsub>,
pub kv: Arc<dyn KvStore>,
pub jobs: Arc<dyn JobQueue>,
pub progress: Option<tokio::sync::broadcast::Sender<crate::AgentEvent>>,
pub cancel: tokio_util::sync::CancellationToken,
pub server_outbound: Option<Arc<dyn crate::ServerOutbound>>,
pub caller_principal: Option<CallerPrincipal>,
pub parent_anchor: Option<ParentAnchor>,
}
impl ToolCtx {
pub fn new(pubsub: Arc<dyn Pubsub>, kv: Arc<dyn KvStore>, jobs: Arc<dyn JobQueue>) -> Self {
Self {
pubsub,
kv,
jobs,
progress: None,
cancel: tokio_util::sync::CancellationToken::new(),
server_outbound: None,
caller_principal: None,
parent_anchor: None,
}
}
#[must_use]
pub fn with_cancel(mut self, cancel: tokio_util::sync::CancellationToken) -> Self {
self.cancel = cancel;
self
}
#[must_use]
pub fn with_progress(
mut self,
progress: tokio::sync::broadcast::Sender<crate::AgentEvent>,
) -> Self {
self.progress = Some(progress);
self
}
#[must_use]
pub fn with_server_outbound(mut self, outbound: Arc<dyn crate::ServerOutbound>) -> Self {
self.server_outbound = Some(outbound);
self
}
#[must_use]
pub fn with_caller_principal(mut self, principal: String) -> Self {
self.caller_principal = Some(CallerPrincipal::new(principal));
self
}
#[must_use]
pub fn with_parent_anchor(mut self, anchor: String) -> Self {
self.parent_anchor = Some(ParentAnchor::new(anchor));
self
}
}
#[async_trait]
pub trait Tool: Send + Sync {
fn name(&self) -> &str;
fn description(&self) -> &str;
fn json_schema(&self) -> &serde_json::Value;
async fn invoke(
&self,
args: serde_json::Value,
ctx: ToolCtx,
) -> Result<serde_json::Value, ToolError>;
fn is_effectful(&self) -> bool {
false
}
fn redacts_audit(&self) -> bool {
false
}
}
#[async_trait]
pub trait ToolInvoker: Send + Sync {
async fn invoke(
&self,
name: &str,
args: serde_json::Value,
ctx: ToolCtx,
) -> Result<serde_json::Value, ToolError>;
fn catalogue(&self) -> Vec<crate::llm::ToolDef>;
fn is_tool_idempotent(&self, _name: &str) -> bool {
false
}
fn tool_redacts_audit(&self, _name: &str) -> bool {
false
}
}
#[cfg(test)]
mod tests {
use super::*;
#[allow(dead_code)]
fn _assert_dyn_invoker(_: &dyn ToolInvoker) {}
#[allow(dead_code)]
fn _assert_dyn_tool(_: &dyn Tool) {}
struct StubInvoker;
#[async_trait::async_trait]
impl ToolInvoker for StubInvoker {
fn catalogue(&self) -> Vec<crate::llm::ToolDef> {
vec![]
}
async fn invoke(
&self,
_name: &str,
_args: serde_json::Value,
_ctx: ToolCtx,
) -> Result<serde_json::Value, ToolError> {
Ok(serde_json::Value::Null)
}
}
#[test]
fn default_is_tool_idempotent_returns_false() {
let inv = StubInvoker;
assert!(!inv.is_tool_idempotent("anything"));
assert!(!inv.is_tool_idempotent(""));
}
}
#[cfg(test)]
mod ctx_tests {
use super::*;
use crate::server_outbound::{ServerOutbound, ServerOutboundError};
use crate::test_utils::noop_bus;
use std::time::Duration;
use tokio_util::sync::CancellationToken;
#[test]
fn new_defaults_progress_none_and_cancel_uncancelled() {
let (pubsub, _, kv, jobs) = noop_bus();
let ctx = ToolCtx::new(pubsub, kv, jobs);
assert!(ctx.progress.is_none());
assert!(!ctx.cancel.is_cancelled());
}
#[test]
fn with_cancel_overrides_default_token() {
let (pubsub, _, kv, jobs) = noop_bus();
let token = CancellationToken::new();
let ctx = ToolCtx::new(pubsub, kv, jobs).with_cancel(token.clone());
token.cancel();
assert!(ctx.cancel.is_cancelled());
}
#[test]
fn with_progress_sets_sender() {
let (pubsub, _, kv, jobs) = noop_bus();
let (tx, _rx) = tokio::sync::broadcast::channel::<crate::AgentEvent>(8);
let ctx = ToolCtx::new(pubsub, kv, jobs).with_progress(tx);
assert!(ctx.progress.is_some());
}
struct StubOutbound;
#[async_trait::async_trait]
impl ServerOutbound for StubOutbound {
async fn outbound_request(
&self,
_method: &str,
_params: serde_json::Value,
_timeout: Duration,
) -> Result<serde_json::Value, ServerOutboundError> {
Err(ServerOutboundError::Unsupported)
}
}
#[test]
fn new_defaults_server_outbound_none() {
let (pubsub, _, kv, jobs) = noop_bus();
let ctx = ToolCtx::new(pubsub, kv, jobs);
assert!(ctx.server_outbound.is_none());
}
#[test]
fn with_server_outbound_attaches_channel() {
let (pubsub, _, kv, jobs) = noop_bus();
let outbound: Arc<dyn ServerOutbound> = Arc::new(StubOutbound);
let ctx = ToolCtx::new(pubsub, kv, jobs).with_server_outbound(outbound);
assert!(ctx.server_outbound.is_some());
}
#[test]
fn new_defaults_caller_principal_none() {
let (pubsub, _, kv, jobs) = noop_bus();
let ctx = ToolCtx::new(pubsub, kv, jobs);
assert!(ctx.caller_principal.is_none());
}
#[test]
fn with_caller_principal_records_value() {
let (pubsub, _, kv, jobs) = noop_bus();
let ctx = ToolCtx::new(pubsub, kv, jobs).with_caller_principal("alice@x".into());
assert_eq!(
ctx.caller_principal.as_ref().map(CallerPrincipal::as_str),
Some("alice@x")
);
}
#[test]
fn new_defaults_parent_anchor_none() {
let (pubsub, _, kv, jobs) = noop_bus();
let ctx = ToolCtx::new(pubsub, kv, jobs);
assert!(ctx.parent_anchor.is_none());
}
#[test]
fn with_parent_anchor_records_value() {
let (pubsub, _, kv, jobs) = noop_bus();
let ctx = ToolCtx::new(pubsub, kv, jobs).with_parent_anchor("sha256:abc123".into());
assert_eq!(
ctx.parent_anchor.as_ref().map(ParentAnchor::as_str),
Some("sha256:abc123")
);
}
}