3333 FastAgentToolCallEvent ,
3434)
3535from mobilerun .agent .fast_agent .xml_parser import (
36+ ToolCallParseStatus ,
3637 ToolResult ,
3738 extract_add_memory ,
3839 format_tool_calls ,
3940 format_tool_results ,
40- parse_tool_calls ,
41+ parse_tool_calls_detailed ,
4142)
4243from mobilerun .agent .usage import get_usage_from_response
4344from mobilerun .agent .utils .chat_utils import limit_history
5960
6061logger = logging .getLogger ("mobilerun" )
6162
63+ _MALFORMED_TOOL_CALL_LIMIT = 3
64+
65+
66+ def _malformed_tool_call_correction (attempt : int ) -> str :
67+ """Build a focused retry instruction without echoing malformed model output."""
68+ return (
69+ "Your previous response contained tool-call markup that could not be parsed "
70+ f"(attempt { attempt } /{ _MALFORMED_TOOL_CALL_LIMIT } ). No tool was executed.\n \n "
71+ "Repeat the intended call using only the ASCII XML tags shown below. Replace "
72+ "the placeholder names and value with the intended tool and arguments. Do not "
73+ "use provider-specific markers, full-width punctuation, or alternate tag names.\n \n "
74+ "<function_calls>\n "
75+ '<invoke name="tool_name">\n '
76+ '<parameter name="parameter_name">value</parameter>\n '
77+ "</invoke>\n "
78+ "</function_calls>"
79+ )
80+
6281
6382class FastAgent (Workflow ):
6483 """Agent that uses XML tool-calling instead of code generation.
@@ -108,6 +127,7 @@ def __init__(
108127
109128 self .system_prompt : ChatMessage | None = None
110129 self .tool_call_counter = 0
130+ self ._consecutive_malformed_tool_calls = 0
111131
112132 # Build tool descriptions and param types from registry
113133 self .tool_descriptions = self .registry .get_tool_descriptions_xml ()
@@ -180,6 +200,7 @@ async def _build_user_prompt(self, goal: str) -> ChatMessage:
180200 async def prepare_chat (self , ctx : Context , ev : StartEvent ) -> FastAgentInputEvent :
181201 """Initialize message history with goal."""
182202 logger .debug ("Preparing chat for task execution..." )
203+ self ._consecutive_malformed_tool_calls = 0
183204
184205 # Get available secrets (only if type_secret is actually in the registry)
185206 if (
@@ -373,7 +394,9 @@ async def handle_llm_input(
373394 response_text = response .message .content
374395
375396 # Parse tool calls from response
376- thought , tool_calls = parse_tool_calls (response_text , self .param_types )
397+ parse_result = parse_tool_calls_detailed (response_text , self .param_types )
398+ thought = parse_result .thought
399+ tool_calls = parse_result .calls
377400
378401 # Extract <add_memory> from thought text and append to unified memory
379402 memory_update = extract_add_memory (thought )
@@ -396,17 +419,63 @@ async def handle_llm_input(
396419 thought = thought ,
397420 code = tool_calls_xml ,
398421 usage = usage ,
422+ tool_call_status = parse_result .status ,
399423 )
400424 ctx .write_event_to_stream (event )
401425 return event
402426
403427 @step
404428 async def handle_llm_output (
405429 self , ctx : Context , ev : FastAgentResponseEvent
406- ) -> FastAgentToolCallEvent | FastAgentInputEvent :
430+ ) -> FastAgentToolCallEvent | FastAgentInputEvent | FastAgentEndEvent :
407431 """Route to execution or request tool call if missing."""
408432 has_tool_calls = ev .code is not None
409433
434+ if ev .tool_call_status == ToolCallParseStatus .MALFORMED and not has_tool_calls :
435+ self ._consecutive_malformed_tool_calls += 1
436+ attempt = self ._consecutive_malformed_tool_calls
437+ logger .warning (
438+ "Malformed tool-call markup detected (%d/%d)" ,
439+ attempt ,
440+ _MALFORMED_TOOL_CALL_LIMIT ,
441+ )
442+
443+ if attempt >= _MALFORMED_TOOL_CALL_LIMIT :
444+ pending = self .shared_state .drain_user_messages ()
445+ if pending :
446+ logger .warning (
447+ "⚠️ Dropping %d external user message(s) at malformed tool-call limit" ,
448+ len (pending ),
449+ )
450+ ctx .write_event_to_stream (
451+ ExternalUserMessageDroppedEvent (
452+ message_ids = [message .id for message in pending ],
453+ reason = "malformed_tool_call_limit_reached" ,
454+ step_number = self .shared_state .step_number ,
455+ )
456+ )
457+ event = FastAgentEndEvent (
458+ success = False ,
459+ reason = (
460+ "Model produced malformed tool-call markup "
461+ f"{ _MALFORMED_TOOL_CALL_LIMIT } consecutive times; stopped to prevent "
462+ "a retry loop. Switch models or verify tool-call protocol compatibility."
463+ ),
464+ tool_call_count = self .tool_call_counter ,
465+ )
466+ ctx .write_event_to_stream (event )
467+ return event
468+
469+ self .shared_state .message_history .append (
470+ ChatMessage (
471+ role = "user" ,
472+ content = _malformed_tool_call_correction (attempt ),
473+ )
474+ )
475+ return FastAgentInputEvent ()
476+
477+ self ._consecutive_malformed_tool_calls = 0
478+
410479 if not ev .thought :
411480 logger .warning ("LLM provided tool calls without reasoning." )
412481 no_thoughts_text = (
0 commit comments