hermes - ✅(Solved) Fix todo_tool: AttributeError 'str' object has no attribute 'get' when LLM emits todos as JSON string [3 pull requests, 1 participants]

Official PRs (…)
ON THIS PAGE

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#14185Fetched 2026-04-23 07:46:22
View on GitHub
Comments
0
Participants
1
Timeline
8
Reactions
0
Author
Participants
Timeline (top)
cross-referenced ×3labeled ×3referenced ×2

todo_tool crashes with AttributeError: 'str' object has no attribute 'get' when the LLM emits the todos parameter as a JSON-encoded string instead of the declared array.

Observed rate: occurs intermittently across Claude 4.5 / 4.6 / 4.7 (via CRS proxy, opus variants). The crash is harder to reproduce with GPT-5.x but has been observed after a prior tool-call rejection — the model "self-corrects" by wrapping the list in json.dumps, which then fails schema validation downstream.

Error Message

  1. todo_tool() entry — if todos is a str, attempt json.loads(); if it is neither list nor None, return a clear error instead of crashing.
  2. TodoStore._validate() — coerce non-dict items to a placeholder error record instead of calling .get() on them. All three guards fail closed (return structured error, never raise).

Root Cause

todo_tool(), TodoStore._validate() and _dedupe_by_id() assume the caller obeys the declared schema. There is no defensive parsing / type coercion before dict access.

Fix Action

Fix / Workaround

Tested locally (both unit tests and live dispatch through the gateway at PID 2473729) — see PR for details.

PR fix notes

PR #14187: fix(tools): defensive type coercion for todos in todo_tool

Description (problem / solution / changelog)

Fixes #14185.

What

Three additive defensive guards in tools/todo_tool.py against schema-violating todos inputs.

Why

See #14185 for full context. TL;DR: Claude-family models occasionally emit todos as a json.dumps(...)-ed string (especially after a prior tool_call rejection), which crashes todo_tool with AttributeError: 'str' object has no attribute 'get'.

Changes

  1. todo_tool() entryjson.loads() string todos; reject non-list/None with a clear error.
  2. TodoStore._validate() — coerce non-dict items to an error placeholder instead of crashing on .get().
  3. TodoStore._dedupe_by_id()isinstance(dict) guard before key access.

All guards fail-closed: invalid input → structured error response, never a raised exception.

Non-goals

  • No change to the declared schema — well-formed callers are unaffected.
  • No change to persistence format — only the validation/ingest path is hardened.

Testing

  • Unit tests added for all three guard paths (double-JSON-encoded string, non-dict item, non-list non-None).
  • Live-tested on this installation's gateway (PID 2473729): Todo_tool with a stringified todos now routes to the handler and returns a normal result.
  • No regression in existing test_todo*.py (passed on venv/bin/pytest tests/test_todo_*.py).

Risk

Minimal. ~30 LOC, pure additive, only reachable on schema violations that currently crash.

Related

Companion PR for the sibling dispatch-layer defense: will be linked once it lands.

Changed files

  • tools/todo_tool.py (modified, +32/-1)

PR #2: fix: apply P1 issue hardening (todo/registry/retry)

Description (problem / solution / changelog)

결론

GitHub P1 오픈 이슈 4건의 핵심 재현 케이스를 테스트로 먼저 추가하고, 최소 수정으로 런타임 방어 로직을 반영했습니다.

반영 이슈

변경 요약

  1. todo_tool 하드닝 (#14185)
  • todos가 문자열이면 JSON 파싱 시도
  • 파싱 실패 시 구조화 에러 반환
  • list 타입 검증 추가
  • _validate, _dedupe_by_id에서 non-dict 입력 방어
  1. registry dispatch 하드닝 (#14186)
  • _normalize_tool_name() 추가
  • CamelCase, _tool suffix, 구분자 드리프트를 정규화 fallback으로 처리
  • 예: TodoTool_tool -> todo, WriteFile_tool -> write_file
  1. run_agent 분류 수정 (#14271)
  • local validation fast-fail 분류에서 json.JSONDecodeError 제외
  • transient JSON 파싱 실패가 retry 경로를 타도록 보정
  1. error classifier 보강 (#14195)
  • _extract_error_body()__cause__/__context__ 체인을 따라 nested body를 추출하도록 수정
  • wrapped 402 오류에서 nested message("usage limit ... try again")를 잃지 않도록 보정
  • billing 오분류를 줄이고 transient rate_limit 분류 정확도 개선

테스트

  • scripts/run_tests.sh tests/tools/test_todo_tool.py tests/tools/test_registry.py tests/run_agent/test_run_agent.py::TestRetryExhaustion::test_jsondecode_error_is_retried_not_treated_as_local_validation
  • scripts/run_tests.sh tests/agent/test_error_classifier.py tests/tools/test_todo_tool.py tests/tools/test_registry.py tests/run_agent/test_run_agent.py::TestRetryExhaustion::test_jsondecode_error_is_retried_not_treated_as_local_validation

모두 통과했습니다.

비고

  • aideautomation/aide_hermes_agent 저장소는 코드 구조가 현재 hermes-agent와 상이하여 동일 커밋 체리픽이 불가했습니다.
  • 실행 가능한 반영은 aideautomation/hermes-agent 포크 기준으로 완료했습니다.

Changed files

  • agent/error_classifier.py (modified, +24/-13)
  • run_agent.py (modified, +1/-1)
  • tests/agent/test_error_classifier.py (modified, +23/-0)
  • tests/run_agent/test_run_agent.py (modified, +20/-0)
  • tests/tools/test_registry.py (modified, +24/-0)
  • tests/tools/test_todo_tool.py (modified, +23/-0)
  • tools/registry.py (modified, +43/-1)
  • tools/todo_tool.py (modified, +16/-2)

PR #14350: fix(todo): accept JSON-string todo lists

Description (problem / solution / changelog)

Summary

  • Fixes #14185
  • Parse todos when a provider sends it as a JSON string instead of an array.
  • Reject invalid JSON, non-list values, and non-object list entries with clean tool errors before reaching TodoStore.write().

Root cause

Some tool-call paths can pass the todos argument as a JSON-encoded string. The todo store assumed it was already a list of objects, so string input was iterated character-by-character and crashed when validation called .get().

Tests

  • uv run --frozen --python 3.11 --extra dev pytest -o addopts='' tests/tools/test_todo_tool.py -q
  • git diff --check

Note: I also tried uv run --frozen --python 3.11 --extra dev ruff check tools/todo_tool.py tests/tools/test_todo_tool.py, but this environment does not install a ruff executable (No such file or directory).

Changed files

  • tests/tools/test_todo_tool.py (modified, +38/-0)
  • tools/todo_tool.py (modified, +23/-2)

Code Example

# Pattern A: double-JSON-encoded
   {"todos": '[{"id":"t1","content":"x","status":"pending"}]'}
   # Pattern B: non-dict item in list
   {"todos": ["not-a-dict"]}
RAW_BUFFERClick to expand / collapse

Summary

todo_tool crashes with AttributeError: 'str' object has no attribute 'get' when the LLM emits the todos parameter as a JSON-encoded string instead of the declared array.

Observed rate: occurs intermittently across Claude 4.5 / 4.6 / 4.7 (via CRS proxy, opus variants). The crash is harder to reproduce with GPT-5.x but has been observed after a prior tool-call rejection — the model "self-corrects" by wrapping the list in json.dumps, which then fails schema validation downstream.

Reproduction

  1. Invoke todo with the todos field serialized as a string (any of these observed patterns):
    # Pattern A: double-JSON-encoded
    {"todos": '[{"id":"t1","content":"x","status":"pending"}]'}
    # Pattern B: non-dict item in list
    {"todos": ["not-a-dict"]}
  2. Handler throws in TodoStore._validate() / _dedupe_by_id() during item.get("id").

Root cause

todo_tool(), TodoStore._validate() and _dedupe_by_id() assume the caller obeys the declared schema. There is no defensive parsing / type coercion before dict access.

Community evidence

  • mastra-ai/mastra#12581 — 44-model study documenting Claude-family schema drift at non-trivial rates.
  • anthropic/anthropic-cookbook#50235 — similar defense-in-depth recommendation for tool handlers.

Proposed fix

Three additive guards (~30 LOC, no behavior change for well-formed input):

  1. todo_tool() entry — if todos is a str, attempt json.loads(); if it is neither list nor None, return a clear error instead of crashing.
  2. TodoStore._validate() — coerce non-dict items to a placeholder error record instead of calling .get() on them.
  3. TodoStore._dedupe_by_id() — defensive isinstance check before key access.

All three guards fail closed (return structured error, never raise).

PR

I will open a PR from JayGwod/hermes-agent:fix/todo-tool-type-coercion right after this issue so they can be linked.

Tested locally (both unit tests and live dispatch through the gateway at PID 2473729) — see PR for details.

extent analysis

TL;DR

The most likely fix is to add defensive parsing and type coercion in todo_tool(), TodoStore._validate(), and TodoStore._dedupe_by_id() to handle cases where the todos parameter is a JSON-encoded string instead of an array.

Guidance

  • Verify that the todos parameter is indeed being passed as a string by checking the input data and the schema validation errors.
  • Attempt to parse the string as JSON using json.loads() in todo_tool() to convert it to a list, and return a clear error if parsing fails.
  • Add a check in TodoStore._validate() to coerce non-dict items to a placeholder error record instead of calling .get() on them.
  • Use an isinstance check in TodoStore._dedupe_by_id() to ensure that the item is a dictionary before accessing its keys.

Example

import json

def todo_tool(todos):
    if isinstance(todos, str):
        try:
            todos = json.loads(todos)
        except json.JSONDecodeError:
            return {"error": "Invalid JSON"}
    # ...

Notes

The proposed fix assumes that the todos parameter should always be a list of dictionaries. If this is not the case, additional validation and error handling may be necessary.

Recommendation

Apply the proposed workaround by adding the three additive guards to todo_tool(), TodoStore._validate(), and TodoStore._dedupe_by_id() to handle cases where the todos parameter is a JSON-encoded string instead of an array. This will prevent the AttributeError and provide a clear error message instead of crashing.

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