smol_workflow_engine/agent_providers/
debug.rs1use 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}