fix(todo_tool): add defensive type coercion for LLM schema drift#22505
Open
AllynSheep wants to merge 2 commits into
Open
fix(todo_tool): add defensive type coercion for LLM schema drift#22505AllynSheep wants to merge 2 commits into
AllynSheep wants to merge 2 commits into
Conversation
liuhao1024
reviewed
May 9, 2026
liuhao1024
left a comment
Contributor
There was a problem hiding this comment.
Bug: todos=None (read path) is now rejected
The new type-validation guards are placed before the existing if todos is not None: block:
# (new code)
if not isinstance(todos, list):
return tool_error(f"todo: todos must be a list, got {type(todos).__name__}")
# (existing code)
if todos is not None:
items = store.write(todos, merge)
else:
items = store.read() # ← this path is now unreachableWhen the LLM calls the tool with no todos argument, todos defaults to None. Since None is not a list, the new guard returns an error and the read path (store.read()) is never reached.
Fix: add an explicit None bypass before the list check:
if todos is not None and not isinstance(todos, list):
return tool_error(f"todo: todos must be a list or null, got {type(todos).__name__}")Or move the new validation inside the if todos is not None: branch.
added 2 commits
May 11, 2026 10:56
Fixes NousResearch#14185: Add three guards to handle cases where the LLM emits todos as a JSON-encoded string instead of an array. - todo_tool(): coerce string -> list via json.loads(); return structured error for non-list/non-string inputs - TodoStore._validate(): return placeholder error record for non-dict items instead of crashing on .get() - TodoStore._dedupe_by_id(): skip non-dict items defensively All guards fail closed (return structured error, never raise).
Move the isinstance(todos, list) check inside the if todos is not None block to avoid rejecting the read path (todos=None). Previously, calling the tool without a todos argument would hit the type guard first and return an error, making store.read() unreachable. Reported-by: liuhao1024 Fixes: NousResearch#22505 review feedback
0dcd8e1 to
9ba78d2
Compare
Collaborator
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Fixes #14185: Add three guards to handle cases where the LLM emits
todosas a JSON-encoded string instead of the declared array.Changes
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; skips non-dict items.All three guards fail closed (return structured error, never raise).
Related
claude-sonnet-4-5model mastra-ai/mastra#12581 — 44-model study documenting Claude-family schema drift