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
- 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"]}
- 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
Proposed fix
Three additive guards (~30 LOC, no behavior change for well-formed input):
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.
TodoStore._validate() — coerce non-dict items to a placeholder error record instead of calling .get() on them.
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.
Summary
todo_toolcrashes withAttributeError: 'str' object has no attribute 'get'when the LLM emits thetodosparameter 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
todowith thetodosfield serialized as a string (any of these observed patterns):TodoStore._validate()/_dedupe_by_id()duringitem.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
claude-sonnet-4-5model mastra-ai/mastra#12581 — 44-model study documenting Claude-family schema drift at non-trivial rates.Proposed fix
Three additive guards (~30 LOC, no behavior change for well-formed input):
todo_tool()entry — iftodosis astr, attemptjson.loads(); if it is neither list nor None, return a clear error instead of crashing.TodoStore._validate()— coerce non-dict items to a placeholder error record instead of calling.get()on them.TodoStore._dedupe_by_id()— defensiveisinstancecheck 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-coercionright 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.