From 0542700f9091ebb08c1a56103b0f0f45f24aa621 Mon Sep 17 00:00:00 2001 From: zhayujie Date: Sat, 7 Feb 2026 20:25:05 +0800 Subject: [PATCH] fix: issues with empty tool calls and handling excessively long tool results --- agent/protocol/agent_stream.py | 27 ++++++++++++++++++++++----- models/linkai/link_ai_bot.py | 21 ++++++++++++++++++++- models/minimax/minimax_bot.py | 10 ++++++++-- models/openai_compatible_bot.py | 32 +++++++++++++++++++++----------- 4 files changed, 71 insertions(+), 19 deletions(-) diff --git a/agent/protocol/agent_stream.py b/agent/protocol/agent_stream.py index e8543070..88c01fee 100644 --- a/agent/protocol/agent_stream.py +++ b/agent/protocol/agent_stream.py @@ -336,7 +336,7 @@ class AgentStreamExecutor: # Build tool result block (Claude format) # Format content in a way that's easy for LLM to understand is_error = result.get("status") == "error" - + if is_error: # For errors, provide clear error message result_content = f"Error: {result.get('result', 'Unknown error')}" @@ -349,7 +349,16 @@ class AgentStreamExecutor: else: # Fallback to full JSON result_content = json.dumps(result, ensure_ascii=False) - + + # Truncate large tool results to prevent message bloat + # Keep tool results under 50KB to avoid context window issues + MAX_TOOL_RESULT_CHARS = 20000 + if len(result_content) > MAX_TOOL_RESULT_CHARS: + truncated_len = len(result_content) + result_content = result_content[:MAX_TOOL_RESULT_CHARS] + \ + f"\n\n[Output truncated: {truncated_len} chars total, showing first {MAX_TOOL_RESULT_CHARS} chars]" + logger.info(f"📎 Truncated tool result for '{tool_call['name']}': {truncated_len} -> {MAX_TOOL_RESULT_CHARS} chars") + tool_result_block = { "type": "tool_result", "tool_use_id": tool_call["id"], @@ -678,6 +687,14 @@ class AgentStreamExecutor: tool_calls = [] for idx in sorted(tool_calls_buffer.keys()): tc = tool_calls_buffer[idx] + + # Ensure tool call has a valid ID (some providers return empty/None IDs) + tool_id = tc.get("id") or "" + if not tool_id: + import uuid + tool_id = f"call_{uuid.uuid4().hex[:24]}" + logger.debug(f"⚠️ Tool call missing ID for '{tc.get('name')}', generated fallback: {tool_id}") + try: # Safely get arguments, handle None case args_str = tc.get("arguments") or "" @@ -690,11 +707,11 @@ class AgentStreamExecutor: logger.error(f"Arguments length: {len(args_str)} chars") logger.error(f"Arguments preview: {args_preview}...") logger.error(f"JSON decode error: {e}") - + # Return a clear error message to the LLM instead of empty dict # This helps the LLM understand what went wrong tool_calls.append({ - "id": tc["id"], + "id": tool_id, "name": tc["name"], "arguments": {}, "_parse_error": f"Invalid JSON in tool arguments: {args_preview}... Error: {str(e)}. Tip: For large content, consider splitting into smaller chunks or using a different approach." @@ -702,7 +719,7 @@ class AgentStreamExecutor: continue tool_calls.append({ - "id": tc["id"], + "id": tool_id, "name": tc["name"], "arguments": arguments }) diff --git a/models/linkai/link_ai_bot.py b/models/linkai/link_ai_bot.py index 28be2d1b..99b62d45 100644 --- a/models/linkai/link_ai_bot.py +++ b/models/linkai/link_ai_bot.py @@ -628,9 +628,28 @@ def _handle_linkai_stream_response(self, base_url, headers, body): break try: chunk = json.loads(line) - yield chunk except json.JSONDecodeError: continue + + # Check for error responses within the stream + # Some providers (e.g., MiniMax via LinkAI) return errors as: + # {'type': 'error', 'error': {'type': '...', 'message': '...', 'http_code': '400'}} + if chunk.get("type") == "error" or ( + isinstance(chunk.get("error"), dict) and "message" in chunk.get("error", {}) + ): + error_data = chunk.get("error", {}) + error_msg = error_data.get("message", "Unknown error") if isinstance(error_data, dict) else str(error_data) + http_code = error_data.get("http_code", "") if isinstance(error_data, dict) else "" + status_code = int(http_code) if http_code and str(http_code).isdigit() else 400 + logger.error(f"[LinkAI] stream error: {error_msg} (http_code={http_code})") + yield { + "error": True, + "message": error_msg, + "status_code": status_code + } + return + + yield chunk except Exception as e: logger.error(f"[LinkAI] stream response error: {e}") diff --git a/models/minimax/minimax_bot.py b/models/minimax/minimax_bot.py index 246978b6..abf0fd1b 100644 --- a/models/minimax/minimax_bot.py +++ b/models/minimax/minimax_bot.py @@ -285,10 +285,16 @@ class MinimaxBot(Bot): text_parts.append(block.get("text", "")) elif block.get("type") == "tool_result": # Tool result should be a separate message with role="tool" + tool_call_id = block.get("tool_use_id") or "" + if not tool_call_id: + logger.warning(f"[MINIMAX] tool_result missing tool_use_id") + result_content = block.get("content", "") + if not isinstance(result_content, str): + result_content = json.dumps(result_content, ensure_ascii=False) tool_results.append({ "role": "tool", - "tool_call_id": block.get("tool_use_id"), - "content": str(block.get("content", "")) + "tool_call_id": tool_call_id, + "content": result_content }) if text_parts: diff --git a/models/openai_compatible_bot.py b/models/openai_compatible_bot.py index 58843540..fd3139d3 100644 --- a/models/openai_compatible_bot.py +++ b/models/openai_compatible_bot.py @@ -233,56 +233,66 @@ class OpenAICompatibleBot: # Separate text content and tool_result blocks text_parts = [] tool_results = [] - + for block in content: if block.get("type") == "text": text_parts.append(block.get("text", "")) elif block.get("type") == "tool_result": tool_results.append(block) - + # First, add tool result messages (must come immediately after assistant with tool_calls) for block in tool_results: + tool_call_id = block.get("tool_use_id") or "" + if not tool_call_id: + logger.warning(f"[OpenAICompatible] tool_result missing tool_use_id, using empty string") + # Ensure content is a string (some providers require string content) + result_content = block.get("content", "") + if not isinstance(result_content, str): + result_content = json.dumps(result_content, ensure_ascii=False) openai_messages.append({ "role": "tool", - "tool_call_id": block.get("tool_use_id"), - "content": block.get("content", "") + "tool_call_id": tool_call_id, + "content": result_content }) - + # Then, add text content as a separate user message if present if text_parts: openai_messages.append({ "role": "user", "content": " ".join(text_parts) }) - + # Check if this is an assistant message with tool_use blocks elif role == "assistant": # Separate text content and tool_use blocks text_parts = [] tool_calls = [] - + for block in content: if block.get("type") == "text": text_parts.append(block.get("text", "")) elif block.get("type") == "tool_use": + tool_id = block.get("id") or "" + if not tool_id: + logger.warning(f"[OpenAICompatible] tool_use missing id for '{block.get('name')}'") tool_calls.append({ - "id": block.get("id"), + "id": tool_id, "type": "function", "function": { "name": block.get("name"), "arguments": json.dumps(block.get("input", {})) } }) - + # Build OpenAI format assistant message openai_msg = { "role": "assistant", "content": " ".join(text_parts) if text_parts else None } - + if tool_calls: openai_msg["tool_calls"] = tool_calls - + openai_messages.append(openai_msg) else: # Other list content, keep as is