1use async_trait::async_trait;
28use serde_json::{Map, Value};
29
30use crate::registry::KernelError;
31use crate::registry::ToolRegistry;
32use crate::tool::{ToolName, ToolResultEnvelope, ToolResultEnvelopeConfig};
33use crate::trace::{DispatchTrace, DispatchTraceEvent, TracedAction, TracedOutcome};
34
35#[derive(Debug, Clone, PartialEq)]
39pub struct ToolInvocation {
40 pub name: ToolName,
42 pub args: Value,
44}
45
46impl ToolInvocation {
47 pub fn new(name: impl Into<ToolName>, args: Value) -> Result<Self, KernelError> {
49 let name = name.into();
50 if name.trim().is_empty() {
51 return Err(KernelError::NormalizerFailed(
52 "empty tool name in structured tool call".into(),
53 ));
54 }
55 validate_identifier("tool name", &name)?;
56 Ok(Self { name, args })
57 }
58
59 pub async fn dispatch(&self, tools: &ToolRegistry) -> Result<Value, KernelError> {
61 tools.invoke(&self.name, self.args.clone()).await
62 }
63}
64
65#[derive(Debug, Clone, PartialEq)]
67pub struct ToolInvocationResult {
68 pub invocation: ToolInvocation,
70 pub output: Value,
72}
73
74#[derive(Debug, Clone, PartialEq)]
81pub struct BoundedToolInvocationResult {
82 pub invocation: ToolInvocation,
84 pub envelope: ToolResultEnvelope,
86}
87
88#[derive(Debug, Clone, PartialEq)]
90pub enum ToolDispatchAction {
91 Continue,
93 Skip {
95 output: Value,
97 reason: Option<String>,
99 },
100 Terminate { reason: String },
102}
103
104#[derive(Debug, Clone, PartialEq)]
106pub enum ToolInvocationOutcome {
107 Completed,
109 Skipped {
111 reason: Option<String>,
113 },
114}
115
116#[async_trait]
123pub trait ToolDispatchHook: Send + Sync {
124 async fn before_invocation(
128 &self,
129 _invocation: &ToolInvocation,
130 ) -> Result<ToolDispatchAction, KernelError> {
131 Ok(ToolDispatchAction::Continue)
132 }
133
134 async fn after_invocation(&self, _result: &ToolInvocationResult) -> Result<(), KernelError> {
136 Ok(())
137 }
138
139 async fn after_invocation_with_outcome(
145 &self,
146 result: &ToolInvocationResult,
147 _outcome: &ToolInvocationOutcome,
148 ) -> Result<(), KernelError> {
149 self.after_invocation(result).await
150 }
151
152 async fn on_invocation_error(
156 &self,
157 _invocation: &ToolInvocation,
158 _error: &KernelError,
159 ) -> Result<(), KernelError> {
160 Ok(())
161 }
162}
163
164pub async fn dispatch_tool_invocations(
171 tools: &ToolRegistry,
172 invocations: &[ToolInvocation],
173) -> Result<Vec<ToolInvocationResult>, KernelError> {
174 dispatch_tool_invocations_with_hooks(tools, invocations, &[]).await
175}
176
177pub async fn dispatch_tool_invocations_with_hooks(
184 tools: &ToolRegistry,
185 invocations: &[ToolInvocation],
186 hooks: &[&dyn ToolDispatchHook],
187) -> Result<Vec<ToolInvocationResult>, KernelError> {
188 dispatch_inner(tools, invocations, hooks, None).await
189}
190
191pub async fn dispatch_tool_invocations_bounded(
198 tools: &ToolRegistry,
199 invocations: &[ToolInvocation],
200 config: &ToolResultEnvelopeConfig,
201) -> Result<Vec<BoundedToolInvocationResult>, KernelError> {
202 dispatch_tool_invocations_with_hooks_bounded(tools, invocations, &[], config).await
203}
204
205pub async fn dispatch_tool_invocations_with_hooks_bounded(
211 tools: &ToolRegistry,
212 invocations: &[ToolInvocation],
213 hooks: &[&dyn ToolDispatchHook],
214 config: &ToolResultEnvelopeConfig,
215) -> Result<Vec<BoundedToolInvocationResult>, KernelError> {
216 let results = dispatch_tool_invocations_with_hooks(tools, invocations, hooks).await?;
217 Ok(bound_invocation_results(results, config))
218}
219
220pub async fn dispatch_tool_invocations_with_trace(
228 tools: &ToolRegistry,
229 invocations: &[ToolInvocation],
230 hooks: &[&dyn ToolDispatchHook],
231 trace: &DispatchTrace,
232) -> Result<Vec<ToolInvocationResult>, KernelError> {
233 dispatch_inner(tools, invocations, hooks, Some(trace)).await
234}
235
236fn bound_invocation_results(
237 results: Vec<ToolInvocationResult>,
238 config: &ToolResultEnvelopeConfig,
239) -> Vec<BoundedToolInvocationResult> {
240 results
241 .into_iter()
242 .map(|result| BoundedToolInvocationResult {
243 invocation: result.invocation,
244 envelope: ToolResultEnvelope::bound(result.output, config),
245 })
246 .collect()
247}
248
249async fn dispatch_inner(
250 tools: &ToolRegistry,
251 invocations: &[ToolInvocation],
252 hooks: &[&dyn ToolDispatchHook],
253 trace: Option<&DispatchTrace>,
254) -> Result<Vec<ToolInvocationResult>, KernelError> {
255 let mut results = Vec::with_capacity(invocations.len());
256
257 for (invocation_index, invocation) in invocations.iter().enumerate() {
258 let mut action = ToolDispatchAction::Continue;
259 let mut observed: usize = 0;
265 let mut before_err: Option<(usize, KernelError)> = None;
266 for (hook_index, hook) in hooks.iter().enumerate() {
267 match hook.before_invocation(invocation).await {
268 Ok(next) => {
269 observed += 1;
270 if let Some(trace) = trace {
271 trace.push(DispatchTraceEvent::HookBefore {
272 invocation_index,
273 hook_index,
274 decision: TracedAction::from(&next),
275 });
276 }
277 action = next;
278 if !matches!(action, ToolDispatchAction::Continue) {
279 break;
280 }
281 }
282 Err(error) => {
283 before_err = Some((hook_index, error));
284 break;
285 }
286 }
287 }
288 if let Some((hook_index, error)) = before_err {
289 if let Some(trace) = trace {
290 trace.push(DispatchTraceEvent::HookBeforeError {
291 invocation_index,
292 hook_index,
293 message: error.to_string(),
294 });
295 }
296 notify_invocation_error_subset(
297 hooks,
298 observed,
299 invocation,
300 &error,
301 trace,
302 invocation_index,
303 )
304 .await?;
305 if let Some(trace) = trace {
306 trace.push(DispatchTraceEvent::InvocationOutcome {
307 invocation_index,
308 outcome: TracedOutcome::Failed {
309 message: error.to_string(),
310 },
311 });
312 }
313 return Err(error);
314 }
315
316 let (output, outcome) = match action {
317 ToolDispatchAction::Continue => match invocation.dispatch(tools).await {
318 Ok(output) => (output, ToolInvocationOutcome::Completed),
319 Err(error) => {
320 notify_invocation_error(hooks, invocation, &error, trace, invocation_index)
321 .await?;
322 if let Some(trace) = trace {
323 trace.push(DispatchTraceEvent::InvocationOutcome {
324 invocation_index,
325 outcome: TracedOutcome::Failed {
326 message: error.to_string(),
327 },
328 });
329 }
330 return Err(error);
331 }
332 },
333 ToolDispatchAction::Skip { output, reason } => {
334 (output, ToolInvocationOutcome::Skipped { reason })
335 }
336 ToolDispatchAction::Terminate { reason } => {
337 let error = KernelError::ToolDispatchTerminated(reason.clone());
338 notify_invocation_error(hooks, invocation, &error, trace, invocation_index).await?;
339 if let Some(trace) = trace {
340 trace.push(DispatchTraceEvent::InvocationOutcome {
341 invocation_index,
342 outcome: TracedOutcome::Terminated { reason },
343 });
344 }
345 return Err(error);
346 }
347 };
348
349 let result = ToolInvocationResult {
350 invocation: invocation.clone(),
351 output,
352 };
353
354 for (hook_index, hook) in hooks.iter().enumerate() {
355 hook.after_invocation_with_outcome(&result, &outcome)
356 .await?;
357 if let Some(trace) = trace {
358 trace.push(DispatchTraceEvent::HookAfter {
359 invocation_index,
360 hook_index,
361 });
362 }
363 }
364
365 if let Some(trace) = trace {
366 let outcome_event = match &outcome {
367 ToolInvocationOutcome::Completed => TracedOutcome::Completed,
368 ToolInvocationOutcome::Skipped { reason } => TracedOutcome::Skipped {
369 reason: reason.clone(),
370 },
371 };
372 trace.push(DispatchTraceEvent::InvocationOutcome {
373 invocation_index,
374 outcome: outcome_event,
375 });
376 }
377
378 results.push(result);
379 }
380
381 Ok(results)
382}
383
384async fn notify_invocation_error(
385 hooks: &[&dyn ToolDispatchHook],
386 invocation: &ToolInvocation,
387 error: &KernelError,
388 trace: Option<&DispatchTrace>,
389 invocation_index: usize,
390) -> Result<(), KernelError> {
391 for (hook_index, hook) in hooks.iter().enumerate() {
392 hook.on_invocation_error(invocation, error).await?;
393 if let Some(trace) = trace {
394 trace.push(DispatchTraceEvent::HookCleanup {
395 invocation_index,
396 hook_index,
397 });
398 }
399 }
400 Ok(())
401}
402
403async fn notify_invocation_error_subset(
407 hooks: &[&dyn ToolDispatchHook],
408 upto: usize,
409 invocation: &ToolInvocation,
410 error: &KernelError,
411 trace: Option<&DispatchTrace>,
412 invocation_index: usize,
413) -> Result<(), KernelError> {
414 for (hook_index, hook) in hooks.iter().take(upto).enumerate() {
415 hook.on_invocation_error(invocation, error).await?;
416 if let Some(trace) = trace {
417 trace.push(DispatchTraceEvent::HookCleanup {
418 invocation_index,
419 hook_index,
420 });
421 }
422 }
423 Ok(())
424}
425
426pub trait ToolCallNormalizer: Send + Sync {
441 fn normalize(&self, raw: &str) -> Result<Vec<ToolInvocation>, KernelError>;
443
444 fn is_applicable(&self, raw: &str) -> bool;
446}
447
448#[derive(Debug, Clone, Default)]
460pub struct StructuredToolCallNormalizer;
461
462impl StructuredToolCallNormalizer {
463 pub fn normalize_openai_responses(value: &Value) -> Result<Vec<ToolInvocation>, KernelError> {
466 match value {
467 Value::Object(object) => {
468 if let Some(output) = object.get("output") {
469 return normalize_responses_output(output);
470 }
471 if is_responses_function_call(object) {
472 return parse_responses_function_call(object).map(|call| vec![call]);
473 }
474 Ok(Vec::new())
475 }
476 Value::Array(items) => items
477 .iter()
478 .map(normalize_responses_output_item)
479 .collect::<Result<Vec<_>, _>>()
480 .map(flatten_invocations),
481 _ => Ok(Vec::new()),
482 }
483 }
484
485 pub fn normalize_openai_chat_completions(
488 value: &Value,
489 ) -> Result<Vec<ToolInvocation>, KernelError> {
490 match value {
491 Value::Object(object) => {
492 if let Some(choices) = object.get("choices") {
493 return normalize_chat_choices(choices);
494 }
495 if let Some(tool_calls) = object.get("tool_calls") {
496 return normalize_chat_tool_calls(tool_calls);
497 }
498 if is_chat_tool_call(object) {
499 return parse_chat_tool_call(object).map(|call| vec![call]);
500 }
501 Ok(Vec::new())
502 }
503 Value::Array(items) => normalize_chat_tool_calls_array(items),
504 _ => Ok(Vec::new()),
505 }
506 }
507
508 pub fn normalize(value: &Value) -> Result<Vec<ToolInvocation>, KernelError> {
514 let mut invocations = Self::normalize_openai_responses(value)?;
515 invocations.extend(Self::normalize_openai_chat_completions(value)?);
516 Ok(invocations)
517 }
518}
519
520const LFM_START: &str = "<|tool_call_start|>";
523const LFM_END: &str = "<|tool_call_end|>";
524
525#[derive(Debug, Clone, Default)]
549pub struct LfmNormalizer;
550
551impl ToolCallNormalizer for LfmNormalizer {
552 fn is_applicable(&self, raw: &str) -> bool {
553 raw.contains(LFM_START)
554 }
555
556 fn normalize(&self, raw: &str) -> Result<Vec<ToolInvocation>, KernelError> {
557 let mut results = Vec::new();
558 let mut remaining = raw;
559
560 while let Some(block_start) = remaining.find(LFM_START) {
561 let after_start = remaining
563 .get(block_start + LFM_START.len()..)
564 .ok_or_else(|| KernelError::NormalizerFailed("LFM: start marker overrun".into()))?;
565
566 let block_end = after_start.find(LFM_END).ok_or_else(|| {
567 KernelError::NormalizerFailed("LFM: unclosed <|tool_call_start|> marker".into())
568 })?;
569
570 let block = after_start.get(..block_end).ok_or_else(|| {
571 KernelError::NormalizerFailed("LFM: block slice out of bounds".into())
572 })?;
573
574 remaining = after_start.get(block_end + LFM_END.len()..).unwrap_or("");
576
577 let calls = parse_lfm_block(block)?;
578 results.extend(calls);
579 }
580
581 Ok(results)
582 }
583}
584
585fn normalize_responses_output(value: &Value) -> Result<Vec<ToolInvocation>, KernelError> {
588 match value {
589 Value::Array(items) => items
590 .iter()
591 .map(normalize_responses_output_item)
592 .collect::<Result<Vec<_>, _>>()
593 .map(flatten_invocations),
594 Value::Object(object) if is_responses_function_call(object) => {
595 parse_responses_function_call(object).map(|call| vec![call])
596 }
597 _ => Ok(Vec::new()),
598 }
599}
600
601fn normalize_responses_output_item(value: &Value) -> Result<Vec<ToolInvocation>, KernelError> {
602 match value {
603 Value::Object(object) if is_responses_function_call(object) => {
604 parse_responses_function_call(object).map(|call| vec![call])
605 }
606 _ => Ok(Vec::new()),
607 }
608}
609
610fn is_responses_function_call(object: &Map<String, Value>) -> bool {
611 object
612 .get("type")
613 .and_then(Value::as_str)
614 .is_some_and(|kind| kind == "function_call")
615}
616
617fn parse_responses_function_call(
618 object: &Map<String, Value>,
619) -> Result<ToolInvocation, KernelError> {
620 let name = required_string_field(object, "name", "OpenAI Responses function_call")?;
621 let args = object
622 .get("arguments")
623 .map(parse_standard_arguments)
624 .transpose()?
625 .unwrap_or_else(|| Value::Object(Map::new()));
626 ToolInvocation::new(name, args)
627}
628
629fn normalize_chat_choices(value: &Value) -> Result<Vec<ToolInvocation>, KernelError> {
630 let choices = value.as_array().ok_or_else(|| {
631 KernelError::NormalizerFailed("OpenAI Chat Completions choices must be an array".into())
632 })?;
633
634 let mut invocations = Vec::new();
635 for choice in choices {
636 let Some(message) = choice.get("message") else {
637 continue;
638 };
639 invocations
640 .extend(StructuredToolCallNormalizer::normalize_openai_chat_completions(message)?);
641 }
642
643 Ok(invocations)
644}
645
646fn normalize_chat_tool_calls(value: &Value) -> Result<Vec<ToolInvocation>, KernelError> {
647 match value {
648 Value::Array(items) => normalize_chat_tool_calls_array(items),
649 Value::Object(object) if is_chat_tool_call(object) => {
650 parse_chat_tool_call(object).map(|call| vec![call])
651 }
652 _ => Ok(Vec::new()),
653 }
654}
655
656fn normalize_chat_tool_calls_array(items: &[Value]) -> Result<Vec<ToolInvocation>, KernelError> {
657 items
658 .iter()
659 .map(|item| match item {
660 Value::Object(object) if is_chat_tool_call(object) => parse_chat_tool_call(object),
661 Value::Object(_) => Err(KernelError::NormalizerFailed(
662 "OpenAI Chat Completions tool call missing function payload".into(),
663 )),
664 _ => Err(KernelError::NormalizerFailed(
665 "OpenAI Chat Completions tool call must be an object".into(),
666 )),
667 })
668 .collect()
669}
670
671fn is_chat_tool_call(object: &Map<String, Value>) -> bool {
672 object.get("function").is_some()
673}
674
675fn parse_chat_tool_call(object: &Map<String, Value>) -> Result<ToolInvocation, KernelError> {
676 let function = object
677 .get("function")
678 .and_then(Value::as_object)
679 .ok_or_else(|| {
680 KernelError::NormalizerFailed(
681 "OpenAI Chat Completions tool call missing function object".into(),
682 )
683 })?;
684 let name = required_string_field(function, "name", "OpenAI Chat Completions function")?;
685 let args = function
686 .get("arguments")
687 .map(parse_standard_arguments)
688 .transpose()?
689 .unwrap_or_else(|| Value::Object(Map::new()));
690
691 ToolInvocation::new(name, args)
692}
693
694fn parse_standard_arguments(value: &Value) -> Result<Value, KernelError> {
695 match value {
696 Value::String(raw) => {
697 let trimmed = raw.trim();
698 if trimmed.is_empty() {
699 return Ok(Value::Object(Map::new()));
700 }
701 serde_json::from_str(trimmed).map_err(|err| {
702 KernelError::NormalizerFailed(format!(
703 "failed to parse standard tool-call arguments JSON: {err}"
704 ))
705 })
706 }
707 Value::Null => Ok(Value::Object(Map::new())),
708 other => Ok(other.clone()),
709 }
710}
711
712fn required_string_field(
713 object: &Map<String, Value>,
714 field: &str,
715 context: &str,
716) -> Result<String, KernelError> {
717 object
718 .get(field)
719 .and_then(Value::as_str)
720 .map(ToOwned::to_owned)
721 .ok_or_else(|| KernelError::NormalizerFailed(format!("{context} missing `{field}` string")))
722}
723
724fn flatten_invocations(nested: Vec<Vec<ToolInvocation>>) -> Vec<ToolInvocation> {
725 nested.into_iter().flatten().collect()
726}
727
728fn parse_lfm_block(block: &str) -> Result<Vec<ToolInvocation>, KernelError> {
732 let block = block.trim();
733 let inner = block
735 .strip_prefix('[')
736 .and_then(|s| s.strip_suffix(']'))
737 .unwrap_or(block);
738
739 split_top_level(inner, ',')
740 .into_iter()
741 .filter(|s| !s.trim().is_empty())
742 .map(|s| parse_lfm_call(s.trim()))
743 .collect()
744}
745
746fn parse_lfm_call(expr: &str) -> Result<ToolInvocation, KernelError> {
748 let (name_raw, rest) = expr.split_once('(').ok_or_else(|| {
749 KernelError::NormalizerFailed(format!("LFM: expected '(' in call: {expr:?}"))
750 })?;
751
752 let name = name_raw.trim().to_string();
753 if name.is_empty() {
754 return Err(KernelError::NormalizerFailed(
755 "LFM: empty tool name in call expression".into(),
756 ));
757 }
758 validate_identifier("tool name", &name)?;
759
760 let (kwargs_str, trailing) = rest.rsplit_once(')').ok_or_else(|| {
762 KernelError::NormalizerFailed(format!("LFM: missing closing ')' in: {expr:?}"))
763 })?;
764 if !trailing.trim().is_empty() {
765 return Err(KernelError::NormalizerFailed(format!(
766 "LFM: trailing content after call expression: {trailing:?}"
767 )));
768 }
769
770 let args = parse_kwargs(kwargs_str)?;
771 Ok(ToolInvocation { name, args })
772}
773
774fn parse_kwargs(s: &str) -> Result<Value, KernelError> {
776 let s = s.trim();
777 if s.is_empty() {
778 return Ok(Value::Object(Map::new()));
779 }
780
781 let mut map = Map::new();
782 for pair in split_top_level(s, ',') {
783 let pair = pair.trim();
784 if pair.is_empty() {
785 continue;
786 }
787 let (key_raw, val_raw) = pair.split_once('=').ok_or_else(|| {
788 KernelError::NormalizerFailed(format!("LFM: kwarg without '=': {pair:?}"))
789 })?;
790 let key = key_raw.trim().to_string();
791 if key.is_empty() {
792 return Err(KernelError::NormalizerFailed(
793 "LFM: empty kwarg name".into(),
794 ));
795 }
796 validate_identifier("kwarg name", &key)?;
797 if map.contains_key(&key) {
798 return Err(KernelError::NormalizerFailed(format!(
799 "LFM: duplicate kwarg: {key}"
800 )));
801 }
802 let val = parse_value(val_raw.trim())?;
803 map.insert(key, val);
804 }
805
806 Ok(Value::Object(map))
807}
808
809fn parse_value(s: &str) -> Result<Value, KernelError> {
815 let s = s.trim();
816
817 if s.is_empty() {
818 return Ok(Value::String(String::new()));
819 }
820
821 if let Some(inner) = s.strip_prefix('\'').and_then(|t| t.strip_suffix('\'')) {
823 return Ok(Value::String(
824 inner.replace("\\'", "'").replace("\\\"", "\""),
825 ));
826 }
827 if s.starts_with('\'') {
828 return Err(KernelError::NormalizerFailed(
829 "LFM: unterminated single-quoted string".into(),
830 ));
831 }
832 if let Some(inner) = s.strip_prefix('"').and_then(|t| t.strip_suffix('"')) {
834 return Ok(Value::String(
835 inner.replace("\\'", "'").replace("\\\"", "\""),
836 ));
837 }
838 if s.starts_with('"') {
839 return Err(KernelError::NormalizerFailed(
840 "LFM: unterminated double-quoted string".into(),
841 ));
842 }
843 if s == "True" {
845 return Ok(Value::Bool(true));
846 }
847 if s == "False" {
848 return Ok(Value::Bool(false));
849 }
850 if s == "None" || s == "null" {
852 return Ok(Value::Null);
853 }
854 if let Some(inner) = s.strip_prefix('[').and_then(|t| t.strip_suffix(']')) {
856 return parse_array(inner);
857 }
858 if s.starts_with('[') {
859 return Err(KernelError::NormalizerFailed(
860 "LFM: unterminated list literal".into(),
861 ));
862 }
863 if let Some(inner) = s.strip_prefix('{').and_then(|t| t.strip_suffix('}')) {
865 return parse_object(inner);
866 }
867 if s.starts_with('{') {
868 return Err(KernelError::NormalizerFailed(
869 "LFM: unterminated object literal".into(),
870 ));
871 }
872 if let Ok(n) = s.parse::<i64>() {
874 return Ok(Value::Number(n.into()));
875 }
876 if let Ok(f) = s.parse::<f64>() {
878 let num = serde_json::Number::from_f64(f).ok_or_else(|| {
879 KernelError::NormalizerFailed(format!("LFM: non-finite float in argument: {s:?}"))
880 })?;
881 return Ok(Value::Number(num));
882 }
883 Ok(Value::String(s.to_string()))
885}
886
887fn parse_array(inner: &str) -> Result<Value, KernelError> {
888 let inner = inner.trim();
889 if inner.is_empty() {
890 return Ok(Value::Array(Vec::new()));
891 }
892
893 let values = split_top_level(inner, ',')
894 .into_iter()
895 .filter(|part| !part.trim().is_empty())
896 .map(|part| parse_value(part.trim()))
897 .collect::<Result<Vec<_>, _>>()?;
898
899 Ok(Value::Array(values))
900}
901
902fn parse_object(inner: &str) -> Result<Value, KernelError> {
903 let inner = inner.trim();
904 if inner.is_empty() {
905 return Ok(Value::Object(Map::new()));
906 }
907
908 let mut map = Map::new();
909 for entry in split_top_level(inner, ',') {
910 let entry = entry.trim();
911 if entry.is_empty() {
912 continue;
913 }
914
915 let (key_raw, value_raw) = split_once_top_level(entry, ':').ok_or_else(|| {
916 KernelError::NormalizerFailed(format!("LFM: object entry without ':': {entry:?}"))
917 })?;
918 let key = parse_object_key(key_raw.trim())?;
919 if map.contains_key(&key) {
920 return Err(KernelError::NormalizerFailed(format!(
921 "LFM: duplicate object key: {key}"
922 )));
923 }
924
925 map.insert(key, parse_value(value_raw.trim())?);
926 }
927
928 Ok(Value::Object(map))
929}
930
931fn parse_object_key(raw: &str) -> Result<String, KernelError> {
932 match parse_value(raw)? {
933 Value::String(key) => Ok(key),
934 _ => Err(KernelError::NormalizerFailed(format!(
935 "LFM: object key must be a string: {raw:?}"
936 ))),
937 }
938}
939
940fn validate_identifier(kind: &str, value: &str) -> Result<(), KernelError> {
944 let valid = value
945 .chars()
946 .all(|ch| ch.is_ascii_alphanumeric() || matches!(ch, '_' | '-' | '.'));
947
948 if valid {
949 return Ok(());
950 }
951
952 Err(KernelError::NormalizerFailed(format!(
953 "invalid {kind}: {value:?}"
954 )))
955}
956
957fn split_top_level(s: &str, delim: char) -> Vec<&str> {
961 let mut parts: Vec<&str> = Vec::new();
962 let mut depth: usize = 0;
963 let mut in_sq = false;
964 let mut in_dq = false;
965 let mut escape_next = false;
966 let mut start = 0usize;
967
968 for (i, ch) in s.char_indices() {
969 if escape_next {
970 escape_next = false;
971 continue;
972 }
973 if ch == '\\' && (in_sq || in_dq) {
974 escape_next = true;
975 continue;
976 }
977 if in_sq {
978 if ch == '\'' {
979 in_sq = false;
980 }
981 continue;
982 }
983 if in_dq {
984 if ch == '"' {
985 in_dq = false;
986 }
987 continue;
988 }
989 match ch {
990 '\'' => in_sq = true,
991 '"' => in_dq = true,
992 '(' | '[' | '{' => depth = depth.saturating_add(1),
993 ')' | ']' | '}' => depth = depth.saturating_sub(1),
994 c if c == delim && depth == 0 => {
995 parts.push(s.get(start..i).unwrap_or(""));
997 start = i + ch.len_utf8();
998 }
999 _ => {}
1000 }
1001 }
1002 parts.push(s.get(start..).unwrap_or(""));
1003 parts
1004}
1005
1006fn split_once_top_level(s: &str, delim: char) -> Option<(&str, &str)> {
1007 split_index_top_level(s, delim).map(|idx| {
1008 let left = s.get(..idx).unwrap_or("");
1009 let right = s.get(idx + delim.len_utf8()..).unwrap_or("");
1010 (left, right)
1011 })
1012}
1013
1014fn split_index_top_level(s: &str, delim: char) -> Option<usize> {
1015 let mut depth: usize = 0;
1016 let mut in_sq = false;
1017 let mut in_dq = false;
1018 let mut escape_next = false;
1019
1020 for (i, ch) in s.char_indices() {
1021 if escape_next {
1022 escape_next = false;
1023 continue;
1024 }
1025 if ch == '\\' && (in_sq || in_dq) {
1026 escape_next = true;
1027 continue;
1028 }
1029 if in_sq {
1030 if ch == '\'' {
1031 in_sq = false;
1032 }
1033 continue;
1034 }
1035 if in_dq {
1036 if ch == '"' {
1037 in_dq = false;
1038 }
1039 continue;
1040 }
1041 match ch {
1042 '\'' => in_sq = true,
1043 '"' => in_dq = true,
1044 '(' | '[' | '{' => depth = depth.saturating_add(1),
1045 ')' | ']' | '}' => depth = depth.saturating_sub(1),
1046 c if c == delim && depth == 0 => return Some(i),
1047 _ => {}
1048 }
1049 }
1050
1051 None
1052}
1053
1054#[cfg(test)]
1057mod tests {
1058 use super::*;
1059 use crate::{LocalTool, ToolRegistry, ToolSchema};
1060 use serde_json::json;
1061 use std::sync::Arc;
1062
1063 #[test]
1066 fn not_applicable_for_plain_text() {
1067 assert!(!LfmNormalizer.is_applicable("hello world"));
1068 }
1069
1070 #[test]
1071 fn applicable_when_start_marker_present() {
1072 assert!(
1073 LfmNormalizer
1074 .is_applicable("<|tool_call_start|>[get_weather(city='Berlin')]<|tool_call_end|>")
1075 );
1076 }
1077
1078 #[test]
1081 fn plain_text_returns_empty() {
1082 let calls = LfmNormalizer
1083 .normalize("The weather in Berlin is sunny.")
1084 .unwrap();
1085 assert!(calls.is_empty());
1086 }
1087
1088 #[test]
1089 fn single_call_string_arg() {
1090 let raw = "<|tool_call_start|>[get_weather(city='Berlin')]<|tool_call_end|>";
1091 let calls = LfmNormalizer.normalize(raw).unwrap();
1092 assert_eq!(calls.len(), 1);
1093 assert_eq!(calls[0].name, "get_weather");
1094 assert_eq!(calls[0].args, json!({"city": "Berlin"}));
1095 }
1096
1097 #[test]
1098 fn single_call_multiple_args() {
1099 let raw = "<|tool_call_start|>[search(query='rust async', limit=10)]<|tool_call_end|>";
1100 let calls = LfmNormalizer.normalize(raw).unwrap();
1101 assert_eq!(calls.len(), 1);
1102 assert_eq!(calls[0].name, "search");
1103 assert_eq!(calls[0].args, json!({"query": "rust async", "limit": 10}));
1104 }
1105
1106 #[test]
1107 fn single_call_no_args() {
1108 let raw = "<|tool_call_start|>[list_tools()]<|tool_call_end|>";
1109 let calls = LfmNormalizer.normalize(raw).unwrap();
1110 assert_eq!(calls.len(), 1);
1111 assert_eq!(calls[0].name, "list_tools");
1112 assert_eq!(calls[0].args, json!({}));
1113 }
1114
1115 #[test]
1116 fn multiple_calls_in_one_block() {
1117 let raw = "<|tool_call_start|>[get_weather(city='Berlin'), get_time(zone='UTC')]<|tool_call_end|>";
1118 let calls = LfmNormalizer.normalize(raw).unwrap();
1119 assert_eq!(calls.len(), 2);
1120 assert_eq!(calls[0].name, "get_weather");
1121 assert_eq!(calls[0].args, json!({"city": "Berlin"}));
1122 assert_eq!(calls[1].name, "get_time");
1123 assert_eq!(calls[1].args, json!({"zone": "UTC"}));
1124 }
1125
1126 #[test]
1127 fn multiple_blocks_in_one_message() {
1128 let raw = concat!(
1129 "<|tool_call_start|>[step_one(x=1)]<|tool_call_end|>",
1130 " some text ",
1131 "<|tool_call_start|>[step_two(y=2)]<|tool_call_end|>",
1132 );
1133 let calls = LfmNormalizer.normalize(raw).unwrap();
1134 assert_eq!(calls.len(), 2);
1135 assert_eq!(calls[0].name, "step_one");
1136 assert_eq!(calls[1].name, "step_two");
1137 }
1138
1139 #[test]
1140 fn block_without_brackets_is_parsed() {
1141 let raw = "<|tool_call_start|>ping(target='8.8.8.8')<|tool_call_end|>";
1143 let calls = LfmNormalizer.normalize(raw).unwrap();
1144 assert_eq!(calls.len(), 1);
1145 assert_eq!(calls[0].name, "ping");
1146 assert_eq!(calls[0].args, json!({"target": "8.8.8.8"}));
1147 }
1148
1149 #[test]
1152 fn integer_arg() {
1153 let raw = "<|tool_call_start|>[set_limit(n=42)]<|tool_call_end|>";
1154 let calls = LfmNormalizer.normalize(raw).unwrap();
1155 assert_eq!(calls[0].args, json!({"n": 42}));
1156 }
1157
1158 #[test]
1159 fn float_arg() {
1160 let raw = "<|tool_call_start|>[set_temp(t=0.7)]<|tool_call_end|>";
1161 let calls = LfmNormalizer.normalize(raw).unwrap();
1162 assert_eq!(calls[0].args["t"].as_f64().unwrap(), 0.7);
1163 }
1164
1165 #[test]
1166 fn boolean_args() {
1167 let raw = "<|tool_call_start|>[configure(verbose=True, strict=False)]<|tool_call_end|>";
1168 let calls = LfmNormalizer.normalize(raw).unwrap();
1169 assert_eq!(calls[0].args, json!({"verbose": true, "strict": false}));
1170 }
1171
1172 #[test]
1173 fn null_args() {
1174 let raw = "<|tool_call_start|>[reset(ctx=None)]<|tool_call_end|>";
1175 let calls = LfmNormalizer.normalize(raw).unwrap();
1176 assert_eq!(calls[0].args, json!({"ctx": null}));
1177 }
1178
1179 #[test]
1180 fn double_quoted_string_arg() {
1181 let raw = r#"<|tool_call_start|>[greet(name="world")]<|tool_call_end|>"#;
1182 let calls = LfmNormalizer.normalize(raw).unwrap();
1183 assert_eq!(calls[0].args, json!({"name": "world"}));
1184 }
1185
1186 #[test]
1187 fn nested_list_and_object_args() {
1188 let raw = "<|tool_call_start|>[plan(items=['a,b', 'c'], meta={'city': 'Berlin', 'coords': [52.52, 13.405], 'active': True})]<|tool_call_end|>";
1189 let calls = LfmNormalizer.normalize(raw).unwrap();
1190 assert_eq!(calls.len(), 1);
1191 assert_eq!(
1192 calls[0].args,
1193 json!({
1194 "items": ["a,b", "c"],
1195 "meta": {
1196 "city": "Berlin",
1197 "coords": [52.52, 13.405],
1198 "active": true
1199 }
1200 })
1201 );
1202 }
1203
1204 #[test]
1205 fn openai_responses_function_call_item() {
1206 let value = json!({
1207 "type": "function_call",
1208 "id": "fc_123",
1209 "call_id": "call_123",
1210 "name": "get_weather",
1211 "arguments": "{\"city\":\"Berlin\"}",
1212 "status": "completed"
1213 });
1214
1215 let calls = StructuredToolCallNormalizer::normalize_openai_responses(&value).unwrap();
1216 assert_eq!(calls.len(), 1);
1217 assert_eq!(calls[0].name, "get_weather");
1218 assert_eq!(calls[0].args, json!({"city": "Berlin"}));
1219 }
1220
1221 #[test]
1222 fn openai_responses_full_response() {
1223 let value = json!({
1224 "id": "resp_123",
1225 "output": [
1226 { "type": "message", "content": [] },
1227 {
1228 "type": "function_call",
1229 "id": "fc_123",
1230 "call_id": "call_123",
1231 "name": "search.docs",
1232 "arguments": {"query": "tool calls"},
1233 "status": "completed"
1234 }
1235 ]
1236 });
1237
1238 let calls = StructuredToolCallNormalizer::normalize_openai_responses(&value).unwrap();
1239 assert_eq!(calls.len(), 1);
1240 assert_eq!(calls[0].name, "search.docs");
1241 assert_eq!(calls[0].args, json!({"query": "tool calls"}));
1242 }
1243
1244 #[test]
1245 fn openai_chat_completions_tool_calls() {
1246 let value = json!({
1247 "choices": [{
1248 "message": {
1249 "role": "assistant",
1250 "content": null,
1251 "tool_calls": [{
1252 "id": "call_123",
1253 "type": "function",
1254 "function": {
1255 "name": "get_weather",
1256 "arguments": "{\"city\":\"Berlin\"}"
1257 }
1258 }]
1259 }
1260 }]
1261 });
1262
1263 let calls =
1264 StructuredToolCallNormalizer::normalize_openai_chat_completions(&value).unwrap();
1265 assert_eq!(calls.len(), 1);
1266 assert_eq!(calls[0].name, "get_weather");
1267 assert_eq!(calls[0].args, json!({"city": "Berlin"}));
1268 }
1269
1270 #[test]
1271 fn structured_normalizer_aggregates_supported_shapes() {
1272 let responses_value = json!({
1273 "output": [{
1274 "type": "function_call",
1275 "name": "first",
1276 "arguments": "{}"
1277 }]
1278 });
1279 let chat_value = json!({
1280 "tool_calls": [{
1281 "function": {
1282 "name": "second",
1283 "arguments": {"ok": true}
1284 }
1285 }]
1286 });
1287
1288 let responses_calls = StructuredToolCallNormalizer::normalize(&responses_value).unwrap();
1289 let chat_calls = StructuredToolCallNormalizer::normalize(&chat_value).unwrap();
1290
1291 assert_eq!(responses_calls[0].name, "first");
1292 assert_eq!(chat_calls[0].name, "second");
1293 assert_eq!(chat_calls[0].args, json!({"ok": true}));
1294 }
1295
1296 #[test]
1299 fn unclosed_marker_returns_error() {
1300 let raw = "<|tool_call_start|>[get_weather(city='Berlin')]";
1301 let err = LfmNormalizer.normalize(raw).unwrap_err();
1302 let msg = err.to_string();
1303 assert!(msg.contains("unclosed"), "expected 'unclosed' in: {msg}");
1304 }
1305
1306 #[test]
1307 fn missing_paren_returns_error() {
1308 let raw = "<|tool_call_start|>[not_a_call]<|tool_call_end|>";
1310 let err = LfmNormalizer.normalize(raw).unwrap_err();
1311 let msg = err.to_string();
1312 assert!(msg.contains("expected '('"), "got: {msg}");
1313 }
1314
1315 #[test]
1316 fn kwarg_without_equals_returns_error() {
1317 let raw = "<|tool_call_start|>[fn(badarg)]<|tool_call_end|>";
1318 let err = LfmNormalizer.normalize(raw).unwrap_err();
1319 let msg = err.to_string();
1320 assert!(msg.contains("kwarg without '='"), "got: {msg}");
1321 }
1322
1323 #[test]
1324 fn invalid_tool_name_returns_error() {
1325 let raw = "<|tool_call_start|>[bad/name(arg=1)]<|tool_call_end|>";
1326 let err = LfmNormalizer.normalize(raw).unwrap_err();
1327 let msg = err.to_string();
1328 assert!(msg.contains("invalid tool name"), "got: {msg}");
1329 }
1330
1331 #[test]
1332 fn empty_kwarg_name_returns_error() {
1333 let raw = "<|tool_call_start|>[fn(=1)]<|tool_call_end|>";
1334 let err = LfmNormalizer.normalize(raw).unwrap_err();
1335 let msg = err.to_string();
1336 assert!(msg.contains("empty kwarg name"), "got: {msg}");
1337 }
1338
1339 #[test]
1340 fn duplicate_kwarg_returns_error() {
1341 let raw = "<|tool_call_start|>[fn(city='Berlin', city='Paris')]<|tool_call_end|>";
1342 let err = LfmNormalizer.normalize(raw).unwrap_err();
1343 let msg = err.to_string();
1344 assert!(msg.contains("duplicate kwarg"), "got: {msg}");
1345 }
1346
1347 #[test]
1348 fn malformed_standard_arguments_return_error() {
1349 let value = json!({
1350 "type": "function_call",
1351 "name": "bad_args",
1352 "arguments": "{not json}"
1353 });
1354
1355 let err = StructuredToolCallNormalizer::normalize_openai_responses(&value).unwrap_err();
1356 let msg = err.to_string();
1357 assert!(msg.contains("arguments JSON"), "got: {msg}");
1358 }
1359
1360 #[test]
1361 fn trailing_call_content_returns_error() {
1362 let raw = "<|tool_call_start|>[fn(arg=1) extra]<|tool_call_end|>";
1363 let err = LfmNormalizer.normalize(raw).unwrap_err();
1364 let msg = err.to_string();
1365 assert!(msg.contains("trailing content"), "got: {msg}");
1366 }
1367
1368 #[test]
1369 fn unterminated_nested_literal_returns_error() {
1370 let raw = "<|tool_call_start|>[fn(items=['a', 'b')]<|tool_call_end|>";
1371 let err = LfmNormalizer.normalize(raw).unwrap_err();
1372 let msg = err.to_string();
1373 assert!(msg.contains("unterminated list"), "got: {msg}");
1374 }
1375
1376 #[tokio::test]
1377 async fn dispatch_invocations_runs_tools_in_order() {
1378 let tools = ToolRegistry::new();
1379 tools.register(Arc::new(LocalTool::new(
1380 ToolSchema {
1381 name: "echo".into(),
1382 description: "echoes args".into(),
1383 args_schema: json!({"type": "object"}),
1384 result_schema: json!({"type": "object"}),
1385 },
1386 |args| async move { Ok(json!({"seen": args})) },
1387 )));
1388
1389 let invocations = LfmNormalizer
1390 .normalize("<|tool_call_start|>[echo(value={'nested': [1, 2]})]<|tool_call_end|>")
1391 .unwrap();
1392 let results = dispatch_tool_invocations(&tools, &invocations)
1393 .await
1394 .unwrap();
1395
1396 assert_eq!(results.len(), 1);
1397 assert_eq!(results[0].invocation.name, "echo");
1398 assert_eq!(
1399 results[0].output,
1400 json!({"seen": {"value": {"nested": [1, 2]}}})
1401 );
1402 }
1403
1404 #[test]
1407 fn split_respects_parens() {
1408 let parts = split_top_level("fn(a, b), fn2(c)", ',');
1410 assert_eq!(parts, vec!["fn(a, b)", " fn2(c)"]);
1411 }
1412
1413 #[test]
1414 fn split_respects_single_quotes() {
1415 let parts = split_top_level("a='x,y', b=2", ',');
1416 assert_eq!(parts, vec!["a='x,y'", " b=2"]);
1417 }
1418
1419 #[test]
1420 fn split_respects_nested_arrays_and_objects() {
1421 let parts = split_top_level("a=[1, 2], b={'x': 'y,z'}, c=3", ',');
1422 assert_eq!(parts, vec!["a=[1, 2]", " b={'x': 'y,z'}", " c=3"]);
1423 }
1424}