use std::sync::Arc;
pub type ToolHandler = Arc<dyn Fn(serde_json::Value) -> Result<String, String> + Send + Sync>;
#[derive(Clone)]
pub struct SeherTool {
pub name: String,
pub description: String,
pub parameters: serde_json::Value,
pub handler: ToolHandler,
}
impl SeherTool {
#[must_use]
pub fn new(
name: impl Into<String>,
description: impl Into<String>,
parameters: serde_json::Value,
handler: ToolHandler,
) -> Self {
Self {
name: name.into(),
description: description.into(),
parameters,
handler,
}
}
}
impl std::fmt::Debug for SeherTool {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("SeherTool")
.field("name", &self.name)
.field("description", &self.description)
.field("parameters", &self.parameters)
.finish_non_exhaustive()
}
}
pub(crate) struct PiToolAdapter {
tool: SeherTool,
}
impl PiToolAdapter {
pub(crate) const fn new(tool: SeherTool) -> Self {
Self { tool }
}
}
#[async_trait::async_trait]
impl pi::tools::Tool for PiToolAdapter {
fn name(&self) -> &str {
&self.tool.name
}
fn label(&self) -> &str {
&self.tool.name
}
fn description(&self) -> &str {
&self.tool.description
}
fn parameters(&self) -> serde_json::Value {
self.tool.parameters.clone()
}
async fn execute(
&self,
_tool_call_id: &str,
input: serde_json::Value,
_on_update: Option<Box<dyn Fn(pi::tools::ToolUpdate) + Send + Sync>>,
) -> pi::error::Result<pi::tools::ToolOutput> {
use pi::model::{ContentBlock, TextContent};
let (text, is_error) = match (self.tool.handler)(input) {
Ok(out) => (out, false),
Err(msg) => (msg, true),
};
Ok(pi::tools::ToolOutput {
content: vec![ContentBlock::Text(TextContent::new(text))],
details: None,
is_error,
})
}
}
#[cfg(test)]
#[expect(clippy::expect_used, reason = "tests may panic on unexpected fixtures")]
mod tests {
use super::*;
use pi::model::ContentBlock;
use pi::tools::Tool as _;
fn echo_tool(handler: ToolHandler) -> SeherTool {
SeherTool::new(
"echo",
"Echo the input back",
serde_json::json!({
"type": "object",
"properties": { "text": { "type": "string" } },
"required": ["text"],
}),
handler,
)
}
fn text_of(output: &pi::tools::ToolOutput) -> &str {
match output.content.first().expect("one content block") {
ContentBlock::Text(t) => &t.text,
other => panic!("expected text block, got {other:?}"),
}
}
#[test]
fn execute_success_returns_text_not_error() {
let adapter = PiToolAdapter::new(echo_tool(Arc::new(|input| Ok(format!("got: {input}")))));
let out = futures::executor::block_on(adapter.execute(
"call-1",
serde_json::json!({"text": "hi"}),
None,
))
.expect("execute succeeds");
assert!(!out.is_error);
assert_eq!(text_of(&out), r#"got: {"text":"hi"}"#);
}
#[test]
fn execute_error_sets_is_error() {
let adapter = PiToolAdapter::new(echo_tool(Arc::new(|_| Err("boom".to_string()))));
let out =
futures::executor::block_on(adapter.execute("call-1", serde_json::json!({}), None))
.expect("execute still returns Ok");
assert!(out.is_error);
assert_eq!(text_of(&out), "boom");
}
#[test]
fn adapter_exposes_name_label_and_parameters() {
let tool = echo_tool(Arc::new(|_| Ok(String::new())));
let params = tool.parameters.clone();
let adapter = PiToolAdapter::new(tool);
assert_eq!(adapter.name(), "echo");
assert_eq!(adapter.label(), "echo");
assert_eq!(adapter.description(), "Echo the input back");
assert_eq!(adapter.parameters(), params);
assert_eq!(adapter.effects(), pi::tools::ToolEffects::write());
}
#[test]
fn seher_tool_debug_skips_handler() {
let tool = echo_tool(Arc::new(|_| Ok(String::new())));
let dbg = format!("{tool:?}");
assert!(dbg.contains("echo"), "got: {dbg}");
assert!(!dbg.contains("handler"), "got: {dbg}");
}
}