Skip to main content

hh_cli/tool/
mod.rs

1pub mod bash;
2pub mod diff;
3pub mod edit;
4pub mod fs;
5pub mod question;
6pub mod registry;
7pub mod schema;
8pub mod skill;
9pub mod task;
10pub mod todo;
11pub mod web;
12
13use async_trait::async_trait;
14use serde::de::DeserializeOwned;
15use serde::{Deserialize, Serialize};
16use serde_json::{Value, json};
17
18pub use schema::ToolSchema;
19
20#[derive(Debug, Clone)]
21pub struct ToolCall {
22    pub name: String,
23    pub arguments: Value,
24}
25
26#[derive(Debug, Clone, Serialize, Deserialize)]
27pub struct ToolResult {
28    pub is_error: bool,
29    pub summary: String,
30    pub content_type: String,
31    pub payload: Value,
32    pub output: String,
33}
34
35impl ToolResult {
36    fn serialization_error(err: serde_json::Error) -> Self {
37        Self::err_text(
38            "serialization_error",
39            format!("failed to serialize output: {err}"),
40        )
41    }
42
43    fn text_result(is_error: bool, summary: impl Into<String>, text: impl Into<String>) -> Self {
44        let output = text.into();
45        Self {
46            is_error,
47            summary: summary.into(),
48            content_type: "text/plain".to_string(),
49            payload: Value::String(output.clone()),
50            output,
51        }
52    }
53
54    fn json_result(
55        is_error: bool,
56        summary: impl Into<String>,
57        content_type: impl Into<String>,
58        payload: Value,
59        fallback: impl Into<String>,
60    ) -> Self {
61        let fallback = fallback.into();
62        let output = serde_json::to_string(&payload).unwrap_or_else(|_| fallback.to_string());
63        Self {
64            is_error,
65            summary: summary.into(),
66            content_type: content_type.into(),
67            payload,
68            output,
69        }
70    }
71
72    pub fn ok_text(summary: impl Into<String>, text: impl Into<String>) -> Self {
73        Self::text_result(false, summary, text)
74    }
75
76    pub fn err_text(summary: impl Into<String>, text: impl Into<String>) -> Self {
77        Self::text_result(true, summary, text)
78    }
79
80    pub fn error(text: impl Into<String>) -> Self {
81        Self::err_text("error", text)
82    }
83
84    pub fn ok_json(summary: impl Into<String>, payload: Value) -> Self {
85        Self::json_result(false, summary, "application/json", payload, "{}")
86    }
87
88    pub fn ok_json_typed(
89        summary: impl Into<String>,
90        content_type: impl Into<String>,
91        payload: Value,
92    ) -> Self {
93        Self::json_result(false, summary, content_type, payload, "{}")
94    }
95
96    pub fn err_json(summary: impl Into<String>, payload: Value) -> Self {
97        Self::json_result(
98            true,
99            summary,
100            "application/json",
101            payload,
102            json!({}).to_string(),
103        )
104    }
105
106    pub fn ok_json_serializable(summary: impl Into<String>, output: &impl Serialize) -> Self {
107        match serde_json::to_value(output) {
108            Ok(value) => Self::ok_json(summary, value),
109            Err(err) => Self::serialization_error(err),
110        }
111    }
112
113    pub fn ok_json_typed_serializable(
114        summary: impl Into<String>,
115        content_type: impl Into<String>,
116        output: &impl Serialize,
117    ) -> Self {
118        match serde_json::to_value(output) {
119            Ok(value) => Self::ok_json_typed(summary, content_type, value),
120            Err(err) => Self::serialization_error(err),
121        }
122    }
123}
124
125pub fn parse_tool_args<T: DeserializeOwned>(args: Value, tool_name: &str) -> Result<T, ToolResult> {
126    serde_json::from_value(args)
127        .map_err(|err| ToolResult::error(format!("invalid {tool_name} args: {err}")))
128}
129
130#[async_trait]
131pub trait Tool: Send + Sync {
132    fn schema(&self) -> ToolSchema;
133    async fn execute(&self, args: Value) -> ToolResult;
134}