fix: tolerate string-encoded skills param for LLM provider compatibility#771
fix: tolerate string-encoded skills param for LLM provider compatibility#771jinglun010-cpu wants to merge 2 commits into
Conversation
Some LLM providers (e.g. qwen3.7-max via DashScope OpenAI-compatible endpoint) serialize array-typed function call parameters as JSON-encoded strings instead of native JSON arrays. This causes Pydantic validation to reject the skills parameter before the function body executes, making every create_agent and load_skill call fail. Changes: - Widen skills type annotation: list[str] -> str | list[str] - Add strict_mode=False to @function_tool decorator - Parse string inputs in function body (json.loads with comma-split fallback) Tested with qwen3.7-max: create_agent successfully spawns child agents that execute 23+ exec_command calls for SQL injection testing. Closes usestrix#770
Greptile SummaryThis PR makes skill arguments compatible with providers that encode arrays as strings. The main changes are:
Confidence Score: 4/5The decoded skill values need shape validation before merging.
strix/tools/agents_graph/tools.py and strix/tools/load_skill/tool.py Important Files Changed
Prompt To Fix All With AIFix the following 2 code review issues. Work through them one at a time, proposing concise fixes.
---
### Issue 1 of 2
strix/tools/agents_graph/tools.py:423-427
**Decoded JSON Bypasses List Shape**
When a provider sends valid JSON that is not a string array, `json.loads()` still succeeds. A JSON string is expanded into individual characters by `list(skills)`, an object is treated as its keys, and a mixed array can raise inside `validate_requested_skills()` while formatting heterogeneous values, so `create_agent` can misapply skills or fail instead of returning a controlled validation error.
```suggestion
if isinstance(skills, str):
original_skills = skills
try:
decoded_skills = json.loads(skills)
except json.JSONDecodeError:
skills = [s.strip() for s in skills.split(",") if s.strip()]
else:
if isinstance(decoded_skills, str):
skills = [decoded_skills]
elif isinstance(decoded_skills, list) and all(
isinstance(skill, str) for skill in decoded_skills
):
skills = decoded_skills
else:
skills = [original_skills]
```
### Issue 2 of 2
strix/tools/load_skill/tool.py:31-35
**Decoded JSON Bypasses List Shape**
When `skills` contains valid non-array JSON, decoding succeeds without enforcing the downstream `list[str]` contract. A JSON string is split into characters, an object silently becomes its keys, and mixed element types can make validation raise, so `load_skill` can load the wrong selection or terminate the tool call unexpectedly.
```suggestion
if isinstance(skills, str):
original_skills = skills
try:
decoded_skills = json.loads(skills)
except json.JSONDecodeError:
skills = [s.strip() for s in skills.split(",") if s.strip()]
else:
if isinstance(decoded_skills, str):
skills = [decoded_skills]
elif isinstance(decoded_skills, list) and all(
isinstance(skill, str) for skill in decoded_skills
):
skills = decoded_skills
else:
skills = [original_skills]
```
Reviews (1): Last reviewed commit: "fix: tolerate string-encoded skills para..." | Re-trigger Greptile |
| if isinstance(skills, str): | ||
| try: | ||
| skills = json.loads(skills) | ||
| except json.JSONDecodeError: | ||
| skills = [s.strip() for s in skills.split(",") if s.strip()] |
There was a problem hiding this comment.
Decoded JSON Bypasses List Shape
When a provider sends valid JSON that is not a string array, json.loads() still succeeds. A JSON string is expanded into individual characters by list(skills), an object is treated as its keys, and a mixed array can raise inside validate_requested_skills() while formatting heterogeneous values, so create_agent can misapply skills or fail instead of returning a controlled validation error.
| if isinstance(skills, str): | |
| try: | |
| skills = json.loads(skills) | |
| except json.JSONDecodeError: | |
| skills = [s.strip() for s in skills.split(",") if s.strip()] | |
| if isinstance(skills, str): | |
| original_skills = skills | |
| try: | |
| decoded_skills = json.loads(skills) | |
| except json.JSONDecodeError: | |
| skills = [s.strip() for s in skills.split(",") if s.strip()] | |
| else: | |
| if isinstance(decoded_skills, str): | |
| skills = [decoded_skills] | |
| elif isinstance(decoded_skills, list) and all( | |
| isinstance(skill, str) for skill in decoded_skills | |
| ): | |
| skills = decoded_skills | |
| else: | |
| skills = [original_skills] |
Prompt To Fix With AI
This is a comment left during a code review.
Path: strix/tools/agents_graph/tools.py
Line: 423-427
Comment:
**Decoded JSON Bypasses List Shape**
When a provider sends valid JSON that is not a string array, `json.loads()` still succeeds. A JSON string is expanded into individual characters by `list(skills)`, an object is treated as its keys, and a mixed array can raise inside `validate_requested_skills()` while formatting heterogeneous values, so `create_agent` can misapply skills or fail instead of returning a controlled validation error.
```suggestion
if isinstance(skills, str):
original_skills = skills
try:
decoded_skills = json.loads(skills)
except json.JSONDecodeError:
skills = [s.strip() for s in skills.split(",") if s.strip()]
else:
if isinstance(decoded_skills, str):
skills = [decoded_skills]
elif isinstance(decoded_skills, list) and all(
isinstance(skill, str) for skill in decoded_skills
):
skills = decoded_skills
else:
skills = [original_skills]
```
How can I resolve this? If you propose a fix, please make it concise.| if isinstance(skills, str): | ||
| try: | ||
| skills = json.loads(skills) | ||
| except json.JSONDecodeError: | ||
| skills = [s.strip() for s in skills.split(",") if s.strip()] |
There was a problem hiding this comment.
Decoded JSON Bypasses List Shape
When skills contains valid non-array JSON, decoding succeeds without enforcing the downstream list[str] contract. A JSON string is split into characters, an object silently becomes its keys, and mixed element types can make validation raise, so load_skill can load the wrong selection or terminate the tool call unexpectedly.
| if isinstance(skills, str): | |
| try: | |
| skills = json.loads(skills) | |
| except json.JSONDecodeError: | |
| skills = [s.strip() for s in skills.split(",") if s.strip()] | |
| if isinstance(skills, str): | |
| original_skills = skills | |
| try: | |
| decoded_skills = json.loads(skills) | |
| except json.JSONDecodeError: | |
| skills = [s.strip() for s in skills.split(",") if s.strip()] | |
| else: | |
| if isinstance(decoded_skills, str): | |
| skills = [decoded_skills] | |
| elif isinstance(decoded_skills, list) and all( | |
| isinstance(skill, str) for skill in decoded_skills | |
| ): | |
| skills = decoded_skills | |
| else: | |
| skills = [original_skills] |
Prompt To Fix With AI
This is a comment left during a code review.
Path: strix/tools/load_skill/tool.py
Line: 31-35
Comment:
**Decoded JSON Bypasses List Shape**
When `skills` contains valid non-array JSON, decoding succeeds without enforcing the downstream `list[str]` contract. A JSON string is split into characters, an object silently becomes its keys, and mixed element types can make validation raise, so `load_skill` can load the wrong selection or terminate the tool call unexpectedly.
```suggestion
if isinstance(skills, str):
original_skills = skills
try:
decoded_skills = json.loads(skills)
except json.JSONDecodeError:
skills = [s.strip() for s in skills.split(",") if s.strip()]
else:
if isinstance(decoded_skills, str):
skills = [decoded_skills]
elif isinstance(decoded_skills, list) and all(
isinstance(skill, str) for skill in decoded_skills
):
skills = decoded_skills
else:
skills = [original_skills]
```
How can I resolve this? If you propose a fix, please make it concise.Greptile review on usestrix#771 flagged that the previous fix accepted any valid JSON, not just string arrays. A JSON object would be silently converted to its keys, an int array would trip downstream validation, and a JSON scalar would be split into characters. Apply the suggested shape-validation fallback: after json.loads, only accept str (wrap in list) or list[str]; otherwise fall back to treating the original string as a single skill name. This keeps the qwen-style JSON-encoded array path working while hardening the other shapes. Cases now handled: '["a", "b"]' -> ['a', 'b'] (target case, unchanged) 'a, b' -> ['a', 'b'] (comma fallback, unchanged) '"a"' -> ['a'] (JSON string scalar) '{"a": 1}' -> ['{"a": 1}'] (was: ['a'] — keys leaked) '[1, 2, 3]' -> ['[1, 2, 3]'] (was: ints passed downstream) '["a", 1, "b"]' -> ['["a", 1, "b"]'] (was: mixed types downstream)
|
Thanks @greptile-apps[bot] for the thorough review — the three flagged regressions (object → keys leak, int array, mixed array) were real. Applied the suggested shape-validation fallback in c1a04fe: After
Otherwise fall back to treating the original string as a single skill name. This preserves the qwen/DashScope JSON-encoded-array path while hardening the other shapes. Verified locally against the flagged cases:
Both |
Summary
Some LLM providers (notably qwen3.7-max via Alibaba DashScope OpenAI-compatible endpoint) serialize array-typed function call parameters as JSON-encoded strings (
'["sql_injection"]') instead of native JSON arrays (["sql_injection"]). This causes Pydantic validation to reject theskillsparameter before the function body executes, making everycreate_agentandload_skillcall fail.This completely breaks multi-agent orchestration — the agent cannot spawn specialist child agents and falls back to serial execution with severely reduced test coverage.
Root Cause
The
skillsparameter is typed aslist[str]in bothcreate_agentandload_skill. When an LLM provider passes it as a string, Pydantic's strict validation rejects it:Fix
Three changes per tool (
create_agent+load_skill):list[str]→str | list[str]— allows Pydantic to accept string inputstrict_mode=Falseto@function_tool— relaxes schema validationThe fallback comma-split handles edge cases where the model passes a bare comma-separated string like
"xss,sqli".Testing
create_agentcalls all failed with Pydantic validation error. Agent could not spawn any child agents.create_agentsuccessfully spawns child agents. Verified with a quick-mode scan where the child agent executed 23exec_commandcalls (curl + Python SQL injection scripts) and completed normally.No regressions for providers that correctly pass arrays (OpenAI, Anthropic) —
isinstance(skills, str)isFalsefor native lists, so the tolerance code is skipped.Files Changed
strix/tools/agents_graph/tools.py—create_agentfunctionstrix/tools/load_skill/tool.py—load_skillfunctionCloses #770