hermes - ✅(Solved) Fix [Bug]: custom-tools plugin tool results cause KeyError: slice(None, 500, None) in _detect_tool_failure [3 pull requests, 1 comments, 1 participants]

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…
GitHub stats
NousResearch/hermes-agent#19814Fetched 2026-05-05 06:05:04
View on GitHub
Comments
1
Participants
1
Timeline
9
Reactions
0
Author
Participants
Timeline (top)
labeled ×4cross-referenced ×3commented ×1subscribed ×1

Error Message

KeyError: slice(None, 500, None)

Root Cause

The root cause is in agent/display.py:830:

Fix Action

Fixed

PR fix notes

PR #19826: fix(agent): handle non-string results in _detect_tool_failure

Description (problem / solution / changelog)

Summary

_detect_tool_failure() assumes its result argument is always a str and unconditionally slices it with result[:500].lower(). When custom-tools plugin handlers return a dict instead of a string, Python evaluates dict[:500] as dict.__getitem__(slice(None, 500, None)), raising KeyError.

Root Cause

In agent/display.py:830, the generic heuristic path does:

lower = result[:500].lower()

with no type guard. The _invoke_toolregistry.dispatch chain can return non-string values when plugin tool handlers return dicts.

Additionally, run_agent.py has multiple function_result[:200] slicing calls in logging paths that would also crash on dict results.

Fix

  1. agent/display.py — Add isinstance(result, str) guard at the top of _detect_tool_failure(). Non-string results are converted to JSON string via json.dumps() before the existing heuristics run.

  2. run_agent.py — Guard all function_result[:200] and function_result[:self.log_prefix_chars] slicing with isinstance checks, converting to str() first.

Regression Coverage

12 tests in tests/agent/test_detect_tool_failure.py:

  • String results (success, error, "failed" keyword) — existing behavior preserved
  • None result — existing behavior preserved
  • dict result (success, error, empty) — the regression case
  • list and int results — edge cases
  • Terminal and memory tool dict results — tool-specific paths
  • get_cute_tool_message with dict result — integration test

Fixes [Bug]: custom-tools plugin tool results cause KeyError: slice(None, 500, None) in _detect_tool_failure #19814

Changed files

  • agent/display.py (modified, +10/-0)
  • gateway/platforms/whatsapp.py (modified, +4/-1)
  • run_agent.py (modified, +13/-8)
  • tests/agent/test_detect_tool_failure.py (added, +98/-0)
  • tests/tools/test_file_tools.py (modified, +37/-0)
  • tools/file_tools.py (modified, +6/-6)

PR #19877: fix(agent): handle non-string tool results from custom-tools plugins

Description (problem / solution / changelog)

Coerce non-string tool results in three layers so custom-tools plugin tools that return dicts no longer crash the agent or get rejected by OpenAI-compatible providers.

What changed and why

  • agent/display.py:_detect_tool_failureresult[:500] on a dict raised KeyError(slice(None, 500, None)), which run_agent then surfaced as the misleading message "Error during OpenAI-compatible API call #N: slice(None, 500, None)". Now coerces dict/list results via json.dumps so the "error"/"failed" heuristic still matches structured payloads.
  • tools/delegate_tool.py — same content[:80] slice in the sub-agent tool_trace builder (_run_single_child). Coerces non-string content before slicing and len().
  • agent/transports/chat_completions.py:convert_messages — non-string tool message content was forwarded as-is to the API, returning HTTP 400 messages.X.content: Invalid input from OpenRouter / DeepSeek / Ollama / NVIDIA NIM / xAI / Kimi etc. Now serializes dict/list content with json.dumps, mirroring the existing handling in agent/anthropic_adapter.py:1525 and agent/bedrock_adapter.py.

All three paths shared the same assumption ("tool result is always str") and the same fix shape, so they are bundled.

How to test

  • pytest tests/agent/test_display.py tests/agent/transports/test_chat_completions.py tests/tools/test_delegate.py -q (all green; new tests cover dict/list/None tool results in each layer).
  • New unit tests:
    • TestDetectToolFailure in tests/agent/test_display.py — dict/list/None tool results no longer crash; dicts containing {"error": ...} are still detected as failures.
    • test_convert_messages_serializes_dict_tool_content in tests/agent/transports/test_chat_completions.py — dict content is JSON-serialized; string content is left untouched (no copy).
    • test_tool_trace_handles_non_string_content in tests/tools/test_delegate.py — sub-agent observability builder no longer crashes on dict tool content.

What platforms tested on

  • macOS on darwin-arm64 (local)

Fixes #19814

<!-- autocontrib:worker-id=issue-new-69d3113e kind=pr-open -->

Changed files

  • agent/display.py (modified, +23/-5)
  • agent/transports/chat_completions.py (modified, +18/-0)
  • tests/agent/test_display.py (modified, +38/-0)
  • tests/agent/transports/test_chat_completions.py (modified, +28/-0)
  • tests/tools/test_delegate.py (modified, +34/-0)
  • tools/delegate_tool.py (modified, +5/-2)

PR #20024: fix: normalize non-string tool results to prevent KeyError on slicing

Description (problem / solution / changelog)

Summary

Custom-tools plugins can return dict results, but handle_function_call() and downstream consumers (_detect_tool_failure, delegate tracing) assume str. When a dict reaches result[:500], Python evaluates dict.__getitem__(slice(...)) and raises KeyError.

