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:
teknium1
2026-07-29 09:02:14 -07:00
committed by Teknium
parent f57cb2e482
commit 1a7f73b8ea
13 changed files with 181 additions and 251 deletions
+10 -13
View File
@@ -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))
# =============================================================================