hermes - ✅(Solved) Fix delegate_task crashes when tool message content is a list [5 pull requests, 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#28639Fetched 2026-05-20 04:03:01
View on GitHub
Comments
0
Participants
1
Timeline
11
Reactions
0
Author
Participants
Timeline (top)
cross-referenced ×5labeled ×4referenced ×2

Error Message

AttributeError: 'list' object has no attribute 'lstrip'

Relevant traceback from ~/.hermes/logs/errors.log:

ERROR root: [subagent-1] failed
  File "/home/whiwoon/.hermes/hermes-agent/tools/delegate_tool.py", line 1657, in _run_single_child
  File "/home/whiwoon/.hermes/hermes-agent/tools/delegate_tool.py", line 290, in _looks_like_error_output
    head = content.lstrip()
AttributeError: 'list' object has no attribute 'lstrip'

The same failure happened for multiple children in one batch run:

Tool delegate_task returned error (138.35s):
{"results": [
  {"task_index": 0, "status": "error", "summary": null, "error": "'list' object has no attribute 'lstrip'"},
  {"task_index": 1, "status": "error", "summary": null, "error": "'list' object has no attribute 'lstrip'"},
  {"task_index": 2, "status": "error", "summary": null, "error": "'list' object has no attribute 'lstrip'"}
]}

Fix Action

Fixed

PR fix notes

PR #28648: fix(delegate): handle list tool message content in _run_single_child (#28639)

Description (problem / solution / changelog)

Summary

Fix a crash in _run_single_child when tool message content is a list (e.g., OpenAI multimodal format). _looks_like_error_output() expects a string and calls .lstrip() on it.

Problem

When delegate_task(tasks=[...]) is called and subagents produce tool messages with content as a list (e.g. [{"type": "text", "text": "..."}]), _run_single_child crashes with:

AttributeError: 'list' object has no attribute 'lstrip'

Root cause: msg.get("content", "") returns the list when the key exists, not the default empty string. The same _build_tool_trace function already had this guard (lines 262-264) but _run_single_child (lines 1656) did not.

Solution

Add the same type guard that already exists in _build_tool_trace:

  • Use msg.get("content") or "" so falsy/empty values get the string default
  • Convert non-string content to str(content) before passing to _looks_like_error_output()

Files Changed

FileChange
tools/delegate_tool.py+3/-1 — add type guard in _run_single_child

Test Results

135 passed in 22.93s

Design Decisions

Pattern consistency: the exact same guard (isinstance check + str() fallback) was already used in _build_tool_trace() at L262-264. This fix copies the proven pattern rather than inventing a new one.

Closes #28639

Changed files

  • tools/delegate_tool.py (modified, +3/-1)

PR #28658: fix(agent): normalize non-string tool content in delegate tool_trace (#28639)

Description (problem / solution / changelog)

What does this PR do?

tools/delegate_tool.py::_run_single_child reads each tool message's content with msg.get("content", "") and immediately calls _looks_like_error_output(content), which does content.lstrip(). When a subagent produces Anthropic/OpenAI-style multi-part tool content (a list of {"type": "text", ...} / image blocks), this raises AttributeError: 'list' object has no attribute 'lstrip' and the entire subagent result assembly crashes — even though the child task itself ran to completion. The reporter (#28639) hit this from a Discord gateway running a batch delegate_task(tasks=[...]), where all three children failed with this single error.

This PR mirrors the canonical normalize-content pattern that already exists at the sibling call site _build_tool_trace_tail (around line 262 of the same file): if content is not a string, coerce it with str(content) before passing it to _looks_like_error_output() and before measuring len(content). After the guard the conservative error detector runs on a safe string, and result_bytes becomes a usable byte estimate instead of a parts count.

Related Issue

Fixes #28639

Type of Change

  • 🐛 Bug fix (non-breaking change that fixes an issue)
  • ✨ New feature (non-breaking change that adds functionality)
  • 🔒 Security fix
  • 📝 Documentation update
  • ✅ Tests (adding or improving test coverage)
  • ♻️ Refactor (no behavior change)
  • 🎯 New skill (bundled or hub)

Changes Made

  • tools/delegate_tool.py — In the tool-result branch of _run_single_child's tool_trace builder, coerce non-string content to str(content) before invoking _looks_like_error_output() and len(content). Two-line guard, mirrors the existing pattern at _build_tool_trace_tail.
  • tests/tools/test_delegate.py — Add test_tool_trace_handles_non_string_content in TestDelegateObservability that exercises a tool message with list-typed content and asserts the child exits cleanly with a usable tool_trace entry. Reproduces the exact AttributeError from the issue when the fix is reverted.

How to Test

  1. From the repo root:
  2. uv run --with pytest --with pytest-xdist --with pytest-asyncio python3 -m pytest tests/tools/test_delegate.py::TestDelegateObservability -v
  3. Expected: 6 passed including test_tool_trace_handles_non_string_content. Reverting the two-line guard in _run_single_child flips the new test to FAIL with the exact AttributeError: 'list' object has no attribute 'lstrip' traceback from the issue.

Checklist

Code

  • I've read the Contributing Guide
  • My commit messages follow Conventional Commits (fix(scope):, feat(scope):, etc.)
  • I searched for existing PRs to make sure this isn't a duplicate
  • My PR contains only changes related to this fix/feature (no unrelated commits)
  • I've run focused tests for the touched code and all pass
  • I've added tests for my changes (required for bug fixes, strongly encouraged for features)
  • I've tested on my platform: macOS 15.x

Documentation & Housekeeping

  • I've updated relevant documentation (README, docs/, docstrings) — N/A
  • I've updated cli-config.yaml.example if I added/changed config keys — N/A
  • I've updated CONTRIBUTING.md or AGENTS.md if I changed architecture or workflows — N/A
  • I've considered cross-platform impact (Windows, macOS) per the compatibility guide — N/A (no platform-specific code paths)
  • I've updated tool descriptions/schemas if I changed tool behavior — N/A

Audited siblings: _build_tool_trace_tail (the other _looks_like_error_output call site at line ~262) already normalizes non-string content with the same str(content) guard. No further widening needed inside this file. The fix is the missing half of an existing convention, not a new one.

Changed files

  • tests/tools/test_delegate.py (modified, +79/-0)
  • tools/delegate_tool.py (modified, +10/-1)

PR #7: fix(delegate): tolerate non-string tool message content in tool_trace builder (#28639)

Description (problem / solution / changelog)

🟢 Merge order: 3 / 12 — trivial, 2-line guard, zero risk

Closes #28639 (P1)

Problem

_looks_like_error_output() assumes content is always a string and calls .lstrip(). When tool message content is a list (OpenAI multipart responses), it crashes with AttributeError.

Fix

Normalize content to str before the call, matching the existing pattern at line 261.

Risk assessment

FactorRating
Lines changed2
New code1 isinstance check
Side effectsNone — str(list) is lossless for error detection
Revert complexityTrivial

Files changed

  • tools/delegate_tool.py (+2)

Changed files

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

PR #1: fix(delegate): tolerate non-string tool message content (#28639)

Description (problem / solution / changelog)

Summary

Fixes #28639

When tool responses contain list or dict content (e.g. from browser or file tools), _looks_like_error_output in tools/delegate_tool.py crashed with AttributeError because it assumed all content is a string.

Changes

tools/delegate_tool.py (+6 -1):

  • Remove str type annotation from _looks_like_error_output parameter to accept any type
  • Add isinstance guard in _looks_like_error_output: coerce non-string content via str() before inspecting
  • Add isinstance guard in _run_with_thread_capture tool-trace assembly: coerce non-string tool message content before passing to _looks_like_error_output

tests/test_delegate_tool_trace.py (new, 132 lines):

  • Unit tests for _looks_like_error_output with string, list, dict, number, and nested content
  • Regression tests specifically for the #28639 crash path
  • Integration test simulating the tool-trace assembly path with list-content messages

Testing

  • All new tests pass
  • Fail-without-fix verified: reverting the production change while keeping the test file causes test failures on the regression tests
  • No regressions in existing test suite

Verification

$ python3 -m pytest tests/test_delegate_tool_trace.py -v -o "addopts="
# All 18 tests pass

Changed files

  • .dockerignore (modified, +15/-0)
  • .env.example (modified, +88/-18)
  • .github/actions/hermes-smoke-test/action.yml (added, +47/-0)
  • .github/actions/nix-setup/action.yml (added, +18/-0)
  • .github/dependabot.yml (added, +44/-0)
  • .github/workflows/contributor-check.yml (modified, +1/-1)
  • .github/workflows/deploy-site.yml (modified, +15/-2)
  • .github/workflows/docker-publish.yml (modified, +481/-38)
  • .github/workflows/docs-site-checks.yml (modified, +5/-2)
  • .github/workflows/history-check.yml (added, +58/-0)
  • .github/workflows/lint.yml (added, +202/-0)
  • .github/workflows/nix-lockfile-fix.yml (added, +254/-0)
  • .github/workflows/nix.yml (modified, +86/-12)
  • .github/workflows/osv-scanner.yml (added, +67/-0)
  • .github/workflows/skills-index.yml (modified, +4/-4)
  • .github/workflows/supply-chain-audit.yml (modified, +67/-1)
  • .github/workflows/tests.yml (modified, +6/-3)
  • .github/workflows/upload_to_pypi.yml (added, +164/-0)
  • .github/workflows/uv-lockfile-check.yml (added, +119/-0)
  • .gitignore (modified, +10/-0)
  • .gitmodules (removed, +0/-3)
  • AGENTS.md (modified, +617/-81)
  • CONTRIBUTING.md (modified, +289/-27)
  • Dockerfile (modified, +75/-11)
  • README.md (modified, +25/-16)
  • README.zh-CN.md (added, +180/-0)
  • RELEASE_v0.11.0.md (added, +453/-0)
  • RELEASE_v0.12.0.md (added, +505/-0)
  • RELEASE_v0.13.0.md (added, +641/-0)
  • RELEASE_v0.14.0.md (added, +479/-0)
  • SECURITY.md (modified, +302/-55)
  • acp_adapter/auth.py (modified, +59/-4)
  • acp_adapter/edit_approval.py (added, +286/-0)
  • acp_adapter/entry.py (modified, +182/-1)
  • acp_adapter/events.py (modified, +89/-4)
  • acp_adapter/permissions.py (modified, +117/-26)
  • acp_adapter/server.py (modified, +1167/-65)
  • acp_adapter/session.py (modified, +79/-19)
  • acp_adapter/tools.py (modified, +1015/-37)
  • acp_registry/agent.json (modified, +12/-8)
  • acp_registry/icon.svg (modified, +7/-24)
  • agent/account_usage.py (added, +326/-0)
  • agent/agent_init.py (added, +1504/-0)
  • agent/agent_runtime_helpers.py (added, +2159/-0)
  • agent/anthropic_adapter.py (modified, +816/-123)
  • agent/async_utils.py (added, +68/-0)
  • agent/auxiliary_client.py (modified, +2774/-332)
  • agent/azure_identity_adapter.py (added, +555/-0)
  • agent/background_review.py (added, +582/-0)
  • agent/bedrock_adapter.py (modified, +198/-7)
  • agent/browser_provider.py (added, +175/-0)
  • agent/browser_registry.py (added, +223/-0)
  • agent/chat_completion_helpers.py (added, +2078/-0)
  • agent/codex_responses_adapter.py (added, +1084/-0)
  • agent/codex_runtime.py (added, +448/-0)
  • agent/context_compressor.py (modified, +596/-66)
  • agent/context_engine.py (modified, +27/-0)
  • agent/context_references.py (modified, +1/-3)
  • agent/conversation_compression.py (added, +0/-0)
  • agent/conversation_loop.py (added, +0/-0)
  • agent/copilot_acp_client.py (modified, +0/-0)
  • agent/credential_pool.py (modified, +0/-0)
  • agent/credential_sources.py (added, +0/-0)
  • agent/curator.py (added, +0/-0)
  • agent/curator_backup.py (added, +0/-0)
  • agent/display.py (modified, +0/-0)
  • agent/error_classifier.py (modified, +0/-0)
  • agent/file_safety.py (added, +0/-0)
  • agent/gemini_cloudcode_adapter.py (modified, +0/-0)
  • agent/gemini_native_adapter.py (modified, +0/-0)
  • agent/gemini_schema.py (added, +0/-0)
  • agent/google_code_assist.py (modified, +0/-0)
  • agent/google_oauth.py (modified, +0/-0)
  • agent/i18n.py (added, +0/-0)
  • agent/image_gen_provider.py (added, +0/-0)
  • agent/image_gen_registry.py (added, +0/-0)
  • agent/image_routing.py (added, +0/-0)
  • agent/insights.py (modified, +0/-0)
  • agent/iteration_budget.py (added, +0/-0)
  • agent/lmstudio_reasoning.py (added, +0/-0)
  • agent/lsp/__init__.py (added, +0/-0)
  • agent/lsp/cli.py (added, +0/-0)
  • agent/lsp/client.py (added, +0/-0)
  • agent/lsp/eventlog.py (added, +0/-0)
  • agent/lsp/install.py (added, +0/-0)
  • agent/lsp/manager.py (added, +0/-0)
  • agent/lsp/protocol.py (added, +0/-0)
  • agent/lsp/range_shift.py (added, +0/-0)
  • agent/lsp/reporter.py (added, +0/-0)
  • agent/lsp/servers.py (added, +0/-0)
  • agent/lsp/workspace.py (added, +0/-0)
  • agent/manual_compression_feedback.py (modified, +0/-0)
  • agent/markdown_tables.py (added, +0/-0)
  • agent/memory_manager.py (modified, +0/-0)
  • agent/memory_provider.py (modified, +0/-0)
  • agent/message_sanitization.py (added, +0/-0)
  • agent/model_metadata.py (modified, +0/-0)
  • agent/models_dev.py (modified, +0/-0)
  • agent/moonshot_schema.py (added, +0/-0)
  • agent/nous_rate_guard.py (modified, +0/-0)

PR #28899: fix(delegate): tolerate non-string tool message content (#28639)

Description (problem / solution / changelog)

Summary

Fixes #28639

When tool responses contain list or dict content (e.g. from browser or file tools), _looks_like_error_output in tools/delegate_tool.py crashed with AttributeError because it assumed all content is a string.

Changes

tools/delegate_tool.py (+6 -1):

  • Remove str type annotation from _looks_like_error_output parameter to accept any type
  • Add isinstance guard in _looks_like_error_output: coerce non-string content via str() before inspecting
  • Add isinstance guard in _run_with_thread_capture tool-trace assembly: coerce non-string tool message content before passing to _looks_like_error_output

tests/test_delegate_tool_trace.py (new, 132 lines):

  • Unit tests for _looks_like_error_output with string, list, dict, number, and nested content
  • Regression tests specifically for the #28639 crash path
  • Integration test simulating the tool-trace assembly path with list-content messages

Testing

  • All new tests pass
  • Fail-without-fix verified: reverting the production change while keeping the test file causes test failures on the regression tests
  • No regressions in existing test suite

Verification

$ python3 -m pytest tests/test_delegate_tool_trace.py -v -o "addopts="
# All 18 tests pass

Changed files

  • tests/test_delegate_tool_trace.py (added, +132/-0)
  • tools/delegate_tool.py (modified, +6/-1)

Code Example

AttributeError: 'list' object has no attribute 'lstrip'

---

ERROR root: [subagent-1] failed
  File "/home/whiwoon/.hermes/hermes-agent/tools/delegate_tool.py", line 1657, in _run_single_child
  File "/home/whiwoon/.hermes/hermes-agent/tools/delegate_tool.py", line 290, in _looks_like_error_output
    head = content.lstrip()
AttributeError: 'list' object has no attribute 'lstrip'

---

Tool delegate_task returned error (138.35s):
{"results": [
  {"task_index": 0, "status": "error", "summary": null, "error": "'list' object has no attribute 'lstrip'"},
  {"task_index": 1, "status": "error", "summary": null, "error": "'list' object has no attribute 'lstrip'"},
  {"task_index": 2, "status": "error", "summary": null, "error": "'list' object has no attribute 'lstrip'"}
]}

---

delegate_task(tasks=[
  {"goal": "review a deck from an education/curriculum perspective", "toolsets": ["file", "vision", "terminal"]},
  {"goal": "review the same deck from a security/safety perspective", "toolsets": ["file", "vision", "terminal"]},
  {"goal": "review the same deck from a PPT design/print perspective", "toolsets": ["file", "vision", "terminal"]},
])

---

tools/delegate_tool.py:277
def _looks_like_error_output(content: str) -> bool:

tools/delegate_tool.py:290
head = content.lstrip()

tools/delegate_tool.py:1656
content = msg.get("content", "")
tools/delegate_tool.py:1657
is_error = _looks_like_error_output(content)

---

Hermes Agent v0.14.0 (2026.5.16)
Local HEAD: 1345dda0c
origin/main checked: a0bd11d02
Platform: Discord gateway
Host: Linux 6.12.75+rpt-rpi-2712
Python: 3.11.15
OpenAI SDK: 2.24.0
RAW_BUFFERClick to expand / collapse

Bug description

Thanks for all the work on delegate_task and the recent tool trace improvements. I ran into a crash while using delegate_task(tasks=[...]) with multiple subagents from a Discord gateway session.

The subagents failed while delegate_task was building the child tool_trace. The immediate cause seems to be that a tool message content value was a list, but _looks_like_error_output() assumes it is always a string and calls .lstrip().

Error

AttributeError: 'list' object has no attribute 'lstrip'

Relevant traceback from ~/.hermes/logs/errors.log:

ERROR root: [subagent-1] failed
  File "/home/whiwoon/.hermes/hermes-agent/tools/delegate_tool.py", line 1657, in _run_single_child
  File "/home/whiwoon/.hermes/hermes-agent/tools/delegate_tool.py", line 290, in _looks_like_error_output
    head = content.lstrip()
AttributeError: 'list' object has no attribute 'lstrip'

The same failure happened for multiple children in one batch run:

Tool delegate_task returned error (138.35s):
{"results": [
  {"task_index": 0, "status": "error", "summary": null, "error": "'list' object has no attribute 'lstrip'"},
  {"task_index": 1, "status": "error", "summary": null, "error": "'list' object has no attribute 'lstrip'"},
  {"task_index": 2, "status": "error", "summary": null, "error": "'list' object has no attribute 'lstrip'"}
]}

Steps to reproduce

I do not yet have a minimal standalone repro, but the failure occurred with:

  1. Running Hermes from the Discord gateway.
  2. Calling delegate_task with the batch form:
delegate_task(tasks=[
  {"goal": "review a deck from an education/curriculum perspective", "toolsets": ["file", "vision", "terminal"]},
  {"goal": "review the same deck from a security/safety perspective", "toolsets": ["file", "vision", "terminal"]},
  {"goal": "review the same deck from a PPT design/print perspective", "toolsets": ["file", "vision", "terminal"]},
])
  1. Each child was given paths to a local markdown file and rendered slide contact sheets.
  2. The parent received error results for all children with "'list' object has no attribute 'lstrip'".

Expected behavior

delegate_task should tolerate non-string tool message content when generating tool_trace. A list or dict content should not crash the subagent result assembly.

Possible approaches:

  • Normalize content before calling _looks_like_error_output(), or
  • Make _looks_like_error_output() accept Any and serialize non-string content safely before inspecting it.

Actual behavior

The child task fails during tool trace generation, even though the underlying child work may have reached tool messages successfully. The parent only receives error summaries.

Code path

Current origin/main still appears to have the string-only assumption:

tools/delegate_tool.py:277
def _looks_like_error_output(content: str) -> bool:

tools/delegate_tool.py:290
head = content.lstrip()

tools/delegate_tool.py:1656
content = msg.get("content", "")
tools/delegate_tool.py:1657
is_error = _looks_like_error_output(content)

This looks related to the recent tool_trace false-positive error detection work, but I could not find an existing issue or PR for the non-string content crash specifically.

Environment

Hermes Agent v0.14.0 (2026.5.16)
Local HEAD: 1345dda0c
origin/main checked: a0bd11d02
Platform: Discord gateway
Host: Linux 6.12.75+rpt-rpi-2712
Python: 3.11.15
OpenAI SDK: 2.24.0

Notes

I checked the public issue/PR list for searches such as delegate_task lstrip list content, subagent lstrip, and "list" "lstrip" delegate, and did not find a matching report. There are other delegate_task issues open, but they appear to cover different failures such as model routing, missing child tools, provider 404s, or TUI progress display.

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…

FAQ

Expected behavior

delegate_task should tolerate non-string tool message content when generating tool_trace. A list or dict content should not crash the subagent result assembly.

Possible approaches:

  • Normalize content before calling _looks_like_error_output(), or
  • Make _looks_like_error_output() accept Any and serialize non-string content safely before inspecting it.

Still need to ship something?

×6

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

Back to top recommendations

TRENDING

hermes - ✅(Solved) Fix delegate_task crashes when tool message content is a list [5 pull requests, 1 participants]