diff --git a/strix/agents/factory.py b/strix/agents/factory.py index e40e60e8..9d599a54 100644 --- a/strix/agents/factory.py +++ b/strix/agents/factory.py @@ -160,7 +160,9 @@ def _schema_types(spec: dict[str, Any]) -> set[str]: def _decode_structured(value: str, types: set[str]) -> Any: stripped = value.strip() if not stripped: - return value + # An empty string is the model's "no value" for a list/dict param; give it + # the empty container so it validates instead of failing the type check. + return [] if "array" in types else {} try: decoded = json.loads(stripped) except json.JSONDecodeError: diff --git a/tests/test_agent_factory_tool_arguments.py b/tests/test_agent_factory_tool_arguments.py index 49dabf5d..70908f26 100644 --- a/tests/test_agent_factory_tool_arguments.py +++ b/tests/test_agent_factory_tool_arguments.py @@ -70,7 +70,6 @@ async def test_encoded_list_is_decoded_for_an_array_parameter(schema: dict[str, "auth", "Endpoint /admin leaks user data, and session tokens never expire", '"auth"', - "", ], ) async def test_free_form_strings_are_never_split_into_an_array(value: str) -> None: @@ -79,6 +78,29 @@ async def test_free_form_strings_are_never_split_into_an_array(value: str) -> No assert parsed["tags"] == value +@pytest.mark.asyncio +@pytest.mark.parametrize("schema", [_ARRAY, _NULLABLE_ARRAY]) +@pytest.mark.parametrize("value", ["", " "]) +async def test_empty_string_becomes_an_empty_array(schema: dict[str, Any], value: str) -> None: + parsed = await _roundtrip(schema, {"tags": value}) + + assert parsed["tags"] == [] + + +@pytest.mark.asyncio +async def test_empty_string_becomes_an_empty_object() -> None: + parsed = await _roundtrip(_OBJECT, {"modifications": ""}) + + assert parsed["modifications"] == {} + + +@pytest.mark.asyncio +async def test_empty_string_for_a_string_parameter_is_untouched() -> None: + parsed = await _roundtrip(_STRING, {"todos": ""}) + + assert parsed["todos"] == "" + + @pytest.mark.asyncio async def test_encoded_mapping_is_decoded_for_an_object_parameter() -> None: parsed = await _roundtrip(_OBJECT, {"modifications": '{"method": "POST"}'})