1use async_trait::async_trait;
12use std::path::PathBuf;
13use tracing::info;
14
15use super::chat_template::{format_prompt_with_template, ChatTemplate};
16use super::validate::validate_model_file;
17use super::{CompletionRequest, CompletionResponse, LlmDriver, ToolCall};
18use crate::agent::result::{AgentError, DriverError, StopReason, TokenUsage};
19use crate::serve::backends::PrivacyTier;
20
21pub struct RealizarDriver {
23 model_path: PathBuf,
25 context_window_size: usize,
27 template: ChatTemplate,
29}
30
31impl RealizarDriver {
32 pub fn new(model_path: PathBuf, context_window: Option<usize>) -> Result<Self, AgentError> {
44 if !model_path.exists() {
45 return Err(AgentError::Driver(DriverError::InferenceFailed(format!(
46 "model not found: {}",
47 model_path.display()
48 ))));
49 }
50
51 validate_model_file(&model_path)?;
53
54 let context_window_size = context_window.unwrap_or(4096);
55 let template = ChatTemplate::from_model_path(&model_path);
56 Ok(Self { model_path, context_window_size, template })
57 }
58}
59
60#[async_trait]
61impl LlmDriver for RealizarDriver {
62 async fn complete(&self, request: CompletionRequest) -> Result<CompletionResponse, AgentError> {
63 let prompt = format_prompt_with_template(&request, self.template);
65
66 let config = realizar::infer::InferenceConfig {
68 model_path: self.model_path.clone(),
69 prompt: Some(prompt),
70 input_tokens: None,
71 max_tokens: request.max_tokens as usize,
72 temperature: request.temperature,
73 top_k: 0,
74 top_p: None,
77 seed: 42,
78 repeat_penalty: 1.0,
79 repeat_last_n: 64,
80 no_gpu: self.model_path.extension().is_some_and(|e| e == "apr"),
83 trace: false,
84 trace_verbose: false,
85 trace_output: None,
86 trace_steps: None,
87 verbose: false,
88 use_mock_backend: false,
89 stop_tokens: vec![],
90 };
91
92 let result = tokio::task::spawn_blocking(move || realizar::infer::run_inference(&config))
94 .await
95 .map_err(|e| {
96 AgentError::Driver(DriverError::InferenceFailed(format!("spawn_blocking: {e}")))
97 })?
98 .map_err(|e| AgentError::Driver(DriverError::InferenceFailed(e.to_string())))?;
99
100 let (raw_text, tool_calls) = parse_tool_calls(&result.text);
102
103 let text = sanitize_output(&raw_text, request.system.as_deref());
105
106 let stop_reason =
107 if tool_calls.is_empty() { StopReason::EndTurn } else { StopReason::ToolUse };
108
109 Ok(CompletionResponse {
110 text,
111 stop_reason,
112 tool_calls,
113 usage: TokenUsage {
114 input_tokens: result.input_token_count as u64,
115 output_tokens: result.generated_token_count as u64,
116 },
117 })
118 }
119
120 fn context_window(&self) -> usize {
121 self.context_window_size
122 }
123
124 fn privacy_tier(&self) -> PrivacyTier {
125 PrivacyTier::Sovereign
126 }
127}
128
129pub fn parse_tool_calls_pub(text: &str) -> (String, Vec<ToolCall>) {
148 parse_tool_calls(text)
149}
150
151fn parse_tool_calls(text: &str) -> (String, Vec<ToolCall>) {
152 let (remaining, calls) = parse_tool_calls_envelope(text);
153 if !calls.is_empty() {
154 return (remaining, calls);
155 }
156 let (salvaged_remaining, salvaged) = salvage_tool_calls(&remaining);
159 if !salvaged.is_empty() {
160 info!(count = salvaged.len(), "salvaged tool call(s) from non-envelope output (CCPA-m296)");
161 return (salvaged_remaining, salvaged);
162 }
163 (remaining, calls)
164}
165
166fn parse_tool_calls_envelope(text: &str) -> (String, Vec<ToolCall>) {
167 let mut tool_calls = Vec::new();
168 let mut remaining = String::new();
169 let mut call_counter = 0u32;
170
171 let mut cursor = text;
172 loop {
173 let xml_pos = cursor.find("<tool_call>");
175 let md_pos = cursor.find("```json");
176
177 let (start, tag_len, is_markdown) = match (xml_pos, md_pos) {
178 (Some(x), Some(m)) if x <= m => (x, "<tool_call>".len(), false),
179 (Some(x), None) => (x, "<tool_call>".len(), false),
180 (_, Some(m)) => (m, "```json".len(), true),
181 (None, None) => {
182 remaining.push_str(cursor);
183 break;
184 }
185 };
186
187 remaining.push_str(&cursor[..start]);
188 let after_tag = &cursor[start + tag_len..];
189
190 let (json_str, advance_past) = if is_markdown {
192 if let Some(end) = after_tag.find("```") {
194 (&after_tag[..end], &after_tag[end + "```".len()..])
195 } else {
196 (after_tag, "")
197 }
198 } else if let Some(end) = after_tag.find("</tool_call>") {
199 (&after_tag[..end], &after_tag[end + "</tool_call>".len()..])
200 } else {
201 (after_tag, "")
203 };
204 let json_str = json_str.trim();
205
206 if let Ok(parsed) = serde_json::from_str::<serde_json::Value>(json_str) {
207 if let Some(name) = parsed.get("name").and_then(|n| n.as_str()) {
209 let name = name.to_string();
210 let input = parsed.get("input").cloned().unwrap_or(serde_json::json!({}));
211 call_counter += 1;
212 tool_calls.push(ToolCall { id: format!("local-{call_counter}"), name, input });
213 } else {
214 remaining.push_str(&cursor[start..]);
215 break;
216 }
217 } else {
218 remaining.push_str(&cursor[start..]);
219 break;
220 }
221
222 cursor = advance_past;
223 if cursor.is_empty() {
224 break;
225 }
226 }
227
228 (remaining.trim().to_string(), tool_calls)
229}
230
231fn salvage_tool_calls(text: &str) -> (String, Vec<ToolCall>) {
251 if let Some((before, inner, after)) = extract_first_fenced_block(text) {
253 if let Some(call) = tool_call_from_json_str(inner.trim(), 1) {
254 let remaining = format!("{before}{after}");
255 return (remaining.trim().to_string(), vec![call]);
256 }
257 }
258
259 if let Some((start, end)) = find_balanced_json_object(text) {
261 if let Some(call) = tool_call_from_json_str(text[start..end].trim(), 1) {
262 let remaining = format!("{}{}", &text[..start], &text[end..]);
263 return (remaining.trim().to_string(), vec![call]);
264 }
265 }
266
267 (text.trim().to_string(), Vec::new())
268}
269
270fn tool_call_from_json_str(json_str: &str, idx: u32) -> Option<ToolCall> {
274 let parsed = serde_json::from_str::<serde_json::Value>(json_str).ok()?;
275 let obj = parsed.as_object()?;
276 let name = obj.get("name")?.as_str()?.to_string();
280 if name.is_empty() {
281 return None;
282 }
283 let input = obj.get("input")?.clone();
284 Some(ToolCall { id: format!("salvage-{idx}"), name, input })
285}
286
287fn extract_first_fenced_block(text: &str) -> Option<(&str, &str, &str)> {
291 let open = text.find("```")?;
292 let before = &text[..open];
293 let rest = &text[open + 3..];
294 let inner_start = rest.find('\n').map(|i| i + 1)?;
296 let body = &rest[inner_start..];
297 let close = body.find("```")?;
298 let inner = &body[..close];
299 let after = &body[close + 3..];
300 Some((before, inner, after))
301}
302
303fn find_balanced_json_object(text: &str) -> Option<(usize, usize)> {
308 let bytes = text.as_bytes();
309 let start = text.find('{')?;
310 let mut depth = 0i32;
311 let mut in_str = false;
312 let mut escaped = false;
313 let mut i = start;
314 while i < bytes.len() {
315 let c = bytes[i];
316 if in_str {
317 if escaped {
318 escaped = false;
319 } else if c == b'\\' {
320 escaped = true;
321 } else if c == b'"' {
322 in_str = false;
323 }
324 } else {
325 match c {
326 b'"' => in_str = true,
327 b'{' => depth += 1,
328 b'}' => {
329 depth -= 1;
330 if depth == 0 {
331 return Some((start, i + 1));
332 }
333 }
334 _ => {}
335 }
336 }
337 i += 1;
338 }
339 None
340}
341
342fn sanitize_output(text: &str, system_prompt: Option<&str>) -> String {
348 let mut cleaned = text.to_string();
349
350 if let Some(sys) = system_prompt {
352 let sys_prefix = &sys[..sys.len().min(80)];
354 if cleaned.starts_with(sys_prefix) {
355 cleaned = cleaned[sys.len().min(cleaned.len())..].to_string();
357 }
358 }
359
360 for marker in &[
362 "<|im_start|>",
363 "<|im_end|>",
364 "<|start_header_id|>",
365 "<|end_header_id|>",
366 "<|eot_id|>",
367 "<|system|>",
368 "<|user|>",
369 "<|assistant|>",
370 "<|end|>",
371 ] {
372 cleaned = cleaned.replace(marker, "");
373 }
374
375 let cleaned = cleaned.trim();
377 let cleaned = cleaned.strip_prefix("system\n").unwrap_or(cleaned);
378 let cleaned = cleaned.strip_prefix("assistant\n").unwrap_or(cleaned);
379 cleaned.trim().to_string()
380}
381
382#[cfg(test)]
383mod tests {
384 use super::*;
385
386 #[test]
387 fn test_parse_no_tool_calls() {
388 let (text, calls) = parse_tool_calls("Hello world");
389 assert_eq!(text, "Hello world");
390 assert!(calls.is_empty());
391 }
392
393 #[test]
394 fn test_parse_single_tool_call() {
395 let input = r#"Before text
396<tool_call>
397{"name": "rag", "input": {"query": "SIMD"}}
398</tool_call>
399After text"#;
400 let (text, calls) = parse_tool_calls(input);
401 assert_eq!(text, "Before text\n\nAfter text");
402 assert_eq!(calls.len(), 1);
403 assert_eq!(calls[0].name, "rag");
404 assert_eq!(calls[0].id, "local-1");
405 assert_eq!(calls[0].input, serde_json::json!({"query": "SIMD"}));
406 }
407
408 #[test]
409 fn test_parse_multiple_tool_calls() {
410 let input = r#"<tool_call>
411{"name": "rag", "input": {"query": "a"}}
412</tool_call>
413Middle
414<tool_call>
415{"name": "memory", "input": {"action": "recall", "query": "b"}}
416</tool_call>"#;
417 let (text, calls) = parse_tool_calls(input);
418 assert_eq!(text, "Middle");
419 assert_eq!(calls.len(), 2);
420 assert_eq!(calls[0].name, "rag");
421 assert_eq!(calls[0].id, "local-1");
422 assert_eq!(calls[1].name, "memory");
423 assert_eq!(calls[1].id, "local-2");
424 }
425
426 #[test]
427 fn test_parse_malformed_json() {
428 let input = r#"<tool_call>
429not valid json
430</tool_call>"#;
431 let (_text, calls) = parse_tool_calls(input);
432 assert!(calls.is_empty());
433 }
434
435 #[test]
436 fn test_parse_missing_close_tag_with_valid_json() {
437 let input =
439 "<tool_call>\n{\"name\": \"file_read\", \"input\": {\"path\": \"src/main.rs\"}}";
440 let (text, calls) = parse_tool_calls(input);
441 assert_eq!(calls.len(), 1, "should extract tool call without closing tag");
442 assert_eq!(calls[0].name, "file_read");
443 assert!(text.is_empty(), "no remaining text expected");
444 }
445
446 #[test]
447 fn test_parse_missing_close_tag_with_trailing_text() {
448 let input =
450 "Let me read that.\n<tool_call> {\"name\": \"file_read\", \"input\": {\"path\": \"foo.rs\"}}";
451 let (text, calls) = parse_tool_calls(input);
452 assert_eq!(calls.len(), 1);
453 assert_eq!(calls[0].name, "file_read");
454 assert!(text.contains("Let me read that"));
455 }
456
457 #[test]
458 fn test_parse_missing_close_tag_invalid_json() {
459 let input = "<tool_call>\nnot valid json at all";
461 let (text, calls) = parse_tool_calls(input);
462 assert!(calls.is_empty(), "invalid JSON should not produce tool call");
463 assert!(text.contains("<tool_call>"));
464 }
465
466 #[test]
467 fn test_parse_markdown_code_block() {
468 let input = "Let me read that file.\n```json\n{\"name\": \"file_read\", \"input\": {\"path\": \"src/main.rs\"}}\n```";
470 let (text, calls) = parse_tool_calls(input);
471 assert_eq!(calls.len(), 1, "should extract tool call from markdown block");
472 assert_eq!(calls[0].name, "file_read");
473 assert_eq!(calls[0].input["path"], "src/main.rs");
474 assert!(text.contains("Let me read that"));
475 }
476
477 #[test]
478 fn test_parse_markdown_code_block_not_tool_call() {
479 let input = "Here's an example:\n```json\n{\"key\": \"value\"}\n```";
481 let (text, calls) = parse_tool_calls(input);
482 assert!(calls.is_empty(), "JSON without name field should not be a tool call");
483 assert!(text.contains("example"));
484 }
485
486 #[test]
487 fn test_parse_missing_name() {
488 let input = r#"<tool_call>
489{"input": {"query": "test"}}
490</tool_call>"#;
491 let (_, calls) = parse_tool_calls(input);
492 assert!(calls.is_empty(), "JSON without name should not be extracted");
493 }
494
495 #[test]
496 fn test_privacy_tier_always_sovereign() {
497 assert_eq!(PrivacyTier::Sovereign, PrivacyTier::Sovereign);
498 }
499
500 #[test]
503 fn test_sanitize_strips_echoed_system_prompt() {
504 let sys = "You are apr code, a sovereign AI coding assistant.";
505 let output = format!("{sys} And then the model continues here.");
506 let cleaned = sanitize_output(&output, Some(sys));
507 assert!(!cleaned.contains("sovereign AI coding assistant"));
508 assert!(cleaned.contains("continues here"));
509 }
510
511 #[test]
512 fn test_sanitize_strips_chat_markers() {
513 let output = "<|im_start|>assistant\nHello world<|im_end|>";
514 let cleaned = sanitize_output(output, None);
515 assert_eq!(cleaned, "Hello world");
516 }
517
518 #[test]
519 fn test_sanitize_preserves_clean_output() {
520 let output = "The answer is 42.";
521 let cleaned = sanitize_output(output, Some("You are helpful."));
522 assert_eq!(cleaned, "The answer is 42.");
523 }
524
525 #[test]
526 fn test_sanitize_strips_role_prefix() {
527 let output = "assistant\nHere is my response.";
528 let cleaned = sanitize_output(output, None);
529 assert_eq!(cleaned, "Here is my response.");
530 }
531
532 #[test]
535 fn test_salvage_bare_top_level_json_tool_call() {
536 let input =
539 "Sure, I'll read it.\n{\"name\": \"file_read\", \"input\": {\"path\": \"src/lib.rs\"}}";
540 let (text, calls) = parse_tool_calls(input);
541 assert_eq!(calls.len(), 1, "salvage must recover a bare tool-call JSON object");
542 assert_eq!(calls[0].name, "file_read");
543 assert_eq!(calls[0].input["path"], "src/lib.rs");
544 assert!(calls[0].id.starts_with("salvage-"), "salvaged calls carry a traceable id");
545 assert!(text.contains("Sure, I'll read it"), "prose around the call is preserved");
546 assert!(!text.contains("file_read"), "the salvaged JSON span is removed from text");
547 }
548
549 #[test]
550 fn test_salvage_generic_fenced_block_non_json_tag() {
551 let input =
554 "```tool_call\n{\"name\": \"shell\", \"input\": {\"command\": \"cargo test\"}}\n```";
555 let (_text, calls) = parse_tool_calls(input);
556 assert_eq!(calls.len(), 1, "salvage must recover a generically-fenced tool call");
557 assert_eq!(calls[0].name, "shell");
558 assert_eq!(calls[0].input["command"], "cargo test");
559 }
560
561 #[test]
562 fn test_salvage_conservative_rejects_plain_json() {
563 let input = "Here is some config:\n{\"key\": \"value\", \"count\": 3}";
565 let (text, calls) = parse_tool_calls(input);
566 assert!(calls.is_empty(), "plain JSON (no name+input) must not be salvaged");
567 assert!(text.contains("config"));
568 }
569
570 #[test]
571 fn test_salvage_conservative_rejects_name_without_input() {
572 let input = "{\"name\": \"file_read\"}";
574 let (_text, calls) = parse_tool_calls(input);
575 assert!(calls.is_empty(), "name without input is too ambiguous to salvage");
576 }
577
578 #[test]
579 fn test_salvage_handles_braces_inside_strings() {
580 let input = "{\"name\": \"shell\", \"input\": {\"command\": \"echo ${HOME} and }{\"}}";
582 let (_text, calls) = parse_tool_calls(input);
583 assert_eq!(calls.len(), 1);
584 assert_eq!(calls[0].name, "shell");
585 assert_eq!(calls[0].input["command"], "echo ${HOME} and }{");
586 }
587
588 #[test]
589 fn test_envelope_takes_precedence_over_salvage() {
590 let input =
593 "<tool_call>\n{\"name\": \"glob\", \"input\": {\"pattern\": \"*.rs\"}}\n</tool_call>";
594 let (_text, calls) = parse_tool_calls(input);
595 assert_eq!(calls.len(), 1);
596 assert_eq!(calls[0].id, "local-1", "envelope parser owns this, not salvage");
597 }
598}