Skip to main content

claude_rust_tools/infrastructure/
synthetic_output_tool.rs

1use claude_rust_errors::AppResult;
2use claude_rust_types::{PermissionLevel, Tool};
3use serde_json::{Value, json};
4
5/// Tool that returns content as structured output with optional format.
6pub struct SyntheticOutputTool;
7
8impl SyntheticOutputTool {
9    pub fn new() -> Self {
10        Self
11    }
12}
13
14#[async_trait::async_trait]
15impl Tool for SyntheticOutputTool {
16    fn name(&self) -> &str {
17        "synthetic_output"
18    }
19
20    fn description(&self) -> &str {
21        "Return content as structured output in a specified format (text, json, or markdown)."
22    }
23
24    fn input_schema(&self) -> Value {
25        json!({
26            "type": "object",
27            "properties": {
28                "content": {
29                    "type": "string",
30                    "description": "The content to output"
31                },
32                "format": {
33                    "type": "string",
34                    "description": "Output format: \"text\", \"json\", or \"markdown\"",
35                    "enum": ["text", "json", "markdown"]
36                }
37            },
38            "required": ["content"]
39        })
40    }
41
42    fn permission_level(&self) -> PermissionLevel {
43        PermissionLevel::ReadOnly
44    }
45
46    fn is_read_only(&self, _input: &Value) -> bool { true }
47    fn is_concurrent_safe(&self, _input: &Value) -> bool { true }
48
49    async fn execute(&self, input: Value) -> AppResult<String> {
50        let content = input
51            .get("content")
52            .and_then(|v| v.as_str())
53            .ok_or_else(|| claude_rust_errors::AppError::Tool("missing 'content' field".into()))?;
54
55        let format = input
56            .get("format")
57            .and_then(|v| v.as_str())
58            .unwrap_or("text");
59
60        tracing::info!(format, content_len = content.len(), "synthetic output");
61
62        match format {
63            "json" => {
64                // Validate it's valid JSON, otherwise wrap it
65                if serde_json::from_str::<Value>(content).is_ok() {
66                    Ok(content.to_string())
67                } else {
68                    Ok(json!({ "content": content }).to_string())
69                }
70            }
71            "markdown" | "text" => Ok(content.to_string()),
72            other => Err(claude_rust_errors::AppError::Tool(
73                format!("unsupported format: {other}. Use 'text', 'json', or 'markdown'.")
74            )),
75        }
76    }
77}