Skip to main content

smol_workflow_engine/agent_providers/
debug.rs

1use super::types::{
2    AgentProvider, AgentProviderResult, AgentProviderRunInput, AgentProviderSchemaMode,
3    AgentProviderUsageMode, AgentUsage, AgentUsageCost,
4};
5use serde_json::{Map, Value};
6
7#[derive(Debug, Default, Clone, Copy)]
8pub struct DebugAgentProvider;
9
10impl DebugAgentProvider {
11    pub fn new() -> Self {
12        Self
13    }
14}
15
16#[async_trait::async_trait]
17impl AgentProvider for DebugAgentProvider {
18    fn name(&self) -> &str {
19        "debug"
20    }
21
22    fn schema_mode(&self) -> AgentProviderSchemaMode {
23        AgentProviderSchemaMode::Builtin
24    }
25
26    fn usage_mode(&self) -> AgentProviderUsageMode {
27        AgentProviderUsageMode::Builtin
28    }
29
30    async fn run(&self, input: AgentProviderRunInput) -> anyhow::Result<AgentProviderResult> {
31        log::debug!(
32            "running debug provider phase={:?} prompt_len={} schema={}",
33            input.context.phase.as_deref(),
34            input.prompt.len(),
35            input
36                .options
37                .as_ref()
38                .and_then(|options| options.get("schema"))
39                .is_some()
40        );
41        let output = input
42            .options
43            .as_ref()
44            .and_then(|options| options.get("schema"))
45            .map(generate_debug_value_from_schema)
46            .unwrap_or_else(|| Value::String(format!("echo: {}", input.prompt)));
47        let input_tokens = estimate_tokens(&input.prompt);
48        let output_tokens = estimate_tokens(&serde_json::to_string(&output)?);
49
50        Ok(AgentProviderResult {
51            output: output.clone(),
52            session_id: None,
53            model: super::common::option_model(&input.options),
54            usage: Some(AgentUsage {
55                input_tokens: Some(input_tokens),
56                output_tokens: Some(output_tokens),
57                total_tokens: Some(input_tokens + output_tokens),
58                cost: Some(AgentUsageCost {
59                    input: Some(0.0),
60                    output: Some(0.0),
61                    total: Some(0.0),
62                    currency: Some("USD".to_string()),
63                    ..AgentUsageCost::default()
64                }),
65                ..AgentUsage::default()
66            }),
67            isolation: None,
68            raw: None,
69        })
70    }
71}
72
73pub fn generate_debug_value_from_schema(schema: &Value) -> Value {
74    match schema {
75        Value::Bool(true) => Value::String("debug".to_string()),
76        Value::Bool(false) => Value::Null,
77        Value::Object(object) => generate_debug_value_from_schema_object(object),
78        _ => Value::String("debug".to_string()),
79    }
80}
81
82fn generate_debug_value_from_schema_object(schema: &Map<String, Value>) -> Value {
83    if let Some(value) = schema.get("const") {
84        return value.clone();
85    }
86
87    if let Some(value) = schema
88        .get("enum")
89        .and_then(Value::as_array)
90        .and_then(|values| values.first())
91    {
92        return value.clone();
93    }
94
95    for key in ["oneOf", "anyOf"] {
96        if let Some(value) = schema
97            .get(key)
98            .and_then(Value::as_array)
99            .and_then(|values| values.first())
100        {
101            return generate_debug_value_from_schema(value);
102        }
103    }
104
105    if let Some(all_of) = schema.get("allOf").and_then(Value::as_array) {
106        return merge_all_of(all_of);
107    }
108
109    match first_schema_type(schema.get("type")).unwrap_or_else(|| infer_schema_type(schema)) {
110        "null" => Value::Null,
111        "boolean" => Value::Bool(true),
112        "integer" => debug_number(schema, true),
113        "number" => debug_number(schema, false),
114        "string" => Value::String(debug_string(schema)),
115        "array" => debug_array(schema),
116        "object" => debug_object(schema),
117        _ => debug_object(schema),
118    }
119}
120
121fn merge_all_of(schemas: &[Value]) -> Value {
122    let values = schemas
123        .iter()
124        .map(generate_debug_value_from_schema)
125        .collect::<Vec<_>>();
126
127    if values.iter().all(Value::is_object) {
128        let mut merged = Map::new();
129        for value in values {
130            if let Value::Object(object) = value {
131                merged.extend(object);
132            }
133        }
134        Value::Object(merged)
135    } else {
136        values.last().cloned().unwrap_or(Value::Null)
137    }
138}
139
140fn first_schema_type(value: Option<&Value>) -> Option<&str> {
141    match value {
142        Some(Value::String(value)) => Some(value.as_str()),
143        Some(Value::Array(values)) => values.first().and_then(Value::as_str),
144        _ => None,
145    }
146}
147
148fn infer_schema_type(schema: &Map<String, Value>) -> &'static str {
149    if schema.contains_key("properties")
150        || schema.contains_key("required")
151        || schema.contains_key("additionalProperties")
152    {
153        "object"
154    } else if schema.contains_key("items") || schema.contains_key("prefixItems") {
155        "array"
156    } else if schema.contains_key("minimum")
157        || schema.contains_key("maximum")
158        || schema.contains_key("multipleOf")
159    {
160        "number"
161    } else if schema.contains_key("minLength")
162        || schema.contains_key("maxLength")
163        || schema.contains_key("pattern")
164        || schema.contains_key("format")
165    {
166        "string"
167    } else {
168        "object"
169    }
170}
171
172fn debug_number(schema: &Map<String, Value>, integer: bool) -> Value {
173    let mut value = schema.get("minimum").and_then(Value::as_f64).unwrap_or(0.0);
174    if let Some(exclusive_minimum) = schema.get("exclusiveMinimum").and_then(Value::as_f64) {
175        value = value.max(exclusive_minimum + if integer { 1.0 } else { f64::EPSILON });
176    }
177    if integer || value.fract() == 0.0 {
178        Value::Number((value.ceil() as i64).into())
179    } else {
180        serde_json::Number::from_f64(value)
181            .map(Value::Number)
182            .unwrap_or_else(|| Value::Number(0.into()))
183    }
184}
185
186fn debug_string(schema: &Map<String, Value>) -> String {
187    match schema.get("format").and_then(Value::as_str) {
188        Some("email") => "debug@example.com",
189        Some("uri" | "url") => "https://example.com/debug",
190        Some("date-time") => "2000-01-01T00:00:00.000Z",
191        Some("date") => "2000-01-01",
192        _ => "debug-string",
193    }
194    .to_string()
195}
196
197fn debug_array(schema: &Map<String, Value>) -> Value {
198    if let Some(prefix_items) = schema.get("prefixItems").and_then(Value::as_array) {
199        if !prefix_items.is_empty() {
200            return Value::Array(
201                prefix_items
202                    .iter()
203                    .map(generate_debug_value_from_schema)
204                    .collect(),
205            );
206        }
207    }
208
209    match schema.get("items") {
210        Some(Value::Object(_)) | Some(Value::Bool(_)) => {
211            Value::Array(vec![generate_debug_value_from_schema(&schema["items"])])
212        }
213        _ => Value::Array(vec![]),
214    }
215}
216
217fn debug_object(schema: &Map<String, Value>) -> Value {
218    let properties = schema
219        .get("properties")
220        .and_then(Value::as_object)
221        .cloned()
222        .unwrap_or_default();
223    let mut keys = properties.keys().cloned().collect::<Vec<_>>();
224    if let Some(required) = schema.get("required").and_then(Value::as_array) {
225        for key in required.iter().filter_map(Value::as_str) {
226            if !keys.iter().any(|existing| existing == key) {
227                keys.push(key.to_string());
228            }
229        }
230    }
231
232    let mut output = Map::new();
233    for key in keys {
234        let value = properties.get(&key).unwrap_or(&Value::Bool(true));
235        output.insert(key, generate_debug_value_from_schema(value));
236    }
237    Value::Object(output)
238}
239
240fn estimate_tokens(text: &str) -> u64 {
241    std::cmp::max(1, text.len().div_ceil(4) as u64)
242}