{"repo":"NousResearch/hermes-agent","public_id":"989","display_label":"DeepSeek V3 parser multi-tool call handling","summary":"These issues and fixes address a bug in the DeepSeek V3 parser where multiple parallel tool calls are dropped, likely due to a greedy regex. The members consistently describe the same parsing defect and proposed corrections.","subsystem":"environments","counts":{"members":5,"open_issues":0,"open_issues_distinct":0,"open_prs":0,"closed_issues":1,"merged_prs":2,"attempt_prs":2},"landed_fix":{"status":"merged_to_main","basis":"structural","pr":1365,"merged_at":"2026-03-15T05:22:45+00:00"},"resolution":{"state":"resolved","by":"pr-1300"},"freshness":{"state":"stale","last_verified_at":"2026-03-15T05:22:45+00:00","is_stale":true},"grouping":{"backbone":"STRUCTURAL_SPANNING","link_density":0.4},"fix_edges":[{"issue_number":989,"pr_number":1300,"tier":"verified","basis":"closing_ref","confidence":0.95,"recoverable_from_timeline":true,"pr_merged_at":"2026-03-14T13:19:29+00:00"},{"issue_number":989,"pr_number":1365,"tier":"verified","basis":"closing_ref","confidence":0.95,"recoverable_from_timeline":true,"pr_merged_at":"2026-03-15T05:22:45+00:00"}],"members":[{"kind":"issue","number":989,"title":"DeepSeek V3 parser drops tool calls when model returns multiple","url":null,"state":"closed"},{"kind":"pr","number":992,"title":"fix: DeepSeek V3 parser drops multi-tool calls (greedy regex)","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#1300","number":1300}],"category":"merged_via","implication":"superseded"},"merged":false},{"kind":"pr","number":1015,"title":"fix: DeepSeek V3 parser drops tool calls when model returns multiple (#989)","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#992","number":992}],"category":"duplicate_of","implication":"duplicate"},"merged":false},{"kind":"pr","number":1300,"title":"fix: DeepSeek V3 parser drops multi-tool calls","url":null,"state":"merged","close_reason":{"category":"merged"},"merged":true,"is_fix_pr":true,"fix_tier":"verified","files_changed":["environments/tool_call_parsers/deepseek_v3_parser.py","tests/test_tool_call_parsers.py"]},{"kind":"pr","number":1365,"title":"fix: support multiple parallel tool calls in DeepSeek V3 parser (#989)","url":null,"state":"merged","close_reason":{"category":"merged"},"merged":true,"is_fix_pr":true,"fix_tier":"verified","files_changed":["environments/tool_call_parsers/deepseek_v3_parser.py"],"fix_hunks":[{"file":"environments/tool_call_parsers/deepseek_v3_parser.py","hunks":["@@ -10,12 +10,13 @@\n     <｜tool▁call▁end｜>\n     <｜tool▁calls▁end｜>\n \n-Based on VLLM's DeepSeekV3ToolParser.extract_tool_calls()\n+Fixes Issue #989: Support for multiple simultaneous tool calls.\n \"\"\"\n \n import re\n import uuid\n-from typing import List, Optional\n+import logging\n+from typing import List, Optional, Tuple\n \n from openai.types.chat.chat_completion_message_tool_call import (\n     ChatCompletionMessageToolCall,","@@ -24,6 +25,7 @@\n \n from environments.tool_call_parsers import ParseResult, ToolCallParser, register_parser\n \n+logger = logging.getLogger(__name__)\n \n @register_parser(\"deepseek_v3\")\n class DeepSeekV3ToolCallParser(ToolCallParser):","@@ -32,45 +34,56 @@ class DeepSeekV3ToolCallParser(ToolCallParser):\n \n     Uses special unicode tokens with fullwidth angle brackets and block elements.\n     Extracts type, function name, and JSON arguments from the structured format.\n+    Ensures all tool calls are captured when the model executes multiple actions.\n     \"\"\"\n \n     START_TOKEN = \"[redacted:secret-assignment]\"\n \n-    # Regex captures: type, function_name, function_arguments\n+    # Updated PATTERN: Using \\s* instead of literal \\n for increased robustness\n+    # against variations in model formatting (Issue #989).\n     PATTERN = re.compile(\n-        r\"<｜tool▁call▁begin｜>(?P<type>.*?)<｜tool▁sep｜>(?P<function_name>.*?)\\n```json\\n(?P<function_arguments>.*?)\\n```<｜tool▁call▁end｜>\",\n+        r\"<｜tool▁call▁begin｜>(?P<type>.*?)<｜tool▁sep｜>(?P<function_name>.*?)\\s*```json\\s*(?P<function_arguments>.*?)\\s*```\\s*<｜tool▁call▁end｜>\",\n         re.DOTALL,\n     )\n \n     def parse(self, text: str) -> ParseResult:\n+        \"\"\"\n+        Parses the input text and extracts all available tool calls.\n+        \"\"\"\n         if self.START_TOKEN not in text:\n             return text, None\n \n         try:\n-            matches = self.PATTERN.findall(text)\n+            # Using finditer to capture ALL tool calls in the sequence\n+            matches = list(self.PATTERN.finditer(text))\n             if not matches:\n                 return text, None\n \n             tool_calls: List[ChatCompletionMessageToolCall] = []\n+            \n             for match in matches:\n-                tc_type, func_name, func_args = match\n+                func_name = match.group(\"function_name\").strip()\n+                func_args = match.group(\"function_arguments\").strip()\n+                \n                 tool_calls.append(\n                     ChatCompletionMessageToolCall(\n                         id=f\"call_{uuid.uuid4().hex[:8]}\",\n                         type=\"function\",\n                         function=Function(\n-                            name=func_name.strip(),\n-                            arguments=func_args.strip(),\n+                            name=func_name,\n+                            arguments=func_args,\n                         ),\n                     )\n                 )\n \n-            if not tool_calls:\n-                return text, None\n+            if tool_calls:\n+                # Content is text before the first tool call block\n+                content_index = text.find(self.START_TOKEN)\n+                content = text[:content_index].strip()\n+                return content if content else None, tool_calls\n \n-            # Content is everything before the tool calls section\n-            content = text[: text.find(self.START_TOKEN)].strip()\n-            return content if content else None, tool_calls\n+            return text, None\n \n-        except Exception:\n+        except Exception as e:\n+            logger.error(f\"Error parsing DeepSeek V3 tool calls: {e}\")\n             return text, None\n"]}]}],"trust":{"default_tier":"verified","includes_suggested":false}}