@@ -5,7 +5,7 @@ use anyhow::{bail, Context};
55
66use rexos_kernel:: router:: { ModelRouter , TaskKind } ;
77use rexos_llm:: driver:: LlmDriver ;
8- use rexos_llm:: openai_compat:: { ChatCompletionRequest , ChatMessage , Role } ;
8+ use rexos_llm:: openai_compat:: { ChatCompletionRequest , ChatMessage , Role , ToolCall , ToolFunction } ;
99use rexos_llm:: registry:: LlmRegistry ;
1010use rexos_memory:: MemoryStore ;
1111use rexos_tools:: Toolset ;
@@ -76,7 +76,7 @@ impl AgentRuntime {
7676 model : model. clone ( ) ,
7777 messages : messages. clone ( ) ,
7878 tools : tool_defs. clone ( ) ,
79- temperature : None ,
79+ temperature : Some ( 0.0 ) ,
8080 } ;
8181
8282 let assistant = self
@@ -89,9 +89,14 @@ impl AgentRuntime {
8989
9090 let tool_calls = match assistant. tool_calls . clone ( ) {
9191 Some ( calls) if !calls. is_empty ( ) => calls,
92- _ => {
93- return Ok ( assistant. content . unwrap_or_default ( ) ) ;
94- }
92+ _ => match assistant
93+ . content
94+ . as_deref ( )
95+ . and_then ( parse_tool_calls_from_json_content)
96+ {
97+ Some ( calls) => calls,
98+ None => return Ok ( assistant. content . unwrap_or_default ( ) ) ,
99+ } ,
95100 } ;
96101
97102 for call in tool_calls {
@@ -102,8 +107,9 @@ impl AgentRuntime {
102107 bail ! ( "tool loop detected: {sig}" ) ;
103108 }
104109
110+ let args_json = normalize_tool_arguments ( & call. function . name , & call. function . arguments ) ;
105111 let output = tools
106- . call ( & call. function . name , & call . function . arguments )
112+ . call ( & call. function . name , & args_json )
107113 . await
108114 . with_context ( || format ! ( "tool {}" , call. function. name) ) ?;
109115
@@ -143,3 +149,161 @@ impl AgentRuntime {
143149 driver. chat ( req) . await
144150 }
145151}
152+
153+ #[ derive( Debug , serde:: Deserialize ) ]
154+ struct JsonToolCall {
155+ name : String ,
156+ #[ serde( alias = "args" ) ]
157+ #[ serde( default ) ]
158+ arguments : Option < serde_json:: Value > ,
159+ #[ serde( flatten) ]
160+ extra : serde_json:: Map < String , serde_json:: Value > ,
161+ }
162+
163+ fn normalize_tool_arguments ( tool_name : & str , raw_arguments_json : & str ) -> String {
164+ let Ok ( v) = serde_json:: from_str :: < serde_json:: Value > ( raw_arguments_json) else {
165+ return raw_arguments_json. to_string ( ) ;
166+ } ;
167+
168+ let Some ( obj) = v. as_object ( ) else {
169+ return raw_arguments_json. to_string ( ) ;
170+ } ;
171+
172+ let matches_name = obj
173+ . get ( "function" )
174+ . and_then ( |v| v. as_str ( ) )
175+ . or_else ( || obj. get ( "name" ) . and_then ( |v| v. as_str ( ) ) )
176+ . map ( |name| name == tool_name)
177+ . unwrap_or ( true ) ;
178+ if !matches_name {
179+ return raw_arguments_json. to_string ( ) ;
180+ }
181+
182+ let Some ( inner) = obj. get ( "arguments" ) else {
183+ return raw_arguments_json. to_string ( ) ;
184+ } ;
185+
186+ if let Some ( s) = inner. as_str ( ) {
187+ return s. to_string ( ) ;
188+ }
189+
190+ serde_json:: to_string ( inner) . unwrap_or_else ( |_| raw_arguments_json. to_string ( ) )
191+ }
192+
193+ fn parse_tool_calls_from_json_content ( content : & str ) -> Option < Vec < ToolCall > > {
194+ let trimmed = content. trim ( ) ;
195+ if trimmed. is_empty ( ) {
196+ return None ;
197+ }
198+
199+ if let Ok ( value) = serde_json:: from_str :: < serde_json:: Value > ( trimmed) {
200+ if let Some ( calls) = parse_json_tool_calls_from_value ( value) {
201+ return Some ( into_tool_calls ( calls) ) ;
202+ }
203+ }
204+
205+ let calls = extract_json_tool_calls_from_text ( trimmed) ;
206+ if calls. is_empty ( ) {
207+ return None ;
208+ }
209+ Some ( into_tool_calls ( calls) )
210+ }
211+
212+ fn into_tool_calls ( calls : Vec < JsonToolCall > ) -> Vec < ToolCall > {
213+ let mut out = Vec :: new ( ) ;
214+ for ( idx, call) in calls. into_iter ( ) . enumerate ( ) {
215+ let args_value = call
216+ . arguments
217+ . unwrap_or_else ( || serde_json:: Value :: Object ( call. extra ) ) ;
218+ let args = if let Some ( s) = args_value. as_str ( ) {
219+ s. to_string ( )
220+ } else {
221+ serde_json:: to_string ( & args_value) . unwrap_or_else ( |_| "{}" . to_string ( ) )
222+ } ;
223+ out. push ( ToolCall {
224+ id : format ! ( "call_json_{}" , idx + 1 ) ,
225+ kind : "function" . to_string ( ) ,
226+ function : ToolFunction {
227+ name : call. name ,
228+ arguments : args,
229+ } ,
230+ } ) ;
231+ }
232+ out
233+ }
234+
235+ fn parse_json_tool_calls_from_value ( value : serde_json:: Value ) -> Option < Vec < JsonToolCall > > {
236+ if let Some ( arr) = value. as_array ( ) {
237+ let mut calls = Vec :: new ( ) ;
238+ for item in arr {
239+ calls. push ( serde_json:: from_value :: < JsonToolCall > ( item. clone ( ) ) . ok ( ) ?) ;
240+ }
241+ return Some ( calls) ;
242+ }
243+
244+ serde_json:: from_value :: < JsonToolCall > ( value) . ok ( ) . map ( |c| vec ! [ c] )
245+ }
246+
247+ fn extract_json_tool_calls_from_text ( content : & str ) -> Vec < JsonToolCall > {
248+ let mut calls = Vec :: new ( ) ;
249+ for ( start, _) in content. match_indices ( '{' ) {
250+ if calls. len ( ) >= 16 {
251+ break ;
252+ }
253+ let Some ( end) = find_balanced_json_object_end ( content, start) else {
254+ continue ;
255+ } ;
256+ let slice = & content[ start..end] ;
257+ let Ok ( value) = serde_json:: from_str :: < serde_json:: Value > ( slice) else {
258+ continue ;
259+ } ;
260+ let Some ( mut parsed) = parse_json_tool_calls_from_value ( value) else {
261+ continue ;
262+ } ;
263+ calls. append ( & mut parsed) ;
264+ }
265+ calls
266+ }
267+
268+ fn find_balanced_json_object_end ( s : & str , start : usize ) -> Option < usize > {
269+ let bytes = s. as_bytes ( ) ;
270+ if start >= bytes. len ( ) || bytes[ start] != b'{' {
271+ return None ;
272+ }
273+
274+ let mut depth: i32 = 0 ;
275+ let mut in_string = false ;
276+ let mut escape = false ;
277+
278+ for ( i, & b) in bytes. iter ( ) . enumerate ( ) . skip ( start) {
279+ if in_string {
280+ if escape {
281+ escape = false ;
282+ continue ;
283+ }
284+ if b == b'\\' {
285+ escape = true ;
286+ continue ;
287+ }
288+ if b == b'"' {
289+ in_string = false ;
290+ continue ;
291+ }
292+ continue ;
293+ }
294+
295+ match b {
296+ b'"' => in_string = true ,
297+ b'{' => depth += 1 ,
298+ b'}' => {
299+ depth -= 1 ;
300+ if depth == 0 {
301+ return Some ( i + 1 ) ;
302+ }
303+ }
304+ _ => { }
305+ }
306+ }
307+
308+ None
309+ }
0 commit comments