From 82c2a45a08445d3679248b02aa3955270b5ab052 Mon Sep 17 00:00:00 2001 From: zwpdbh Date: Tue, 9 Jun 2026 13:11:12 +0800 Subject: [PATCH] feat(hooks): surface PostToolUse hook stderr to LLM context MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PostToolUse hooks were fire-and-forget: their stdout/stderr were captured but discarded. This made it impossible for hooks like docref to report drift back to the LLM so it could act on the information. Now PostToolUse hooks are awaited (not fire-and-forget) and any stderr they produce is appended to the tool result's .message field, which the LLM sees in the next turn's context. This enables hooks to act as reporters rather than actors — they can detect state changes and feed structured observations to the LLM, which then decides what tools to call next. --- src/kimi_cli/soul/toolset.py | 42 +++++++++++++++++++++++------------- 1 file changed, 27 insertions(+), 15 deletions(-) diff --git a/src/kimi_cli/soul/toolset.py b/src/kimi_cli/soul/toolset.py index ff2231fba..5ef78fc5e 100644 --- a/src/kimi_cli/soul/toolset.py +++ b/src/kimi_cli/soul/toolset.py @@ -458,22 +458,34 @@ async def _call(): dup_type="cross_step" if is_cross_step_dup else "normal", ) - # --- PostToolUse (fire-and-forget) --- - _hook_task = asyncio.create_task( - self._hook_engine.trigger( - "PostToolUse", - matcher_value=tool_name, - input_data=events.post_tool_use( - session_id=_get_session_id(), - cwd=str(Path.cwd()), - tool_name=tool_name, - tool_input=tool_input_dict, - tool_output=str(ret)[:2000], - tool_call_id=tool_call.id, - ), - ) + # --- PostToolUse (awaited, not fire-and-forget) --- + # Hooks run after the tool completes. Their stderr is appended + # to the tool result message so the LLM can see and act on it. + hook_results = await self._hook_engine.trigger( + "PostToolUse", + matcher_value=tool_name, + input_data=events.post_tool_use( + session_id=_get_session_id(), + cwd=str(Path.cwd()), + tool_name=tool_name, + tool_input=tool_input_dict, + tool_output=str(ret)[:2000], + tool_call_id=tool_call.id, + ), ) - _hook_task.add_done_callback(lambda t: t.exception() if not t.cancelled() else None) + + # Collect non-empty stderr from hooks for LLM visibility + hook_stderr_lines: list[str] = [] + for hr in hook_results: + if hr.stderr.strip(): + hook_stderr_lines.append(hr.stderr.strip()) + + if hook_stderr_lines: + hook_output = "\n".join(hook_stderr_lines) + if ret.message: + ret.message = f"{ret.message}\n\n[post-tool-use-hooks]\n{hook_output}" + else: + ret.message = f"[post-tool-use-hooks]\n{hook_output}" return ToolResult(tool_call_id=tool_call.id, return_value=ret)