Skip to content

Commit f883360

Browse files
committed
fix: harden JSON misformat handling to prevent infinite loops and crashes
Three interrelated fixes for the misformat handling pipeline: 1. extract_json_object_string: replace rfind with brace-depth tracking that respects string literals and escape sequences, preventing false JSON extraction when model output contains incidental braces. 2. process_tools: add consecutive misformat counter on LoopData with a circuit breaker at 5 attempts, raising HandledException to stop the agent instead of looping indefinitely. 3. validate_tool_request: return early for None (already handled by the misformat path) and raise RepairableException instead of ValueError so the agent can retry instead of crashing. Also improves fw.msg_misformat.md to show the expected JSON format explicitly instead of the vague original one-liner.
1 parent 36f26c1 commit f883360

3 files changed

Lines changed: 61 additions & 14 deletions

File tree

agent.py

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -341,6 +341,7 @@ def __init__(self, **kwargs):
341341
self.params_temporary: dict = {}
342342
self.params_persistent: dict = {}
343343
self.current_tool = None
344+
self.consecutive_misformat = 0
344345

345346
# override values with kwargs
346347
for key, value in kwargs.items():
@@ -943,22 +944,37 @@ async def process_tools(self, msg: str):
943944
type="warning", content=f"{self.agent_name}: {error_detail}"
944945
)
945946
else:
946-
warning_msg_misformat = self.read_prompt("fw.msg_misformat.md")
947+
self.loop_data.consecutive_misformat += 1
948+
warning_msg_misformat = self.read_prompt(
949+
"fw.msg_misformat.md",
950+
attempt=self.loop_data.consecutive_misformat,
951+
max_attempts=5,
952+
)
947953
self.hist_add_warning(warning_msg_misformat)
948954
PrintStyle(font_color="red", padding=True).print(warning_msg_misformat)
949955
self.context.log.log(
950956
type="warning",
951-
content=f"{self.agent_name}: Message misformat, no valid tool request found.",
957+
content=f"{self.agent_name}: Message misformat ({self.loop_data.consecutive_misformat}/5), no valid tool request found.",
952958
)
959+
if self.loop_data.consecutive_misformat >= 5:
960+
raise HandledException(
961+
"Too many consecutive misformat errors (5/5). Stopping to prevent infinite loop."
962+
)
963+
return
964+
965+
# Reset counter on successful tool parse
966+
self.loop_data.consecutive_misformat = 0
953967

954968
@extension.extensible
955969
async def validate_tool_request(self, tool_request: Any):
970+
if tool_request is None:
971+
return # let process_tools handle the misformat case
956972
if not isinstance(tool_request, dict):
957-
raise ValueError("Tool request must be a dictionary")
973+
raise RepairableException("Tool request must be a dictionary")
958974
if not tool_request.get("tool_name") or not isinstance(tool_request.get("tool_name"), str):
959-
raise ValueError("Tool request must have a tool_name (type string) field")
975+
raise RepairableException("Tool request must have a tool_name (type string) field")
960976
if not tool_request.get("tool_args") or not isinstance(tool_request.get("tool_args"), dict):
961-
raise ValueError("Tool request must have a tool_args (type dictionary) field")
977+
raise RepairableException("Tool request must have a tool_args (type dictionary) field")
962978

963979

964980

helpers/extract_tools.py

Lines changed: 26 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -25,14 +25,32 @@ def extract_json_object_string(content):
2525
if start == -1:
2626
return ""
2727

28-
# Find the first '{'
29-
end = content.rfind('}')
30-
if end == -1:
31-
# If there's no closing '}', return from start to the end
32-
return content[start:]
33-
else:
34-
# If there's a closing '}', return the substring from start to end
35-
return content[start:end+1]
28+
# Walk forward from the first '{' tracking brace depth, respecting strings and escapes
29+
depth = 0
30+
in_string = False
31+
escape_next = False
32+
for i in range(start, len(content)):
33+
ch = content[i]
34+
if escape_next:
35+
escape_next = False
36+
continue
37+
if ch == '\\' and in_string:
38+
escape_next = True
39+
continue
40+
if ch == '"':
41+
in_string = not in_string
42+
continue
43+
if in_string:
44+
continue
45+
if ch == '{':
46+
depth += 1
47+
elif ch == '}':
48+
depth -= 1
49+
if depth == 0:
50+
return content[start:i+1]
51+
52+
# No matching '}' found — return from start to end
53+
return content[start:]
3654

3755
def extract_json_string(content):
3856
# Regular expression pattern to match a JSON object

prompts/fw.msg_misformat.md

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,14 @@
1-
You have misformatted your message. Follow system prompt instructions on JSON message formatting precisely.
1+
Your response could not be parsed as a valid JSON tool call. You MUST respond with a single JSON object in this exact format:
2+
3+
```json
4+
{
5+
"thoughts": ["your reasoning here"],
6+
"headline": "Short description of action",
7+
"tool_name": "response",
8+
"tool_args": {
9+
"text": "your message to the user"
10+
}
11+
}
12+
```
13+
14+
Do NOT wrap JSON in markdown code fences. Do NOT use XML-style tool calls. Output ONLY the raw JSON object, nothing else.

0 commit comments

Comments
 (0)