fix: handle double-encoded JSON in tool call arguments (Moonshot API)#2407
fix: handle double-encoded JSON in tool call arguments (Moonshot API)#2407wintrover wants to merge 1 commit into
Conversation
Moonshot API returns function.arguments with double-encoded JSON strings
for nested array/object values. After json.loads parses the outer JSON,
inner values like 'todos' remain as strings, causing Pydantic validation
failures ('Input should be a valid list', input_type=str).
This affects all tools with array/dict parameters:
- SetTodoList (todos: list)
- ExitPlanMode (options: list)
- StrReplaceFile (edit: dict/list)
Fix adds:
1. isinstance(dict) check for already-parsed arguments
2. Double-encoding detection: re-parse string values that look like JSON
Fixes MoonshotAI#2406
| if isinstance(arguments, dict): | ||
| for k, v in list(arguments.items()): | ||
| if isinstance(v, str) and v and v[0] in ("[", "{"): | ||
| try: | ||
| arguments[k] = json.loads(v, strict=False) | ||
| except (json.JSONDecodeError, ValueError): | ||
| pass |
There was a problem hiding this comment.
🔴 Double-decode heuristic corrupts legitimate string parameters containing JSON
The new double-decode logic at lines 309-312 blindly parses any string value starting with { or [ as JSON, without checking the tool's parameter schema. This causes false-positive decoding for tools whose str-typed parameters legitimately contain JSON text. The most impactful case is WriteFile (src/kimi_cli/tools/file/write.py:29), where content: str often holds JSON when writing .json files. For example, when the LLM writes {"key": "value"} as content, json.loads at line 311 converts the string into a Python dict. When the mutated arguments reach CallableTool2.call() → self.params.model_validate(arguments) (packages/kosong/src/kosong/tooling/__init__.py:296), Pydantic rejects the dict because the schema expects str, returning a ToolValidateError. This makes writing any JSON, YAML-like, or array-like file content fail. Other affected tools include StrReplaceFile (whose old/new fields contain code snippets that could be valid JSON) when the edit is double-decoded, and potentially Shell.command or Agent.prompt in rare cases.
Prompt for agents
The double-decode heuristic at lines 307-313 of src/kimi_cli/soul/toolset.py needs to be schema-aware to avoid corrupting legitimate string parameters. Currently it blindly parses any dict value that is a string starting with { or [ as JSON. This breaks tools like WriteFile whose content parameter is typed as str but frequently contains JSON text (e.g. writing package.json files).
The fix should consult the tool's parameter schema before attempting to double-decode. Specifically:
1. Look up the tool from self._tool_dict[tool_name]
2. Check the tool's parameters JSON schema to see if the field is declared as type object or array (not string)
3. Only attempt json.loads on string values whose schema-declared type is object or array
Alternatively, a simpler approach: attempt the double-decode but then validate the result against the tool schema. If validation fails with the decoded value but would have succeeded with the original string, revert the decoding for that key.
The relevant tool lookup is already at line 373 (tool = self._tool_dict[tool_name]). The tool's JSON schema is accessible via tool.base.parameters (for CallableTool) or tool._base.parameters (for CallableTool2), which contains a standard JSON Schema dict with properties and their types.
Was this helpful? React with 👍 or 👎 to provide feedback.
Summary
Fixes #2406
Moonshot API returns
function.argumentswith double-encoded JSON strings for nested array/object values. Afterjson.loadsparses the outer JSON, inner values liketodosremain as strings, causing Pydantic validation failures.Affected tools:
SetTodoList,ExitPlanMode,StrReplaceFile, and any tool with array/dict parameters.Changes
src/kimi_cli/soul/toolset.py—KimiToolset.handle():isinstance(raw, dict)check for already-parsed arguments (defensive)Testing
Verified locally:
SetTodoListwithtodos=[{"title": "test", "status": "in_progress"}]→ ✅ workstodos) → ✅ worksExitPlanModewithoptions=[...]→ ✅ works