| 572 | int depth = 0; |
| 573 | while (end < arg_text.size()) { |
| 574 | char c = arg_text[end]; |
| 575 | if (c == '(' || c == '[' || c == '{') depth++; |
| 576 | else if (c == ')' || c == ']' || c == '}') { |
| 577 | if (depth == 0) break; |
| 578 | depth--; |
| 579 | } |
| 580 | else if (c == ',' && depth == 0) break; |
| 581 | end++; |
| 582 | } |
| 583 | std::string raw = arg_text.substr(pos, end - pos); |
| 584 | while (!raw.empty() && raw.back() == ' ') raw.pop_back(); |
| 585 | pos = end; |
| 586 | |
| 587 | // Try to parse as JSON literal |
| 588 | try { |
| 589 | out_args[key] = json::parse(raw); |
| 590 | } catch (...) { |
| 591 | out_args[key] = raw; |
| 592 | } |
| 593 | } |
| 594 | } |
| 595 | return true; |
| 596 | } |
| 597 | |
| 598 | // ─── Main parser ──────────────────────────────────────────────────────── |
| 599 | |
| 600 | ToolParseResult parse_tool_calls(const std::string & text, const json & tools) { |
| 601 | ToolParseResult result; |
| 602 | std::vector<Span> removals; |
| 603 | |
| 604 | auto add_call = [&](const std::string & fn_name, const json & args, |
| 605 | size_t start, size_t end) { |
| 606 | if (!tool_allowed(tools, fn_name)) return; |
| 607 | ToolCall tc; |
| 608 | tc.id = generate_call_id(); |
| 609 | tc.name = fn_name; |
| 610 | tc.arguments = args.dump(); |
| 611 | result.tool_calls.push_back(std::move(tc)); |
| 612 | removals.push_back({start, end}); |
| 613 | }; |
| 614 | |
| 615 | // Pattern 8 (Laguna): <tool_call>NAME\n<arg_key>K</arg_key>\n |
| 616 | // <arg_value>V</arg_value>...\n</tool_call>. Values are raw strings or |
| 617 | // JSON (the template emits non-strings via tojson); coerce via the |
| 618 | // declared tool schema like the other patterns. Checked before pattern 1 |
| 619 | // so the shared <tool_call> wrapper is not half-consumed by the Qwen |
| 620 | // regexes. |
| 621 | { |
| 622 | size_t pos = 0; |
| 623 | while ((pos = text.find("<tool_call>", pos)) != std::string::npos) { |
| 624 | const size_t body_start = pos + 11; |
| 625 | const size_t close = text.find("</tool_call>", body_start); |
| 626 | if (close == std::string::npos) break; |
| 627 | const std::string body = text.substr(body_start, close - body_start); |
| 628 | const size_t first_key = body.find("<arg_key>"); |
| 629 | // Only claim bodies in the Laguna shape: bare name then arg tags |
| 630 | // (or a bare name alone for zero-arg calls); leave <function=...> |
| 631 | // bodies to the Qwen patterns below. |