1use async_trait::async_trait;
2use deepstrike_core::types::message::ToolSchema;
3use futures::future::BoxFuture;
4use serde_json::Value;
5use std::collections::HashMap;
6use std::sync::Arc;
7
8use crate::{Error, Result};
9
10#[derive(Debug, Clone, PartialEq)]
11pub enum ToolChunk {
12 Text(String),
13 Progress {
14 progress: f64,
15 message: Option<String>,
16 },
17 Artifact {
18 artifact_id: String,
19 mime_type: Option<String>,
20 label: Option<String>,
21 },
22 JsonPatch(Value),
23 Suspend {
24 suspension_id: String,
25 payload: Option<Value>,
26 },
27}
28
29impl ToolChunk {
30 pub fn text(value: impl Into<String>) -> Self {
31 Self::Text(value.into())
32 }
33 pub fn progress(progress: f64, message: Option<String>) -> Self {
34 Self::Progress { progress, message }
35 }
36 pub fn artifact(
37 artifact_id: impl Into<String>,
38 mime_type: Option<String>,
39 label: Option<String>,
40 ) -> Self {
41 Self::Artifact {
42 artifact_id: artifact_id.into(),
43 mime_type,
44 label,
45 }
46 }
47 pub fn json_patch(patch: Value) -> Self {
48 Self::JsonPatch(patch)
49 }
50 pub fn suspend(suspension_id: impl Into<String>, payload: Option<Value>) -> Self {
51 Self::Suspend {
52 suspension_id: suspension_id.into(),
53 payload,
54 }
55 }
56 pub fn text_projection(&self) -> &str {
57 match self {
58 Self::Text(s) => s.as_str(),
59 _ => "",
60 }
61 }
62}
63
64#[derive(Debug, Clone, PartialEq)]
65pub enum ToolStep {
66 Chunk(ToolChunk),
67 Done(String),
68}
69
70#[async_trait]
71pub trait ToolSession: Send {
72 async fn next(&mut self, resume_input: Option<Value>) -> Result<ToolStep>;
73}
74
75pub struct TextToolSession {
76 output: Option<String>,
77}
78
79impl TextToolSession {
80 pub fn new(output: impl Into<String>) -> Self {
81 Self {
82 output: Some(output.into()),
83 }
84 }
85}
86
87#[async_trait]
88impl ToolSession for TextToolSession {
89 async fn next(&mut self, _resume_input: Option<Value>) -> Result<ToolStep> {
90 Ok(ToolStep::Done(self.output.take().unwrap_or_default()))
91 }
92}
93
94pub type ToolFn =
95 Arc<dyn Fn(Value) -> BoxFuture<'static, Result<Box<dyn ToolSession>>> + Send + Sync>;
96
97pub struct RegisteredTool {
98 pub schema: ToolSchema,
99 pub start: ToolFn,
100}
101
102impl RegisteredTool {
103 pub fn new(
104 name: impl Into<compact_str::CompactString>,
105 description: impl Into<String>,
106 parameters: Value,
107 f: impl Fn(Value) -> BoxFuture<'static, Result<Box<dyn ToolSession>>> + Send + Sync + 'static,
108 ) -> Self {
109 Self {
110 schema: ToolSchema {
111 name: name.into(),
112 description: description.into(),
113 parameters,
114 },
115 start: Arc::new(f),
116 }
117 }
118
119 pub fn text(
120 name: impl Into<compact_str::CompactString>,
121 description: impl Into<String>,
122 parameters: Value,
123 f: impl Fn(Value) -> BoxFuture<'static, Result<String>> + Send + Sync + 'static,
124 ) -> Self {
125 Self::new(name, description, parameters, move |args| {
126 let fut = f(args);
127 Box::pin(async move {
128 Ok(Box::new(TextToolSession::new(fut.await?)) as Box<dyn ToolSession>)
129 })
130 })
131 }
132}
133
134pub fn validate_tool_arguments(
135 schema: &Value,
136 args: &mut Value,
137) -> std::result::Result<bool, String> {
138 let mut repaired = false;
139 validate_value(schema, args, "$", true, &mut repaired)?;
140 Ok(repaired)
141}
142
143fn validate_value(
144 schema: &Value,
145 value: &mut Value,
146 path: &str,
147 is_root: bool,
148 repaired: &mut bool,
149) -> std::result::Result<(), String> {
150 if let Some(union) = schema
152 .get("oneOf")
153 .or_else(|| schema.get("anyOf"))
154 .and_then(Value::as_array)
155 {
156 for sub in union {
157 let mut probe = value.clone();
159 let mut probe_repaired = false;
160 if validate_value(sub, &mut probe, path, is_root, &mut probe_repaired).is_ok() {
161 *value = probe; if probe_repaired {
163 *repaired = true;
164 }
165 return Ok(());
166 }
167 }
168 return Err(format!("{path} does not match any allowed shape"));
169 }
170
171 if let Some(expected) = schema.get("type").and_then(Value::as_str) {
173 match expected {
174 "boolean" => {
175 if let Some(s) = value.as_str() {
176 if s == "true" {
177 *value = Value::Bool(true);
178 *repaired = true;
179 } else if s == "false" {
180 *value = Value::Bool(false);
181 *repaired = true;
182 }
183 }
184 }
185 "number" | "integer" => {
186 if let Some(s) = value.as_str() {
187 if let Ok(num) = s.parse::<f64>() {
188 if expected == "integer" {
189 if num == num.round() {
190 *value = Value::Number(serde_json::Number::from(num as i64));
191 *repaired = true;
192 }
193 } else {
194 if let Some(n) = serde_json::Number::from_f64(num) {
195 *value = Value::Number(n);
196 *repaired = true;
197 }
198 }
199 }
200 }
201 }
202 "array" => {
203 if value.is_object() {
209 let coerced = {
210 let obj = value.as_object().unwrap();
211 let single = if obj.len() == 1 {
212 obj.get("item").or_else(|| obj.get("items"))
213 } else {
214 None
215 };
216 match single {
217 Some(inner) if inner.is_array() => inner.clone(),
218 Some(inner) => Value::Array(vec![inner.clone()]),
219 None => Value::Array(vec![value.clone()]),
220 }
221 };
222 *value = coerced;
223 *repaired = true;
224 }
225 }
226 _ => {}
227 }
228 }
229
230 if let Some(obj) = value.as_object_mut() {
232 if let Some(properties) = schema.get("properties").and_then(Value::as_object) {
233 for (key, child_schema) in properties {
234 if !obj.contains_key(key) {
235 if let Some(default_val) = child_schema.get("default") {
236 obj.insert(key.clone(), default_val.clone());
237 *repaired = true;
238 }
239 }
240 }
241 }
242 }
243
244 if let Some(expected) = schema.get("type").and_then(Value::as_str) {
246 match expected {
247 "object" => {
248 let Some(obj) = value.as_object_mut() else {
249 return Err(format!("{path} must be object"));
250 };
251
252 if let Some(properties) = schema.get("properties").and_then(Value::as_object) {
256 let allowed_keys: std::collections::HashSet<&str> =
257 properties.keys().map(|s| s.as_str()).collect();
258 let additional = schema.get("additionalProperties");
259 let extra_keys: Vec<String> = obj
260 .keys()
261 .filter(|k| !allowed_keys.contains(k.as_str()))
262 .cloned()
263 .collect();
264 for k in extra_keys {
265 match additional {
266 Some(Value::Bool(true)) => {} Some(sub @ Value::Object(_)) => {
268 if let Some(child) = obj.get_mut(&k) {
270 validate_value(
271 sub,
272 child,
273 &format!("{path}.{k}"),
274 false,
275 repaired,
276 )?;
277 }
278 }
279 _ => {
280 obj.remove(&k);
282 *repaired = true;
283 }
284 }
285 }
286 }
287
288 if let Some(required) = schema.get("required").and_then(Value::as_array) {
289 for key in required.iter().filter_map(Value::as_str) {
290 if !obj.contains_key(key) {
291 return Err(format!("{path}.{key} is required"));
292 }
293 }
294 }
295 if let Some(properties) = schema.get("properties").and_then(Value::as_object) {
296 for (key, child_schema) in properties {
297 if let Some(child_value) = obj.get_mut(key) {
298 validate_value(
299 child_schema,
300 child_value,
301 &format!("{path}.{key}"),
302 false,
303 repaired,
304 )?;
305 }
306 }
307 }
308 }
309 "array" => {
310 let Some(arr) = value.as_array_mut() else {
311 return Err(format!("{path} must be array"));
312 };
313 if let Some(items_schema) = schema.get("items") {
314 for (i, child_value) in arr.iter_mut().enumerate() {
315 validate_value(
316 items_schema,
317 child_value,
318 &format!("{path}[{i}]"),
319 false,
320 repaired,
321 )?;
322 }
323 }
324 }
325 "string" if !value.is_string() => return Err(format!("{path} must be string")),
326 "number" if !value.is_number() => return Err(format!("{path} must be number")),
327 "integer" if !value.is_i64() && !value.is_u64() => {
328 return Err(format!("{path} must be integer"));
329 }
330 "boolean" if !value.is_boolean() => return Err(format!("{path} must be boolean")),
331 _ => {}
332 }
333 } else if is_root && !value.is_object() {
334 return Err(format!("{path} must be object"));
335 }
336 if let Some(values) = schema.get("enum").and_then(Value::as_array) {
337 if !values.contains(value) {
338 return Err(format!("{path} must be one of enum values"));
339 }
340 }
341 Ok(())
342}
343
344pub async fn execute_tools(
345 calls: &[deepstrike_core::types::message::ToolCall],
346 registry: &HashMap<String, RegisteredTool>,
347) -> Vec<deepstrike_core::types::message::ToolResult> {
348 let mut results = Vec::new();
349 for call in calls {
350 let Some(tool) = registry.get(call.name.as_str()) else {
351 results.push(tool_result(
352 call.id.clone(),
353 format!("unknown tool: {}", call.name),
354 true,
355 ));
356 continue;
357 };
358 let mut call_args = call.arguments.clone();
359 if let Err(e) = validate_tool_arguments(&tool.schema.parameters, &mut call_args) {
360 results.push(tool_result(
361 call.id.clone(),
362 format!("invalid arguments: {e}"),
363 true,
364 ));
365 continue;
366 }
367 let mut session = match (tool.start)(call_args).await {
368 Ok(session) => session,
369 Err(e) => {
370 results.push(tool_result(call.id.clone(), e.to_string(), true));
371 continue;
372 }
373 };
374 let mut combined = String::new();
375 loop {
376 match session.next(None).await {
377 Ok(ToolStep::Chunk(chunk)) => {
378 if matches!(chunk, ToolChunk::Suspend { .. }) {
379 results.push(tool_result(
380 call.id.clone(),
381 "tool suspended without resume handler".into(),
382 true,
383 ));
384 break;
385 }
386 combined.push_str(chunk.text_projection());
387 }
388 Ok(ToolStep::Done(text)) => {
389 combined.push_str(&text);
390 results.push(tool_result(call.id.clone(), combined, false));
391 break;
392 }
393 Err(e) => {
394 results.push(tool_result(call.id.clone(), e.to_string(), true));
395 break;
396 }
397 }
398 }
399 }
400 results
401}
402
403fn tool_result(
404 call_id: compact_str::CompactString,
405 output: String,
406 is_error: bool,
407) -> deepstrike_core::types::message::ToolResult {
408 deepstrike_core::types::message::ToolResult {
409 call_id,
410 output: deepstrike_core::types::message::Content::Text(output),
411 is_error,
412 is_fatal: false,
413 error_kind: None,
414 token_count: None,
415 }
416}
417
418#[derive(Debug, Clone, serde::Serialize)]
425#[serde(untagged)]
426pub enum ToolEnvelope {
427 Ok(ToolEnvelopeOk),
428 Fail(ToolEnvelopeFail),
429}
430
431#[derive(Debug, Clone, serde::Serialize)]
432pub struct ToolEnvelopeOk {
433 pub success: bool, #[serde(skip_serializing_if = "Option::is_none")]
435 pub data: Option<Value>,
436}
437
438#[derive(Debug, Clone, serde::Serialize)]
439pub struct ToolEnvelopeFail {
440 pub success: bool, pub code: String,
442 pub error: String,
443 #[serde(skip_serializing_if = "Option::is_none")]
444 pub hint: Option<String>,
445}
446
447pub fn ok(data: impl Into<Option<Value>>) -> ToolEnvelope {
448 ToolEnvelope::Ok(ToolEnvelopeOk {
449 success: true,
450 data: data.into(),
451 })
452}
453
454pub fn fail(
455 code: impl Into<String>,
456 error: impl Into<String>,
457 hint: Option<String>,
458) -> ToolEnvelope {
459 ToolEnvelope::Fail(ToolEnvelopeFail {
460 success: false,
461 code: code.into(),
462 error: error.into(),
463 hint,
464 })
465}
466
467pub fn tool_fail(
471 message: impl Into<String>,
472 code: Option<String>,
473 hint: Option<String>,
474) -> crate::Error {
475 crate::Error::ToolFail {
476 output: message.into(),
477 code,
478 hint,
479 is_fatal: false,
480 error_kind: None,
481 }
482}
483
484pub fn safe_tool<F, Fut>(
491 name: impl Into<compact_str::CompactString>,
492 description: impl Into<String>,
493 parameters: Value,
494 f: F,
495) -> RegisteredTool
496where
497 F: Fn(Value) -> Fut + Send + Sync + 'static,
498 Fut: std::future::Future<Output = Result<SafeToolResult>> + Send + 'static,
499{
500 let f = Arc::new(f);
501 RegisteredTool::text(name, description, parameters, move |args| {
502 let f = Arc::clone(&f);
503 Box::pin(async move {
504 let envelope = match f(args).await {
505 Ok(SafeToolResult::Envelope(env)) => env,
506 Ok(SafeToolResult::Data(v)) => ok(Some(v)),
507 Err(e) => {
508 let env = match &e {
509 crate::Error::ToolFail {
510 output, code, hint, ..
511 } => fail(
512 code.clone().unwrap_or_else(|| "internal".to_string()),
513 output.clone(),
514 hint.clone(),
515 ),
516 _ => fail("internal", crate::format_tool_error(&e), None),
517 };
518 env
519 }
520 };
521 Ok(serde_json::to_string(&envelope).unwrap_or_else(|_| String::from(r#"{"success":false,"code":"internal","error":"envelope serialization failed"}"#)))
522 })
523 })
524}
525
526#[derive(Debug, Clone)]
530pub enum SafeToolResult {
531 Envelope(ToolEnvelope),
532 Data(Value),
533}
534
535impl From<ToolEnvelope> for SafeToolResult {
536 fn from(e: ToolEnvelope) -> Self {
537 Self::Envelope(e)
538 }
539}
540impl From<Value> for SafeToolResult {
541 fn from(v: Value) -> Self {
542 Self::Data(v)
543 }
544}
545
546pub fn read_file_tool() -> RegisteredTool {
547 RegisteredTool::text(
548 "read_file",
549 "Read the contents of a file.",
550 serde_json::json!({ "type": "object", "properties": { "path": { "type": "string" } }, "required": ["path"] }),
551 |args| {
552 Box::pin(async move {
553 let path = args["path"]
554 .as_str()
555 .ok_or_else(|| Error::Tool("missing path".into()))?;
556 tokio::fs::read_to_string(path)
557 .await
558 .map_err(|e| Error::Tool(e.to_string()))
559 })
560 },
561 )
562}