refactor: migrate hand-rolled error envelopes to shared tool_error()
Replace json.dumps({"error": ...}) boilerplate with the documented
tools/registry.py tool_error() helper across 13 files.
Migrated: 59 sites (58 code sites + 1 docstring example in
path_security.py), incl. multi-key envelopes passed via kwargs
(available_actions, path/already_read, pattern/already_searched,
parameters/hint, needs_reauth/server, error_type/tool/result_type).
Also removed 2 now-redundant local tool_error imports in mcp_tool.py
in favor of a module-level import.
Skipped (not byte/shape-compatible with tool_error):
- {"success": false, "error": ...} envelopes (browser_tool,
browser_camofox, browser_dialog_tool, web_tools, tts_tool,
skills_tool, image_generation_tool, project_tools, memory_tool,
cronjob_tools, x_search_tool, xai_video_tools) — leading keys
differ; key order would change.
- terminal_tool/code_execution_tool envelopes carrying output/
exit_code/status leading keys.
- tool_search.py:912-area multi-key success paths (non-error).
- mcp_tool.py MCPSampling._error — returns MCP-spec ErrorData
object, not a JSON string; incompatible.
- send_message_tool._error — returns a dict (not str) and applies
secret redaction; return type must be preserved.
Behavior note: sites that previously omitted ensure_ascii=False now
emit raw UTF-8 (tool_error's canonical behavior) — JSON-equivalent.
Tests: 23 targeted files (tool_search, discord, file_tools/read
guards/operations, registry, clarify, homeassistant, code_execution,
send_message, delegate, terminal, mcp, model_tools, sanitize_tool_error,
retaindb plugin) — all pass. ruff clean.
This commit is contained in:
+10
-13
@@ -29,7 +29,7 @@ import threading
|
||||
import time
|
||||
from typing import Dict, Any, List, Optional, Tuple
|
||||
|
||||
from tools.registry import discover_builtin_tools, registry
|
||||
from tools.registry import discover_builtin_tools, registry, tool_error
|
||||
from toolsets import resolve_toolset, validate_toolset
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -1170,8 +1170,7 @@ def handle_function_call(
|
||||
if function_name == _ts_mod.TOOL_CALL_NAME:
|
||||
underlying_name, underlying_args, err = _ts_mod.resolve_underlying_call(function_args or {})
|
||||
if err or not underlying_name:
|
||||
return json.dumps({"error": err or "tool_call could not be resolved"},
|
||||
ensure_ascii=False)
|
||||
return tool_error(err or "tool_call could not be resolved")
|
||||
# Defense in depth: the underlying tool MUST be in the session's
|
||||
# scoped deferrable catalog. resolve_underlying_call() only checks
|
||||
# that the name is deferrable in the global registry; this gate
|
||||
@@ -1180,12 +1179,10 @@ def handle_function_call(
|
||||
# the bridge even if the catalog scoping above regressed.
|
||||
_scoped_deferrable = _ts_mod.scoped_deferrable_names(current_defs)
|
||||
if underlying_name not in _scoped_deferrable:
|
||||
return json.dumps({
|
||||
"error": (
|
||||
f"'{underlying_name}' is not available in this session. "
|
||||
"Use tool_search to find tools you can call."
|
||||
),
|
||||
}, ensure_ascii=False)
|
||||
return tool_error(
|
||||
f"'{underlying_name}' is not available in this session. "
|
||||
"Use tool_search to find tools you can call."
|
||||
)
|
||||
# Probe-validate against the deferred tool's schema (ironclaw#5149):
|
||||
# a blind call missing required arguments returns the parameter
|
||||
# schema instead of dispatching into an opaque downstream failure.
|
||||
@@ -1232,7 +1229,7 @@ def handle_function_call(
|
||||
|
||||
try:
|
||||
if function_name in _AGENT_LOOP_TOOLS:
|
||||
return json.dumps({"error": f"{function_name} must be handled by the agent loop"})
|
||||
return tool_error(f"{function_name} must be handled by the agent loop")
|
||||
|
||||
# Check plugin hooks for a block/approve directive (unless caller
|
||||
# already checked — e.g. run_agent._invoke_tool passes skip=True to
|
||||
@@ -1263,7 +1260,7 @@ def handle_function_call(
|
||||
logger.debug("pre_tool_call hook error: %s", _hook_err)
|
||||
|
||||
if block_message is not None:
|
||||
result = json.dumps({"error": block_message}, ensure_ascii=False)
|
||||
result = tool_error(block_message)
|
||||
_emit_post_tool_call_hook(
|
||||
function_name=function_name,
|
||||
function_args=function_args,
|
||||
@@ -1292,7 +1289,7 @@ def handle_function_call(
|
||||
except Exception as _edit_approval_err:
|
||||
logger.debug("ACP edit approval guard error: %s", _edit_approval_err)
|
||||
if function_name in {"write_file", "patch"}:
|
||||
return json.dumps({"error": "Edit approval denied: approval guard failed"}, ensure_ascii=False)
|
||||
return tool_error("Edit approval denied: approval guard failed")
|
||||
|
||||
# Notify the read-loop tracker when a non-read/search tool runs,
|
||||
# so the *consecutive* counter resets (reads after other work are fine).
|
||||
@@ -1419,7 +1416,7 @@ def handle_function_call(
|
||||
except Exception as e:
|
||||
error_msg = f"Error executing {function_name}: {str(e)}"
|
||||
logger.exception(error_msg)
|
||||
return json.dumps({"error": _sanitize_tool_error(error_msg)}, ensure_ascii=False)
|
||||
return tool_error(_sanitize_tool_error(error_msg))
|
||||
|
||||
|
||||
# =============================================================================
|
||||
|
||||
Reference in New Issue
Block a user