Changes

  • model_tools.py — Normalize non-string results to JSON at the return point of handle_function_call(), matching the function's -> str type annotation.
  • agent/display.py — Add defensive isinstance(result, str) guard in _detect_tool_failure() before string slicing.
  • tools/delegate_tool.py — Add defensive isinstance(content, str) guard in the sub-agent trace builder before slicing.

Verification

  • All three files pass py_compile
  • No new ruff lint issues introduced
  • 166 relevant tests pass (tests/agent/, tests/run_agent/ filtered by tool keywords)
  • Pre-existing test failures in test_delegate.py (credential resolution) confirmed identical on main

Closes #19814

Changed files

  • agent/display.py (modified, +2/-0)
  • model_tools.py (modified, +2/-0)
  • tools/delegate_tool.py (modified, +2/-0)

Code Example

lower = result[:500].lower()

---

KeyError: slice(None, 500, None)

---

File "/usr/local/lib/python3.12/site-packages/run_agent.py", line 11323, in run_conversation
    self._execute_tool_calls(assistant_message, messages, effective_task_id, api_call_count)
File "/usr/local/lib/python3.12/site-packages/run_agent.py", line 7646, in _execute_tool_calls
    return self._execute_tool_calls_sequential(
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/usr/local/lib/python3.12/site-packages/run_agent.py", line 8377, in _execute_tool_calls_sequential
    _is_error_result, _ = _detect_tool_failure(function_name, function_result)
                          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/usr/local/lib/python3.12/site-packages/agent/display.py", line 830, in _detect_tool_failure
    lower = result[:500].lower()
            ~~~~~~^^^^^^
KeyError: slice(None, 500, None)

---

Error during OpenAI-compatible API call #N: slice(None, 500, None)

---

# Before:
lower = result[:500].lower()

# After:
result_str = result if isinstance(result, str) else str(result)
lower = result_str[:500].lower()
RAW_BUFFERClick to expand / collapse

Bug Description

When using tools provided by the custom-tools plugin (e.g., read_mail, read_mail_since, check_budget), every tool execution triggers a KeyError: slice(None, 500, None) in _detect_tool_failure(). The error occurs regardless of the LLM provider (reproduced with both DeepSeek direct API and OpenRouter).

The root cause is in agent/display.py:830:

lower = result[:500].lower()

_detect_tool_failure() assumes result is always a string, but custom-tools plugin tools return a dict. When Python evaluates dict[:500], it calls dict.__getitem__(slice(None, 500, None)), which raises KeyError because no such key exists.

The error is then caught at run_agent.py and wrapped as "Error during OpenAI-compatible API call #N: slice(None, 500, None)", misleadingly suggesting an API issue when it is actually a local tool-result inspection failure.

Steps to Reproduce

  1. Configure a custom-tools plugin that provides tools returning dict results
  2. Run Hermes Agent v0.11.0 (v2026.4.23) with any OpenAI-compatible provider
  3. Trigger a custom tool that returns a dict (e.g., read_mail)
  4. Observe the KeyError on every tool call

Expected Behavior

_detect_tool_failure() should handle non-string tool results gracefully, either by:

  • Converting the result to string before slicing, or
  • Skipping string-based failure detection for non-string results

Actual Behavior

KeyError: slice(None, 500, None)

Full traceback:

File "/usr/local/lib/python3.12/site-packages/run_agent.py", line 11323, in run_conversation
    self._execute_tool_calls(assistant_message, messages, effective_task_id, api_call_count)
File "/usr/local/lib/python3.12/site-packages/run_agent.py", line 7646, in _execute_tool_calls
    return self._execute_tool_calls_sequential(
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/usr/local/lib/python3.12/site-packages/run_agent.py", line 8377, in _execute_tool_calls_sequential
    _is_error_result, _ = _detect_tool_failure(function_name, function_result)
                          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/usr/local/lib/python3.12/site-packages/agent/display.py", line 830, in _detect_tool_failure
    lower = result[:500].lower()
            ~~~~~~^^^^^^
KeyError: slice(None, 500, None)

This error is then surfaced to the user as:

Error during OpenAI-compatible API call #N: slice(None, 500, None)

Proposed Fix

In agent/display.py:830, guard against non-string results:

# Before:
lower = result[:500].lower()

# After:
result_str = result if isinstance(result, str) else str(result)
lower = result_str[:500].lower()

Affected Component

Tools (custom-tools plugin interaction with _detect_tool_failure)

Environment

  • Hermes Agent v0.11.0 (v2026.4.23)
  • Python 3.12
  • Provider: DeepSeek direct API (also reproduced with OpenRouter)
  • OS: Linux (Docker container)
  • Installation method: Docker (nousresearch/hermes-agent image)

extent analysis

TL;DR

Modify the _detect_tool_failure function in agent/display.py to handle non-string results by converting them to strings before slicing.

Guidance

  • Verify that the error occurs when using tools from the custom-tools plugin that return dictionaries.
  • Check the agent/display.py file at line 830 to confirm the issue is with the line lower = result[:500].lower().
  • Apply the proposed fix by changing the line to result_str = result if isinstance(result, str) else str(result); lower = result_str[:500].lower().
  • Test the modified code with the custom-tools plugin to ensure the KeyError is resolved.

Example

The proposed fix can be applied as follows:

# Before:
lower = result[:500].lower()

# After:
result_str = result if isinstance(result, str) else str(result)
lower = result_str[:500].lower()

Notes

This fix assumes that converting non-string results to strings before slicing is the desired behavior. If this is not the case, alternative solutions may be needed.

Recommendation

Apply the workaround by modifying the _detect_tool_failure function as described, as it directly addresses the identified issue and prevents the KeyError from occurring.

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING