Skip to content

fix: tolerate string-encoded skills param for LLM provider compatibility#771

Open
jinglun010-cpu wants to merge 2 commits into
usestrix:mainfrom
jinglun010-cpu:fix/create-agent-skills-string-tolerance
Open

fix: tolerate string-encoded skills param for LLM provider compatibility#771
jinglun010-cpu wants to merge 2 commits into
usestrix:mainfrom
jinglun010-cpu:fix/create-agent-skills-string-tolerance

Conversation

@jinglun010-cpu

Copy link
Copy Markdown

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 the skills parameter before the function body executes, making every create_agent and load_skill call 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 skills parameter is typed as list[str] in both create_agent and load_skill. When an LLM provider passes it as a string, Pydantic's strict validation rejects it:

Invalid JSON input for tool create_agent: 1 validation error for create_agent_args
skills
  Input should be a valid list [type=list_type, input_value='["sql_injection"]', input_type=str]

Fix

Three changes per tool (create_agent + load_skill):

  1. Widen type annotation: list[str]str | list[str] — allows Pydantic to accept string input
  2. Add strict_mode=False to @function_tool — relaxes schema validation
  3. Parse string in function body:
    if isinstance(skills, str):
        try:
            skills = json.loads(skills)
        except json.JSONDecodeError:
            skills = [s.strip() for s in skills.split(",") if s.strip()]

The fallback comma-split handles edge cases where the model passes a bare comma-separated string like "xss,sqli".

Testing

  • Before fix: 6 consecutive create_agent calls all failed with Pydantic validation error. Agent could not spawn any child agents.
  • After fix: create_agent successfully spawns child agents. Verified with a quick-mode scan where the child agent executed 23 exec_command calls (curl + Python SQL injection scripts) and completed normally.

No regressions for providers that correctly pass arrays (OpenAI, Anthropic) — isinstance(skills, str) is False for native lists, so the tolerance code is skipped.

Files Changed

  • strix/tools/agents_graph/tools.pycreate_agent function
  • strix/tools/load_skill/tool.pyload_skill function

Closes #770

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-apps

greptile-apps Bot commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR makes skill arguments compatible with providers that encode arrays as strings. The main changes are:

  • Widen skills to accept strings in create_agent and load_skill.
  • Disable strict tool validation for both functions.
  • Decode JSON strings with a comma-separated fallback.

Confidence Score: 4/5

The decoded skill values need shape validation before merging.

  • JSON-encoded string arrays follow the intended path.
  • Valid JSON scalars, objects, and mixed arrays can violate the downstream list[str] contract.
  • Both changed tools can misinterpret input or fail unexpectedly.

strix/tools/agents_graph/tools.py and strix/tools/load_skill/tool.py

Important Files Changed

Filename Overview
strix/tools/agents_graph/tools.py Adds string decoding for create_agent, but decoded values are not checked against the required string-list shape.
strix/tools/load_skill/tool.py Adds matching compatibility parsing for load_skill, with the same missing decoded-value validation.
Prompt To Fix All With AI
Fix 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

Comment on lines +423 to +427
if isinstance(skills, str):
try:
skills = json.loads(skills)
except json.JSONDecodeError:
skills = [s.strip() for s in skills.split(",") if s.strip()]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 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.

Suggested change
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.

Comment on lines +31 to +35
if isinstance(skills, str):
try:
skills = json.loads(skills)
except json.JSONDecodeError:
skills = [s.strip() for s in skills.split(",") if s.strip()]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 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.

Suggested change
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)
@jinglun010-cpu

Copy link
Copy Markdown
Author

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 json.loads, only accept:

  • str → wrap in list ('"a"'['a'])
  • list[str] with all-string elements → use as-is

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:

input before after
'{"a": 1}' ['a'] (keys leaked) ['{"a": 1}']
'[1, 2, 3]' [1, 2, 3] (ints downstream) ['[1, 2, 3]']
'["a", 1, "b"]' mixed downstream ['["a", 1, "b"]']
'["a", "b"]' ['a', 'b'] ['a', 'b'] (unchanged)
'a, b' ['a', 'b'] ['a', 'b'] (unchanged)

Both create_agent and load_skill patched identically. Ready for another look.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[BUG] create_agent fails with Pydantic validation error when LLM passes skills as JSON string instead of list

1 participant