feat(plugins): add transform_tool_result hook for generic tool-result rewriting (#12972)

Closes #8933 more fully, extending the per-tool transform_terminal_output
hook from #12929 to a generic seam that fires after every tool dispatch.
Plugins can rewrite any tool's result string (normalize formats, redact
fields, summarize verbose output) without wrapping individual tools.

Changes
- hermes_cli/plugins.py: add "transform_tool_result" to VALID_HOOKS
- model_tools.py: invoke the hook in handle_function_call after
  post_tool_call (which remains observational); first valid str return
  replaces the result; fail-open
- tests/test_transform_tool_result_hook.py: 9 new tests covering no-op,
  None return, non-string return, first-match wins, kwargs, hook
  exception fallback, post_tool_call observation invariant, ordering
  vs post_tool_call, and an end-to-end real-plugin integration
- tests/hermes_cli/test_plugins.py: assert new hook in VALID_HOOKS
- tests/test_model_tools.py: extend the hook-call-sequence assertion
  to include the new hook

Design
- transform_tool_result runs AFTER post_tool_call so observers always
  see the original (untransformed) result. This keeps post_tool_call's
  observational contract.
- transform_terminal_output (from #12929) still runs earlier, inside
  terminal_tool, so plugins can canonicalize BEFORE the 50k truncation
  drops middle content. Both hooks coexist; they target different layers.
This commit is contained in:
Teknium
2026-04-20 03:48:08 -07:00
committed by GitHub
parent 9f22977fc0
commit 04068c5891
6 changed files with 222 additions and 0 deletions

View File

@ -550,6 +550,30 @@ def handle_function_call(
except Exception:
pass
# Generic tool-result canonicalization seam: plugins receive the
# final result string (JSON, usually) and may replace it by
# returning a string from transform_tool_result. Runs after
# post_tool_call (which stays observational) and before the result
# is appended back into conversation context. Fail-open; the first
# valid string return wins; non-string returns are ignored.
try:
from hermes_cli.plugins import invoke_hook
hook_results = invoke_hook(
"transform_tool_result",
tool_name=function_name,
args=function_args,
result=result,
task_id=task_id or "",
session_id=session_id or "",
tool_call_id=tool_call_id or "",
)
for hook_result in hook_results:
if isinstance(hook_result, str):
result = hook_result
break
except Exception:
pass
return result
except Exception as e: