Skip to main content

repl_core/dsl/
reasoning_builtins.rs

1//! Core reasoning builtins for the DSL
2//!
3//! Provides async builtin functions that bridge the DSL with the
4//! reasoning loop infrastructure: `reason`, `llm_call`, `parse_json`,
5//! `delegate`, and `tool_call`.
6
7use crate::dsl::agent_composition::{check_comm_policy, log_comm_message};
8use crate::dsl::evaluator::DslValue;
9use crate::error::{ReplError, Result};
10use std::collections::HashMap;
11use std::sync::Arc;
12use std::time::Duration;
13use symbi_runtime::communication::policy_gate::CommunicationPolicyGate;
14use symbi_runtime::communication::CommunicationBus;
15use symbi_runtime::reasoning::agent_registry::AgentRegistry;
16use symbi_runtime::reasoning::inference::InferenceProvider;
17use symbi_runtime::types::{AgentId, MessageType, RequestId};
18
19/// Shared state for async reasoning builtins.
20#[derive(Clone, Default)]
21pub struct ReasoningBuiltinContext {
22    /// Inference provider for LLM calls.
23    pub provider: Option<Arc<dyn InferenceProvider>>,
24    /// Agent registry for multi-agent composition.
25    pub agent_registry: Option<Arc<AgentRegistry>>,
26    /// The AgentId of the calling agent (for communication tracking).
27    pub sender_agent_id: Option<AgentId>,
28    /// Communication bus for message tracking and audit.
29    pub comm_bus: Option<Arc<dyn CommunicationBus + Send + Sync>>,
30    /// Communication policy gate for inter-agent authorization.
31    pub comm_policy: Option<Arc<CommunicationPolicyGate>>,
32}
33
34/// Execute the `reason` builtin: runs a full reasoning loop.
35///
36/// Arguments (positional or named):
37/// - system: string — system prompt
38/// - user: string — user message
39/// - max_iterations: integer (optional, default 10)
40/// - max_tokens: integer (optional, default 100000)
41///
42/// Returns a map with keys: response, iterations, total_tokens, termination_reason.
43pub async fn builtin_reason(args: &[DslValue], ctx: &ReasoningBuiltinContext) -> Result<DslValue> {
44    let provider = ctx
45        .provider
46        .as_ref()
47        .ok_or_else(|| ReplError::Execution("No inference provider configured".into()))?;
48
49    let (system, user, max_iterations, max_tokens) = parse_reason_args(args)?;
50
51    use symbi_runtime::reasoning::circuit_breaker::CircuitBreakerRegistry;
52    use symbi_runtime::reasoning::context_manager::DefaultContextManager;
53    use symbi_runtime::reasoning::conversation::{Conversation, ConversationMessage};
54    use symbi_runtime::reasoning::executor::DefaultActionExecutor;
55    use symbi_runtime::reasoning::loop_types::{BufferedJournal, LoopConfig};
56    use symbi_runtime::reasoning::policy_bridge::DefaultPolicyGate;
57    use symbi_runtime::reasoning::reasoning_loop::ReasoningLoopRunner;
58
59    let runner = ReasoningLoopRunner {
60        provider: Arc::clone(provider),
61        policy_gate: Arc::new(DefaultPolicyGate::permissive()),
62        executor: Arc::new(DefaultActionExecutor::default()),
63        context_manager: Arc::new(DefaultContextManager::default()),
64        circuit_breakers: Arc::new(CircuitBreakerRegistry::default()),
65        journal: Arc::new(BufferedJournal::new(1000)),
66        knowledge_bridge: None,
67    };
68
69    let mut conv = Conversation::with_system(&system);
70    conv.push(ConversationMessage::user(&user));
71
72    let config = LoopConfig {
73        max_iterations,
74        max_total_tokens: max_tokens,
75        ..Default::default()
76    };
77
78    let result = runner.run(AgentId::new(), conv, config).await;
79
80    let mut map = HashMap::new();
81    map.insert("response".to_string(), DslValue::String(result.output));
82    map.insert(
83        "iterations".to_string(),
84        DslValue::Integer(result.iterations as i64),
85    );
86    map.insert(
87        "total_tokens".to_string(),
88        DslValue::Integer(result.total_usage.total_tokens as i64),
89    );
90    map.insert(
91        "termination_reason".to_string(),
92        DslValue::String(format!("{:?}", result.termination_reason)),
93    );
94
95    Ok(DslValue::Map(map))
96}
97
98/// Execute the `llm_call` builtin: one-shot LLM call.
99///
100/// Arguments:
101/// - prompt: string — the prompt to send
102/// - model: string (optional) — model override
103/// - temperature: number (optional)
104/// - max_tokens: integer (optional)
105///
106/// Returns a string.
107pub async fn builtin_llm_call(
108    args: &[DslValue],
109    ctx: &ReasoningBuiltinContext,
110) -> Result<DslValue> {
111    let provider = ctx
112        .provider
113        .as_ref()
114        .ok_or_else(|| ReplError::Execution("No inference provider configured".into()))?;
115
116    let prompt = match args.first() {
117        Some(DslValue::String(s)) => s.clone(),
118        Some(DslValue::Map(map)) => map
119            .get("prompt")
120            .and_then(|v| match v {
121                DslValue::String(s) => Some(s.clone()),
122                _ => None,
123            })
124            .ok_or_else(|| ReplError::Execution("llm_call requires 'prompt' argument".into()))?,
125        _ => {
126            return Err(ReplError::Execution(
127                "llm_call requires a string prompt".into(),
128            ))
129        }
130    };
131
132    use symbi_runtime::reasoning::conversation::{Conversation, ConversationMessage};
133    use symbi_runtime::reasoning::inference::InferenceOptions;
134
135    let mut conv = Conversation::new();
136    conv.push(ConversationMessage::user(&prompt));
137
138    let options = InferenceOptions::default();
139    let response = provider
140        .complete(&conv, &options)
141        .await
142        .map_err(|e| ReplError::Execution(format!("LLM call failed: {}", e)))?;
143
144    Ok(DslValue::String(response.content))
145}
146
147/// Execute the `parse_json` builtin: parse a string as JSON.
148///
149/// Arguments:
150/// - text: string — the JSON text to parse
151///
152/// Returns a DslValue (Map, List, String, Number, Boolean, or Null).
153pub fn builtin_parse_json(args: &[DslValue]) -> Result<DslValue> {
154    let text = match args.first() {
155        Some(DslValue::String(s)) => s,
156        _ => {
157            return Err(ReplError::Execution(
158                "parse_json requires a string argument".into(),
159            ))
160        }
161    };
162
163    let value: serde_json::Value = serde_json::from_str(text)
164        .map_err(|e| ReplError::Execution(format!("JSON parse error: {}", e)))?;
165
166    Ok(json_to_dsl_value(&value))
167}
168
169/// Execute the `tool_call` builtin: explicit tool invocation.
170///
171/// Arguments:
172/// - name: string — tool name
173/// - args: map — tool arguments
174///
175/// Returns the tool result as a string.
176pub async fn builtin_tool_call(
177    args: &[DslValue],
178    _ctx: &ReasoningBuiltinContext,
179) -> Result<DslValue> {
180    let (name, arguments) = match args {
181        [DslValue::String(name), DslValue::Map(args_map)] => {
182            let json_args: serde_json::Map<String, serde_json::Value> = args_map
183                .iter()
184                .map(|(k, v)| (k.clone(), v.to_json()))
185                .collect();
186            (
187                name.clone(),
188                serde_json::Value::Object(json_args).to_string(),
189            )
190        }
191        [DslValue::String(name), DslValue::String(args_str)] => (name.clone(), args_str.clone()),
192        [DslValue::String(name)] => (name.clone(), "{}".to_string()),
193        _ => {
194            return Err(ReplError::Execution(
195                "tool_call requires (name: string, args?: map|string)".into(),
196            ))
197        }
198    };
199
200    // In a full setup, this would go through ToolInvocationEnforcer.
201    // For now, return a structured result indicating the tool call was made.
202    let mut result = HashMap::new();
203    result.insert("tool".to_string(), DslValue::String(name));
204    result.insert("arguments".to_string(), DslValue::String(arguments));
205    result.insert(
206        "status".to_string(),
207        DslValue::String("executed".to_string()),
208    );
209
210    Ok(DslValue::Map(result))
211}
212
213/// Execute the `delegate` builtin: send a message to another agent.
214///
215/// Arguments:
216/// - agent: string — agent name
217/// - message: string — message to send
218/// - timeout: duration (optional)
219///
220/// Returns the agent's response as a string.
221pub async fn builtin_delegate(
222    args: &[DslValue],
223    ctx: &ReasoningBuiltinContext,
224) -> Result<DslValue> {
225    let (agent_name, message) = match args {
226        [DslValue::String(agent), DslValue::String(msg)] => (agent.clone(), msg.clone()),
227        [DslValue::Map(map)] => {
228            let agent = map
229                .get("agent")
230                .and_then(|v| match v {
231                    DslValue::String(s) => Some(s.clone()),
232                    _ => None,
233                })
234                .ok_or_else(|| ReplError::Execution("delegate requires 'agent' argument".into()))?;
235            let msg = map
236                .get("message")
237                .and_then(|v| match v {
238                    DslValue::String(s) => Some(s.clone()),
239                    _ => None,
240                })
241                .ok_or_else(|| {
242                    ReplError::Execution("delegate requires 'message' argument".into())
243                })?;
244            (agent, msg)
245        }
246        _ => {
247            return Err(ReplError::Execution(
248                "delegate requires (agent: string, message: string)".into(),
249            ))
250        }
251    };
252
253    // Communication bus wiring: resolve recipient (fallback for unregistered agents)
254    let recipient_id = if let Some(registry) = &ctx.agent_registry {
255        registry
256            .get_agent(&agent_name)
257            .await
258            .map(|a| a.agent_id)
259            .unwrap_or_default()
260    } else {
261        AgentId::new()
262    };
263    let sender_id = ctx.sender_agent_id.unwrap_or_default();
264    let request_id = RequestId::new();
265
266    check_comm_policy(
267        ctx,
268        sender_id,
269        recipient_id,
270        MessageType::Request(request_id),
271    )?;
272    log_comm_message(
273        ctx,
274        sender_id,
275        recipient_id,
276        &message,
277        MessageType::Request(request_id),
278        Duration::from_secs(30),
279    )
280    .await;
281
282    // Use inference provider to simulate delegation (each agent is a separate conversation)
283    let provider = ctx
284        .provider
285        .as_ref()
286        .ok_or_else(|| ReplError::Execution("No inference provider configured".into()))?;
287
288    use symbi_runtime::reasoning::conversation::{Conversation, ConversationMessage};
289    use symbi_runtime::reasoning::inference::InferenceOptions;
290
291    let mut conv = Conversation::with_system(format!(
292        "You are agent '{}'. Respond to the delegated task.",
293        agent_name
294    ));
295    conv.push(ConversationMessage::user(&message));
296
297    let response = provider
298        .complete(&conv, &InferenceOptions::default())
299        .await
300        .map_err(|e| {
301            ReplError::Execution(format!("Delegation to '{}' failed: {}", agent_name, e))
302        })?;
303
304    log_comm_message(
305        ctx,
306        recipient_id,
307        sender_id,
308        &response.content,
309        MessageType::Response(request_id),
310        Duration::from_secs(30),
311    )
312    .await;
313
314    Ok(DslValue::String(response.content))
315}
316
317// --- Helper functions ---
318
319fn parse_reason_args(args: &[DslValue]) -> Result<(String, String, u32, u32)> {
320    match args {
321        // Named arguments via map
322        [DslValue::Map(map)] => {
323            let system = map
324                .get("system")
325                .and_then(|v| match v {
326                    DslValue::String(s) => Some(s.clone()),
327                    _ => None,
328                })
329                .ok_or_else(|| ReplError::Execution("reason requires 'system' argument".into()))?;
330            let user = map
331                .get("user")
332                .and_then(|v| match v {
333                    DslValue::String(s) => Some(s.clone()),
334                    _ => None,
335                })
336                .ok_or_else(|| ReplError::Execution("reason requires 'user' argument".into()))?;
337            let max_iterations = map
338                .get("max_iterations")
339                .and_then(|v| match v {
340                    DslValue::Integer(i) => Some(*i as u32),
341                    DslValue::Number(n) => Some(*n as u32),
342                    _ => None,
343                })
344                .unwrap_or(10);
345            let max_tokens = map
346                .get("max_tokens")
347                .and_then(|v| match v {
348                    DslValue::Integer(i) => Some(*i as u32),
349                    DslValue::Number(n) => Some(*n as u32),
350                    _ => None,
351                })
352                .unwrap_or(100_000);
353            Ok((system, user, max_iterations, max_tokens))
354        }
355        // Positional: system, user
356        [DslValue::String(system), DslValue::String(user)] => {
357            Ok((system.clone(), user.clone(), 10, 100_000))
358        }
359        // Positional: system, user, max_iterations
360        [DslValue::String(system), DslValue::String(user), DslValue::Integer(max_iter)] => {
361            Ok((system.clone(), user.clone(), *max_iter as u32, 100_000))
362        }
363        _ => Err(ReplError::Execution(
364            "reason requires (system: string, user: string, [max_iterations?, max_tokens?])".into(),
365        )),
366    }
367}
368
369/// Convert a serde_json::Value to a DslValue.
370pub fn json_to_dsl_value(value: &serde_json::Value) -> DslValue {
371    match value {
372        serde_json::Value::Null => DslValue::Null,
373        serde_json::Value::Bool(b) => DslValue::Boolean(*b),
374        serde_json::Value::Number(n) => {
375            if let Some(i) = n.as_i64() {
376                DslValue::Integer(i)
377            } else if let Some(f) = n.as_f64() {
378                DslValue::Number(f)
379            } else {
380                DslValue::Number(0.0)
381            }
382        }
383        serde_json::Value::String(s) => DslValue::String(s.clone()),
384        serde_json::Value::Array(arr) => {
385            DslValue::List(arr.iter().map(json_to_dsl_value).collect())
386        }
387        serde_json::Value::Object(obj) => {
388            let map: HashMap<String, DslValue> = obj
389                .iter()
390                .map(|(k, v)| (k.clone(), json_to_dsl_value(v)))
391                .collect();
392            DslValue::Map(map)
393        }
394    }
395}
396
397#[cfg(test)]
398mod tests {
399    use super::*;
400
401    #[test]
402    fn test_parse_json_valid() {
403        let result =
404            builtin_parse_json(&[DslValue::String(r#"{"key": "value", "num": 42}"#.into())])
405                .unwrap();
406        match result {
407            DslValue::Map(map) => {
408                assert_eq!(map.get("key"), Some(&DslValue::String("value".into())));
409                assert_eq!(map.get("num"), Some(&DslValue::Integer(42)));
410            }
411            _ => panic!("Expected Map"),
412        }
413    }
414
415    #[test]
416    fn test_parse_json_array() {
417        let result = builtin_parse_json(&[DslValue::String("[1, 2, 3]".into())]).unwrap();
418        match result {
419            DslValue::List(items) => {
420                assert_eq!(items.len(), 3);
421                assert_eq!(items[0], DslValue::Integer(1));
422            }
423            _ => panic!("Expected List"),
424        }
425    }
426
427    #[test]
428    fn test_parse_json_invalid() {
429        let result = builtin_parse_json(&[DslValue::String("not json".into())]);
430        assert!(result.is_err());
431    }
432
433    #[test]
434    fn test_parse_json_nested() {
435        let json = r#"{"tasks": [{"id": 1, "done": false}], "count": 1}"#;
436        let result = builtin_parse_json(&[DslValue::String(json.into())]).unwrap();
437        match result {
438            DslValue::Map(map) => match map.get("tasks") {
439                Some(DslValue::List(tasks)) => {
440                    assert_eq!(tasks.len(), 1);
441                    match &tasks[0] {
442                        DslValue::Map(task) => {
443                            assert_eq!(task.get("id"), Some(&DslValue::Integer(1)));
444                            assert_eq!(task.get("done"), Some(&DslValue::Boolean(false)));
445                        }
446                        _ => panic!("Expected Map in list"),
447                    }
448                }
449                _ => panic!("Expected List for tasks"),
450            },
451            _ => panic!("Expected Map"),
452        }
453    }
454
455    #[test]
456    fn test_json_to_dsl_value_all_types() {
457        let json = serde_json::json!({
458            "str": "hello",
459            "int": 42,
460            "float": 3.14,
461            "bool": true,
462            "null": null,
463            "arr": [1, 2],
464            "obj": {"nested": "value"}
465        });
466
467        let dsl = json_to_dsl_value(&json);
468        match dsl {
469            DslValue::Map(map) => {
470                assert_eq!(map.get("str"), Some(&DslValue::String("hello".into())));
471                assert_eq!(map.get("int"), Some(&DslValue::Integer(42)));
472                assert_eq!(map.get("bool"), Some(&DslValue::Boolean(true)));
473                assert_eq!(map.get("null"), Some(&DslValue::Null));
474            }
475            _ => panic!("Expected Map"),
476        }
477    }
478
479    #[test]
480    fn test_parse_reason_args_positional() {
481        let args = vec![
482            DslValue::String("system prompt".into()),
483            DslValue::String("user message".into()),
484        ];
485        let (system, user, max_iter, max_tokens) = parse_reason_args(&args).unwrap();
486        assert_eq!(system, "system prompt");
487        assert_eq!(user, "user message");
488        assert_eq!(max_iter, 10);
489        assert_eq!(max_tokens, 100_000);
490    }
491
492    #[test]
493    fn test_parse_reason_args_named() {
494        let mut map = HashMap::new();
495        map.insert("system".into(), DslValue::String("sys".into()));
496        map.insert("user".into(), DslValue::String("usr".into()));
497        map.insert("max_iterations".into(), DslValue::Integer(5));
498
499        let args = vec![DslValue::Map(map)];
500        let (system, user, max_iter, max_tokens) = parse_reason_args(&args).unwrap();
501        assert_eq!(system, "sys");
502        assert_eq!(user, "usr");
503        assert_eq!(max_iter, 5);
504        assert_eq!(max_tokens, 100_000);
505    }
506
507    #[test]
508    fn test_parse_reason_args_missing_required() {
509        let mut map = HashMap::new();
510        map.insert("system".into(), DslValue::String("sys".into()));
511        // Missing "user"
512
513        let args = vec![DslValue::Map(map)];
514        assert!(parse_reason_args(&args).is_err());
515    }
516}