AttributeError crashes when content or args are dicts/lists

NousResearch/hermes-agent · cluster 759 · agent

Data as of

Nothing open on GitHub. Fix merged in #1440. Check it covers your case.

The short answer

0open issues
0open PRs
19items grouped
Known?
Yes. 19 related items grouped here, 4 issues already closed.
Fix event
A fix was merged to main in PR #1440. Check it covers your case. No release data is tracked.
Tracker
The maintainers' tracker marks this partially resolved, deciding PR #63755. That is separate from the fix event.
Still open
Nothing open on GitHub.

Multiple bugs and fixes related to code paths that assume string types but receive dicts or lists instead, particularly in streaming content deltas, tool call arguments, and compression/summarization logic. Affects various backends including Mistral, NVIDIA, llama.cpp, and Ollama.

Open issues0

No open issues.

Open PRs0

No open pull requests.

Fixes on record

Merged to main. A stamp counts the issues a fix is linked to.

  • #1440 fix: handle dict tool call arguments from local backends

    merged to main

    files: run_agent.py, tests/test_dict_tool_call_args.py

  • #63755 fix(agent): normalize list-shaped streaming content deltas

    merged to main

    files: agent/chat_completion_helpers.py, tests/run_agent/test_streaming.py

  • #64772 fix: display layers never crash on non-string tool-call arguments

    merged to main

    files: acp_adapter/tools.py, agent/context_compressor.py, agent/display.py, scripts/release.py, tests/acp/test_tools.py, tests/agent/test_summarize_tool_result_type_safety.py

    Show the diff
    • acp_adapter/tools.py
      @@ -3,6 +3,7 @@
       from __future__ import annotations
       
       import json
      +import logging
       import uuid
       from typing import Any, Dict, List, Optional
       
      @@ -14,6 +15,8 @@
           ToolKind,
       )
       
      +logger = logging.getLogger(__name__)
      +
       # ---------------------------------------------------------------------------
       # Map hermes tool names -> ACP ToolKind
       # ---------------------------------------------------------------------------
      @@ -1026,7 +1029,37 @@ def build_tool_start(
           *,
           edit_diff: Any = None,
       ) -> ToolCallStart:
      -    """Create a ToolCallStart event for the given hermes tool invocation."""
      +    """Create a ToolCallStart event for the given hermes tool invocation.
      +
      +    A malformed tool argument (e.g. a non-string ``command``/``path`` from a
      +    model that ignores the schema) must never abort the ACP tool-call render —
      +    ``build_tool_start`` runs on the live tool-progress callback and during
      +    session history replay. On any failure in the title/content/location
      +    builders, fall back to a minimal, valid start event. Mirrors
      +    ``get_cute_tool_message`` in ``agent/display.py``, wrapped for the same
      +    reason on the CLI side.
      +    """
      +    try:
      +        return _build_tool_start(
      +            tool_call_id, tool_name, arguments, edit_diff=edit_diff
      +        )
      +    except Exception as exc:  # noqa: BLE001 — a tool-call render must never abort the turn
      +        logger.debug("ACP tool-start render failed for %r: %s", tool_name, exc)
      +        safe_name = tool_name if isinstance(tool_name, str) and tool_name else "tool"
      +        return acp.start_tool_call(
      +            tool_call_id, safe_name, kind=get_tool_kind(safe_name),
      +            content=None, locations=[], raw_input=None,
      +        )
      +
      +
      +def _build_tool_start(
      +    tool_call_id: str,
      +    tool_name: str,
      +    arguments: Dict[str, Any],
      +    *,
      +    edit_diff: Any = None,
      +) -> ToolCallStart:
      +    """Build the ToolCallStart event (unguarded; see ``build_tool_start``)."""
           kind = get_tool_kind(tool_name)
           title = build_tool_title(tool_name, arguments)
           locations = extract_locations(arguments)
    • agent/context_compressor.py
      @@ -586,6 +586,21 @@ def _strip_historical_media(messages: List[Dict[str, Any]]) -> List[Dict[str, An
           return result if changed else messages
       
       
      +def _str_arg(args: dict, key: str, default: str = "") -> str:
      +    """Safely get a string argument from parsed tool args.
      +
      +    LLMs sometimes return non-string parameter values (e.g. bool, int) for
      +    tool calls.  Calling ``len()`` / ``.count()`` / slicing on those causes
      +    ``TypeError`` / ``AttributeError`` which crashes context compression.
      +    This helper coerces any value to ``str`` so downstream code can assume
      +    a string is always returned.
      +    """
      +    val = args.get(key, default)
      +    if isinstance(val, str):
      +        return val
      +    return str(val) if val is not None else default
      +
      +
       def _summarize_tool_result(tool_name: str, tool_args: str, tool_content: str) -> str:
           """Create an informative 1-line summary of a tool call + result.
       
      @@ -598,18 +613,37 @@ def _summarize_tool_result(tool_name: str, tool_args: str, tool_content: str) ->
               [terminal] ran `npm test` -> exit 0, 47 lines output
               [read_file] read config.py from line 1 (1,200 chars)
               [search_files] content search for 'compress' in agent/ -> 12 matches
      +
      +    Never raises: models sometimes emit non-string argument values (bool,
      +    int, None) and the args here come from persisted session history, so a
      +    single malformed historical call must not crash compression — which
      +    retries on the same history and would crash-loop. Individual branches
      +    coerce the values they slice/measure (keeping summaries informative);
      +    this wrapper is the backstop for anything they miss.
           """
      +    try:
      +        return _summarize_tool_result_unguarded(tool_name, tool_args, tool_content)
      +    except Exception as exc:  # noqa: BLE001 — a summary must never crash compression
      +        logger.debug("Tool-result summary failed for %s: %s", tool_name, exc)
      +        _len = len(tool_content) if isinstance(tool_content, str) else 0
      +        return f"[{tool_name}] ({_len:,} chars result)"
      +
      +
      +def _summarize_tool_result_unguarded(tool_name: str, tool_args: str, tool_content: str) -> str:
      +    """Build the summary line (unguarded; see ``_summarize_tool_result``)."""
           try:
               args = json.loads(tool_args) if tool_args else {}
           except (json.JSONDecodeError, TypeError):
               args = {}
      +    if not isinstance(args, dict):
      +        args = {}
       
           content = tool_content or ""
           content_len = len(content)
           line_count = content.count("\n") + 1 if content.strip() else 0
       
           if tool_name == "terminal":
      -        cmd = args.get("command", "")
      +        cmd = _str_arg(args, "command")
               if len(cmd) > 80:
                   cmd = cmd[:77] + "..."
               exit_match = re.search(r'"exit_code"\s*:\s*(-?\d+)', content)
      @@ -623,7 +657,7 @@ def _summarize_tool_result(tool_name: str, tool_args: str, tool_content: str) ->
       
           if tool_name == "write_file":
               path = args.get("path", "?")
      -        written_lines = args.get("content", "").count("\n") + 1 if args.get("content") else "?"
      +        written_lines = _str_arg(args, "content").count("\n") + 1 if args.get("content") else "?"
               return f"[write_file] wrote to {path} ({written_lines} lines)"
       
           if tool_name == "search_files":
      @@ -658,14 +692,15 @@ def _summarize_tool_result(tool_name: str, tool_args: str, tool_content: str) ->
               return f"[web_extract] {url_desc} ({content_len:,} chars)"
       
           if tool_name == "delegate_task":
      -        goal = args.get("goal", "")
      +        goal = _str_arg(args, "goal")
               if len(goal) > 60:
                   goal = goal[:57] + "..."
               return f"[delegate_task] '{goal}' ({content_len:,} chars result)"
       
           if tool_name == "execute_code":
      -        code_preview = (args.get("code") or "")[:60].replace("\n", " ")
      -        if len(args.get("code", "")) > 60:
      +        code_str = _str_arg(args, "code")
      +        code_preview = code_str[:60].replace("\n", " ")
      +        if len(code_str) > 60:
                   code_preview += "..."
               return f"[execute_code] `{code_preview}` ({line_count} lines output)"
       
      @@ -674,7 +709,7 @@ def _summarize_tool_result(tool_name: str, tool_args: str, tool_content: str) ->
               return f"[{tool_name}] name={name} ({content_len:,} chars)"
       
           if tool_name == "vision_analyze":
      -        question = args.get("question", "")[:50]
      +        question = _str_arg(args, "question")[:50]
               return f"[vision_analyze] '{question}' ({content_len:,} chars)"
       
           if tool_name == "memory":
    • agent/display.py
      @@ -462,13 +462,14 @@ def build_tool_preview(tool_name: str, args: dict, max_len: int | None = None) -
               sid = args.get("session_id", "")
               data = args.get("data", "")
               timeout_val = args.get("timeout")
      -        parts = [action]
      +        parts = [str(action) if action else ""]
               if sid:
      -            parts.append(sid[:16])
      +            parts.append(str(sid)[:16])
               if data:
      -            parts.append(f'"{_oneline(data[:20])}"')
      +            parts.append(f'"{_oneline(str(data)[:20])}"')
               if timeout_val and action == "wait":
                   parts.append(f"{timeout_val}s")
      +        parts = [p for p in parts if p]
               return " ".join(parts) if parts else None
       
           if tool_name == "todo":
    • scripts/release.py
      @@ -332,6 +332,8 @@
           "[redacted:email]": "synapsesx",
           "[redacted:email]": "itsflownium",
           "[redacted:email]": "dirtyren",
      +    "[redacted:email]": "liuwei666888",
      +    "[redacted:email]": "liuwei666888",
           "[redacted:email]": "justinschille",
           "[redacted:email]": "unsupportedpastels",
           "[redacted:email]": "John-Lussier",
    • tests/acp/test_tools.py
      @@ -227,6 +227,26 @@ def test_build_tool_start_for_read_file(self):
               assert result.content is None
               assert result.raw_input is None
       
      +    def test_build_tool_start_survives_non_string_command(self):
      +        """A malformed (non-string) terminal command previously raised
      +        TypeError in build_tool_title (len(None)) and aborted the render."""
      +        result = build_tool_start("tc-bad-cmd", "terminal", {"command": None})
      +        assert isinstance(result, ToolCallStart)
      +        assert result.kind == "execute"  # tool identity preserved in the fallback
      +
      +    def test_build_tool_start_survives_non_string_path(self):
      +        """A non-string read_file path previously raised a ToolCallLocation
      +        pydantic ValidationError in extract_locations and aborted the render."""
      +        result = build_tool_start("tc-bad-path", "read_file", {"path": {"p": "x"}})
      +        assert isinstance(result, ToolCallStart)
      +        assert result.kind == "read"
      +
      +    def test_build_tool_start_survives_non_string_goal(self):
      +        """A non-string delegate_task goal previously raised TypeError
      +        (len(123)) in build_tool_title and aborted the render."""
      +        result = build_tool_start("tc-bad-goal", "delegate_task", {"goal": 123})
      +        assert isinstance(result, ToolCallStart)
      +
           def test_build_tool_start_for_web_extract_is_compact(self):
               """web_extract start should stay compact; title identifies URLs."""
               args = {"urls": ["https://example.com/docs"]}
    • tests/agent/test_summarize_tool_result_type_safety.py
      @@ -0,0 +1,286 @@
      +"""Type safety tests for _summarize_tool_result.
      +
      +When LLMs return non-string parameter values (e.g. bool, int, None) in tool
      +call arguments, _summarize_tool_result() must not crash with TypeError or
      +AttributeError. This caused an infinite TUI crash loop in production.
      +"""
      +import json
      +import pytest
      +from agent.context_compressor import _summarize_tool_result
      +
      +
      +class TestTypeSafety:
      +    """Non-string tool arguments must not crash _summarize_tool_result."""
      +
      +    def test_terminal_command_bool(self):
      +        """bool value for 'command' should not raise TypeError."""
      +        args = json.dumps({"command": True})
      +        result = _summarize_tool_result("terminal", args, '{"exit_code": 0}')
      +        assert "terminal" in result
      +        assert "True" in result or "exit" in result
      +
      +    def test_terminal_command_int(self):
      +        """int value for 'command' should not raise TypeError."""
      +        args = json.dumps({"command": 42})
      +        result = _summarize_tool_result("terminal", args, '{"exit_code": 0}')
      +        assert "terminal" in result
      +        assert "42" in result
      +
      +    def test_terminal_command_none(self):
      +        """None value for 'command' should not raise TypeError."""
      +        args = json.dumps({"command": None})
      +        result = _summarize_tool_result("terminal", args, '{"exit_code": 0}')
      +        assert "terminal" in result
      +
      +    def test_write_file_content_bool(self):
      +        """bool value for 'content' should not raise AttributeError."""
      +        args = json.dumps({"path": "test.txt", "content": False})
      +        result = _summarize_tool_result("write_file", args, "OK")
      +        assert "write_file" in result
      +        assert "test.txt" in result
      +
      +    def test_write_file_content_int(self):
      +        """int value for 'content' should not raise AttributeError."""
      +        args = json.dumps({"path": "test.txt", "content": 123})
      +        result = _summarize_tool_result("write_file", args, "OK")
      +        assert "write_file" in result
      +
      +    def test_delegate_task_goal_bool(self):
      +        """bool value for 'goal' should not raise TypeError."""
      +        args = json.dumps({"goal": False})
      +        result = _summarize_tool_result("delegate_task", args, "result")
      +        assert "delegate_task" in result
      +        assert "False" in result
      +
      +    def test_delegate_task_goal_int(self):
      +        """int value for 'goal' should not raise TypeError."""
      +        args = json.dumps({"goal": 999})
      +        result = _summarize_tool_result("delegate_task", args, "result")
      +        assert "delegate_task" in result
      +        assert "999" in result
      +
      +    def test_execute_code_code_bool(self):
      +        """bool value for 'code' should not raise TypeError."""
      +        args = json.dumps({"code": True})
      +        result = _summarize_tool_result("execute_code", args, "output")
      +        assert "execute_code" in result
      +        assert "True" in result
      +
      +    def test_execute_code_code_int(self):
      +        """int value for 'code' should not raise TypeError."""
      +        args = json.dumps({"code": 0})
      +        result = _summarize_tool_result("execute_code", args, "output")
      +        assert "execute_code" in result
      +
      +    def test_vision_analyze_question_bool(self):
      +        """bool value for 'question' should not raise TypeError."""
      +        args = json.dumps({"question": True})
      +        result = _summarize_tool_result("vision_analyze", args, "analysis")
      +        assert "vision_analyze" in result
      +        assert "True" in result
      +
      +    def test_vision_analyze_question_int(self):
      +        """int value for 'question' should not raise TypeError."""
      +        args = json.dumps({"question": 123})
      +        result = _summarize_tool_result("vision_analyze", args, "analysis")
      +        assert "vision_analyze" in result
      +        assert "123" in result
      +
      +    def test_vision_analyze_question_list(self):
      +        """list value for 'question' should not raise TypeError."""
      +        args = json.dumps({"question": ["a", "b"]})
      +        result = _summarize_tool_result("vision_analyze", args, "analysis")
      +        assert "vision_analyze" in result
      +
      +    def test_vision_analyze_question_dict(self):
      +        """dict value for 'question' should not raise TypeError."""
      +        args = json.dumps({"question": {"key": "value"}})
      +        result = _summarize_tool_result("vision_analyze", args, "analysis")
      +        assert "vision_analyze" in result
      +
      +
      +class TestNormalStringArguments:
      +    """Normal string arguments should continue to work as before."""
      +
      +    def test_terminal_normal_command(self):
      +        """Normal string command should be summarized correctly."""
      +        args = json.dumps({"command": "ls -la"})
      +        result = _summarize_tool_result("terminal", args, '{"exit_code": 0}')
      +        assert "terminal" in result
      +        assert "ls -la" in result
      +        assert "exit 0" in result
      +
      +    def test_terminal_long_command_truncated(self):
      +        """Long commands should be truncated."""
      +        long_cmd = "a" * 100
      +        args = json.dumps({"command": long_cmd})
      +        result = _summarize_tool_result("terminal", args, '{"exit_code": 0}')
      +        assert "..." in result
      +        assert len(result) < 150
      +
      +    def test_write_file_normal_content(self):
      +        """Normal string content should count lines correctly."""
      +        args = json.dumps({"path": "test.py", "content": "line1\nline2\nline3"})
      +        result = _summarize_tool_result("write_file", args, "OK")
      +        assert "write_file" in result
      +        assert "test.py" in result
      +        assert "3 lines" in result
      +
      +    def test_delegate_task_normal_goal(self):
      +        """Normal string goal should be summarized correctly."""
      +        args = json.dumps({"goal": "Fix the bug"})
      +        result = _summarize_tool_result("delegate_task", args, "done")
      +        assert "delegate_task" in result
      +        assert "Fix the bug" in result
      +
      +    def test_delegate_task_long_goal_truncated(self):
      +        """Long goals should be truncated."""
      +        long_goal = "x" * 100
      +        args = json.dumps({"goal": long_goal})
      +        result = _summarize_tool_result("delegate_task", args, "done")
      +        assert "..." in result
      +
      +    def test_execute_code_normal_code(self):
      +        """Normal code should be previewed correctly."""
      +        args = json.dumps({"code": "print('hello')"})
      +        result = _summarize_tool_result("execute_code", args, "hello")
      +        assert "execute_code" in result
      +        assert "print" in result
      +
      +    def test_execute_code_long_code_truncated(self):
      +        """Long code should be truncated."""
      +        long_code = "a = 1\n" * 20
      +        args = json.dumps({"code": long_code})
      +        result = _summarize_tool_result("execute_code", args, "output")
      +        assert "..." in result
      +
      +    def test_vision_analyze_normal_question(self):
      +        """Normal question should be included."""
      +        args = json.dumps({"question": "What is this?"})
      +        result = _summarize_tool_result("vision_analyze", args, "It's a cat")
      +        assert "vision_analyze" in result
      +        assert "What is this?" in result
      +
      +    def test_vision_analyze_long_question_truncated(self):
      +        """Long questions should be truncated."""
      +        long_q = "?" * 100
      +        args = json.dumps({"question": long_q})
      +        result = _summarize_tool_result("vision_analyze", args, "answer")
      +        assert len(result) < 150
      +
      +
      +class TestEdgeCases:
      +    """Edge cases and boundary conditions."""
      +
      +    def test_empty_args(self):
      +        """Empty JSON object should not crash."""
      +        result = _summarize_tool_result("terminal", "{}", "output")
      +        assert "terminal" in result
      +
      +    def test_invalid_json(self):
      +        """Invalid JSON should not crash."""
      +        result = _summarize_tool_result("terminal", "not json", "output")
      +        assert "terminal" in result
      +
      +    def test_null_args(self):
      +        """None/null args should not crash."""
      +        result = _summarize_tool_result("terminal", None, "output")
      +        assert "terminal" in result
      +
      +    def test_non_dict_json_args(self):
      +        """Args that parse to a non-dict (list/scalar) should not crash."""
      +        result = _summarize_tool_result("terminal", json.dumps([1, 2]), "output")
      +        assert "terminal" in result
      +        result = _summarize_tool_result("terminal", json.dumps("bare"), "output")
      +        assert "terminal" in result
      +
      +    def test_unknown_tool_name(self):
      +        """Unknown tool name should return generic summary."""
      +        args = json.dumps({"foo": "bar"})
      +        result = _summarize_tool_result("unknown_tool", args, "output")
      +        # Should return some fallback, not crash
      +        assert isinstance(result, str)
      +
      +    def test_mixed_types_in_args(self):
      +        """Args with mixed types should not crash."""
      +        args = json.dumps({
      +            "command": "ls",
      +            "background": True,
      +            "timeout": 30,
      +            "extra": None
      +        })
      +        result = _summarize_tool_result("terminal", args, '{"exit_code": 0}')
      +        assert "terminal" in result
      +        assert "ls" in result
      +
      +
      +class TestBackstopWrapper:
      +    """The outer guard: NO input shape may raise out of _summarize_tool_result.
      +
      +    Compression retries on the same persisted history, so an escaping
      +    exception here becomes a crash loop. The wrapper returns a minimal
      +    '[tool] (N chars result)' summary when a branch fails.
      +    """
      +
      +    def test_never_raises_matrix(self):
      +        """Fuzz the per-tool branches with hostile value shapes."""
      +        hostile_values = [None, True, 42, 3.14, ["a"], {"k": "v"}]
      +        tools = [
      +            "terminal", "read_file", "write_file", "search_files", "patch",
      +            "browser_navigate", "web_search", "web_extract", "delegate_task",
      +            "execute_code", "skill_view", "vision_analyze", "memory",
      +            "cronjob", "process", "totally_unknown_tool",
      +        ]
      +        keys = ["command", "path", "content", "pattern", "url", "query",
      +                "urls", "goal", "code", "name", "question", "action",
      +                "target", "session_id", "mode", "offset", "ref"]
      +        for tool in tools:
      +            for value in hostile_values:
      +                args = json.dumps({k: value for k in keys})
      +                result = _summarize_tool_result(tool, args, "x" * 250)
      +                assert isinstance(result, str) and result, (tool, value)
      +
      +    def test_backstop_fallback_shape(self):
      +        """When a branch does fail, the fallback names the tool and size."""
      +        from unittest.mock import patch as _patch
      +        with _patch(
      +            "agent.context_compressor._summarize_tool_result_unguarded",
      +            side_effect=TypeError("boom"),
      +        ):
      +            result = _summarize_tool_result("terminal", "{}", "y" * 300)
      +        assert result == "[terminal] (300 chars result)"
      +
      +    def test_backstop_handles_non_string_content(self):
      +        from unittest.mock import patch as _patch
      +        with _patch(
      +            "agent.context_compressor._summarize_tool_result_unguarded",
      +            side_effect=TypeError("boom"),
      +        ):
      +            result = _summarize_tool_result("terminal", "{}", None)
      +        assert result == "[terminal] (0 chars result)"
      +
      +
      +class TestDisplayPreviewTypeSafety:
      +    """Sibling site: agent/display.py previews run on the live
      +    tool-progress callback and crashed on non-string process args."""
      +
      +    def test_process_preview_non_string_session_id(self):
      +        from agent.display import build_tool_preview
      +        assert build_tool_preview("process", {"action": "poll", "session_id": 123}) == "poll 123"
      +
      +    def test_process_preview_non_string_data(self):
      +        from agent.display import build_tool_preview
      +        result = build_tool_preview(
      +            "process", {"action": "submit", "session_id": "abc", "data": 42}
      +        )
      +        assert result == 'submit abc "42"'
      +
      +    def test_process_preview_none_action(self):
      +        from agent.display import build_tool_preview
      +        result = build_tool_preview("process", {"action": None, "session_id": "abc"})
      +        assert isinstance(result, str)
      +
      +    def test_process_label_non_string_session_id(self):
      +        from agent.display import build_tool_label
      +        result = build_tool_label("process", {"action": "poll", "session_id": 123})
      +        assert isinstance(result, str)
  • #104994 fix: structured reasoning no longer breaks chat consumers (#104711)

    merged to main

    files: agent/agent_runtime_helpers.py, agent/auxiliary_client.py, agent/reasoning_summaries.py, contributors/emails/[redacted:email], contributors/emails/[redacted:email], evals/providers/reasoning_shapes.py, tests/agent/test_reasoning_shape_boundaries.py, website/docs/developer-guide/provider-runtime.md

11 PRs closed without merging

From the close comments: duplicate 3 · closed without comment 2 · done elsewhere 2 · superseded by another PR 2 · merged via another change 1 · partial fix 1

  • #1076 fix: handle dict tool call arguments from llama.cpp/Ollama backends · closed without comment
  • #1080 fix: handle dict tool call arguments from llama.cpp/Ollama backends · closed without comment
  • #1081 fix: handle dict tool call arguments from llama.cpp/Ollama backends · merged via another change → #1440
  • #1210 fix: handle dict arguments in tool call validation · superseded by another PR → #1440
  • #58333 fix(agent): harden compressor tool arg summaries · done elsewhere → #64772, #52431, #58317
Show 6 more
  • #58442 fix(compression): prevent AttributeError crash on dict tool arguments in _summarize_tool_result (#58317) · duplicate
  • #58595 fix(compression): handle dict/list content in write_file summarizer · superseded by another PR → #64772, #52431
  • #62168 fix(compressor): handle dict content in _summarize_tool_result · duplicate → #58333, #58317, #52431
  • #64047 fix(agent): normalize list-type delta.content on streaming follow-up · duplicate → #63755
  • #85791 fix(agent): flatten list-typed delta.content/reasoning in auxiliary s… · done elsewhere → 37fb7ad, 3bc8395, #104711, #104994, #104848, #104717
  • #94905 fix(agent): tolerate list-form delta.content in streaming (Mistral/GLM) · partial fix → #63755, #63734, #94905, #85791
Closed issues · 4
  • Closed#759[Bug]: AttributeError "dict' object has no attribute 'strip" when using OpenAI endpoint
  • Closed#1071[Bug]: Error during OpenAI-compatible API call #1: 'dict' object has no attribute 'strip'
  • Closed#58317Compression crash: AttributeError 'dict' object has no attribute 'count' in _summarize_tool_result
  • Closed#63734AttributeError: 'list' object has no attribute 'strip' during streaming follow-up after a tool call (Mistral / NVIDIA custom provider)
How this cluster was grouped
  • Link evidence (partly structural links): some members are connected by structural evidence (shared changed files, or a closing/superseded reference); the rest are joined by an automated relevance judgement over their text.
  • Members outside that structural core may belong to a different underlying problem.
  • Pairwise link density 0.09: the share of item pairs that carry a link, counting a structural link in full and a relevance-only link at 0.36. 1.00 means every pair is linked.

Pick up a Lab task

Check whether a fix covers the open issues here, with your own agent. Bring back evidence for maintainers.

Open the Lab

At a glance

Subsystem
agent
Merged PRs
#1440, #63755, #64772, #104994
Closed issues
4
Attempts
11 PRs closed without merging
Fix link checked
7 Sep 2026

For your agent

The same facts, typed.