| 226 | return None |
| 227 | |
| 228 | def parse_tool_call(text): |
| 229 | # ── Primary: JSON format in <tool> tags ────────────────────────── |
| 230 | match = re.search(r"<tool>\s*(\{.*)", text, re.DOTALL) |
| 231 | if match: |
| 232 | result = extract_json(match.group(1)) |
| 233 | if result and "name" in result: |
| 234 | return result |
| 235 | # Rogue tags: <write_file>{json}</write_file> etc. |
| 236 | for tag, canonical in ROGUE_TAG_MAP.items(): |
| 237 | match = re.search(r"<" + tag + r">\s*(\{.*)", text, re.DOTALL) |
| 238 | if match: |
| 239 | inner = extract_json(match.group(1)) |
| 240 | if inner: |
| 241 | if "name" in inner: |
| 242 | return inner |
| 243 | return {"name": canonical, "args": inner} |
| 244 | |
| 245 | # ── Fallback: block-style tags (no JSON escaping) ──────────────── |
| 246 | # <write_file path="...">...code...</write_file> |
| 247 | m = re.search(r'<write_file\s+path="([^"]+)">\s*\n?(.*?)(?:</write_file>|\Z)', text, re.DOTALL) |
| 248 | if m and m.group(2).strip(): |
| 249 | return {"name": "write_file", "args": {"path": m.group(1), "content": m.group(2).strip()}} |
| 250 | |
| 251 | return None |
| 252 | |
| 253 | def _format_tool_for_history(tool_dict): |
| 254 | """Format a tool call for conversation history.""" |