fix(tools): accept both the string and structured form of every tool argument (#957)

Co-authored-by: Ahmed Allam <ahmed39652003@gmail.com>
This commit is contained in:
devin-ai-integration[bot]
2026-08-01 18:53:24 -07:00
committed by GitHub
co-authored by Ahmed Allam
parent 2e7040240d
commit c240068c2c
2 changed files with 210 additions and 4 deletions
+86 -4
View File
@@ -143,6 +143,83 @@ def _with_bounded_result(tool: FunctionTool) -> FunctionTool:
return tool
def _schema_types(spec: dict[str, Any]) -> set[str]:
types: set[str] = set()
raw = spec.get("type")
if isinstance(raw, str):
types.add(raw)
elif isinstance(raw, list):
types.update(t for t in raw if isinstance(t, str))
for variant in spec.get("anyOf") or ():
if isinstance(variant, dict):
types |= _schema_types(variant)
types.discard("null")
return types
def _decode_structured(value: str, types: set[str]) -> Any:
stripped = value.strip()
if not stripped:
return value
try:
decoded = json.loads(stripped)
except json.JSONDecodeError:
return value
wanted = list if "array" in types else dict
return decoded if isinstance(decoded, wanted) else value
def _coerce_argument(value: Any, spec: dict[str, Any]) -> Any:
types = _schema_types(spec)
if not types or value is None:
return value
if isinstance(value, list | dict) and "string" in types and not types & {"array", "object"}:
return json.dumps(value, ensure_ascii=False)
if isinstance(value, str) and types & {"array", "object"} and "string" not in types:
return _decode_structured(value, types)
return value
def _coerce_arguments(raw_input: str, schema: dict[str, Any]) -> str:
properties = schema.get("properties")
if not isinstance(properties, dict) or not properties:
return raw_input
try:
payload = json.loads(raw_input) if raw_input else None
except json.JSONDecodeError:
return raw_input
if not isinstance(payload, dict):
return raw_input
changed = False
for key, value in payload.items():
spec = properties.get(key)
if not isinstance(spec, dict):
continue
coerced = _coerce_argument(value, spec)
if coerced is not value:
payload[key] = coerced
changed = True
if not changed:
return raw_input
return json.dumps(payload, ensure_ascii=False)
def _with_coerced_arguments(tool: FunctionTool) -> FunctionTool:
if getattr(tool, "_strix_coerced", False):
return tool
invoke_tool = tool.on_invoke_tool
schema = tool.params_json_schema
async def invoke(ctx: Any, raw_input: str) -> Any:
return await invoke_tool(ctx, _coerce_arguments(raw_input, schema))
tool.on_invoke_tool = invoke
tool._strix_coerced = True # type: ignore[attr-defined]
return tool
def _function_tool_with_error_result(tool: FunctionTool) -> FunctionTool:
invoke_tool = tool.on_invoke_tool
@@ -212,11 +289,13 @@ def _configure_filesystem_tools(toolset: Any, *, chat_completions: bool) -> None
if isinstance(tool, CustomTool):
setattr(toolset, name, _custom_tool_as_function_tool(tool))
elif isinstance(tool, FunctionTool):
setattr(toolset, name, _function_tool_with_error_result(tool))
setattr(
toolset, name, _function_tool_with_error_result(_with_coerced_arguments(tool))
)
elif isinstance(tool, CustomTool):
setattr(toolset, name, _bound_custom_tool(tool))
elif isinstance(tool, FunctionTool):
setattr(toolset, name, _with_bounded_result(tool))
setattr(toolset, name, _with_bounded_result(_with_coerced_arguments(tool)))
def _make_filesystem_configurator(*, chat_completions: bool) -> Any:
@@ -329,7 +408,7 @@ def _configure_shell_tools(toolset: Any, *, chat_completions: bool) -> None:
for name, tool in vars(toolset).items():
if not isinstance(tool, FunctionTool):
continue
wrapped = tool
wrapped = _with_coerced_arguments(tool)
if tool.name == "exec_command":
wrapped = _wrap_exec_command(wrapped)
elif tool.name == "write_stdin":
@@ -523,7 +602,10 @@ def build_strix_agent(
tools = [*_BASE_TOOLS, *agent_tools, agent_finish]
_ensure_unique_tool_names(tools)
tools = [
_with_bounded_result(tool) if isinstance(tool, FunctionTool) else tool for tool in tools
_with_bounded_result(_with_coerced_arguments(tool))
if isinstance(tool, FunctionTool)
else tool
for tool in tools
]
logger.info(