rightkit_agent_tools/
spec.rs1use serde::Serialize;
5use serde_json::{json, Value};
6use std::fmt;
7use std::sync::atomic::{AtomicBool, Ordering};
8use std::sync::Arc;
9
10#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
12#[serde(rename_all = "snake_case")]
13pub enum Effect {
14 Observe,
16 Act,
18 Launch,
20 Capture,
22 Network,
24}
25
26impl Effect {
27 pub fn label(self) -> &'static str {
28 match self {
29 Effect::Observe => "observe",
30 Effect::Act => "act",
31 Effect::Launch => "launch",
32 Effect::Capture => "capture",
33 Effect::Network => "network",
34 }
35 }
36}
37
38#[derive(Debug, Clone, Serialize)]
39pub struct ToolSpec {
40 pub name: String,
42 pub description: String,
43 pub input_schema: Value,
45 pub effect: Effect,
46}
47
48impl ToolSpec {
49 pub fn new(
50 name: impl Into<String>,
51 description: impl Into<String>,
52 effect: Effect,
53 input_schema: Value,
54 ) -> Self {
55 Self {
56 name: name.into(),
57 description: description.into(),
58 input_schema,
59 effect,
60 }
61 }
62}
63
64#[derive(Debug, Clone, PartialEq, Serialize)]
66pub struct ToolError {
67 pub code: String,
68 pub message: String,
69 pub retryable: bool,
70}
71
72impl ToolError {
73 pub fn new(code: impl Into<String>, message: impl Into<String>) -> Self {
74 Self {
75 code: code.into(),
76 message: message.into(),
77 retryable: false,
78 }
79 }
80 pub fn retryable(code: impl Into<String>, message: impl Into<String>) -> Self {
81 Self {
82 code: code.into(),
83 message: message.into(),
84 retryable: true,
85 }
86 }
87 pub fn to_value(&self) -> Value {
88 json!({"code": self.code, "message": self.message, "retryable": self.retryable})
89 }
90}
91
92impl fmt::Display for ToolError {
93 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
94 write!(f, "{}: {}", self.code, self.message)
95 }
96}
97impl std::error::Error for ToolError {}
98
99pub const ERR_UNKNOWN_TOOL: &str = "unknown_tool";
100pub const ERR_NOT_EXPOSED: &str = "not_exposed";
101pub const ERR_NOT_ALLOWED: &str = "tool_not_allowed";
102pub const ERR_DENIED: &str = "denied_by_admission";
103pub const ERR_INVALID_ARGS: &str = "invalid_arguments";
104pub const ERR_CANCELLED: &str = "cancelled";
105
106#[derive(Debug, Clone)]
108pub struct Caller {
109 pub app: String,
110}
111
112impl Caller {
113 pub fn new(app: impl Into<String>) -> Self {
114 Self { app: app.into() }
115 }
116}
117
118#[derive(Clone, Default)]
120pub struct CancelToken(Arc<AtomicBool>);
121impl CancelToken {
122 pub fn new() -> Self {
123 Self::default()
124 }
125 pub fn cancel(&self) {
126 self.0.store(true, Ordering::SeqCst);
127 }
128 pub fn is_cancelled(&self) -> bool {
129 self.0.load(Ordering::SeqCst)
130 }
131}
132
133pub struct CallCtx<'a> {
134 pub caller: &'a Caller,
135 pub cancel: &'a CancelToken,
136}
137
138pub trait Tool: Send + Sync {
139 fn spec(&self) -> &ToolSpec;
140 fn subject(&self, _args: &Value) -> Option<String> {
144 None
145 }
146 fn call(&self, ctx: &CallCtx, args: Value) -> Result<Value, ToolError>;
147}
148
149pub fn validate_args(schema: &Value, args: &Value) -> Result<(), String> {
151 let obj = match args {
152 Value::Object(o) => o,
153 Value::Null => return validate_args(schema, &json!({})),
154 _ => return Err("arguments must be a JSON object".into()),
155 };
156 if let Some(req) = schema["required"].as_array() {
157 for r in req.iter().filter_map(Value::as_str) {
158 if !obj.contains_key(r) {
159 return Err(format!("missing required argument `{r}`"));
160 }
161 }
162 }
163 if let Some(props) = schema["properties"].as_object() {
164 for (k, v) in obj {
165 let Some(p) = props.get(k) else { continue };
166 if let Some(t) = p["type"].as_str() {
167 let ok = match t {
168 "string" => v.is_string(),
169 "number" => v.is_number(),
170 "integer" => v.is_i64() || v.is_u64(),
171 "boolean" => v.is_boolean(),
172 "object" => v.is_object(),
173 "array" => v.is_array(),
174 _ => true,
175 };
176 if !ok {
177 return Err(format!("argument `{k}` must be {t}"));
178 }
179 }
180 if let Some(e) = p["enum"].as_array() {
181 if !e.contains(v) {
182 return Err(format!("argument `{k}` is not one of the allowed values"));
183 }
184 }
185 }
186 }
187 Ok(())
188}