Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ Only write entries that are worth mentioning to users.

## Unreleased

- Core: Include the assistant's final response text and stop reason in the Stop hook payload — hook scripts can now inspect turn outcomes

## 1.44.0 (2026-05-13)

- Shell: Add slash command alias resolution — aliases such as `/h`, `?`, and `status` now resolve to their canonical commands (`/help`, `/usage`); the completer and help output display alias matches as `/name (alias)` for clarity
Expand Down
2 changes: 1 addition & 1 deletion docs/en/customization/hooks.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ Kimi Code CLI supports 13 lifecycle events:
| `PostToolUse` | After successful tool execution | Tool name | `tool_name`, `tool_input`, `tool_output` |
| `PostToolUseFailure` | After tool execution fails | Tool name | `tool_name`, `tool_input`, `error` |
| `UserPromptSubmit` | Before user input is processed | None | `prompt` |
| `Stop` | When Agent turn ends | None | `stop_hook_active` |
| `Stop` | When Agent turn ends | None | `stop_hook_active`, `response`, `stop_reason` |
Comment thread
AkaCoder404 marked this conversation as resolved.
| `StopFailure` | When turn ends due to error | Error type | `error_type`, `error_message` |
| `SessionStart` | When session is created/resumed | Source (`startup`/`resume`) | `source` |
| `SessionEnd` | When session closes | Reason | `reason` |
Expand Down
2 changes: 2 additions & 0 deletions docs/en/release-notes/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ This page documents the changes in each Kimi Code CLI release.

## Unreleased

- Core: Include the assistant's final response text and stop reason in the Stop hook payload — hook scripts can now inspect turn outcomes

## 1.44.0 (2026-05-13)

- Shell: Add slash command alias resolution — aliases such as `/h`, `?`, and `status` now resolve to their canonical commands (`/help`, `/usage`); the completer and help output display alias matches as `/name (alias)` for clarity
Expand Down
2 changes: 1 addition & 1 deletion docs/zh/customization/hooks.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ Kimi Code CLI 支持 13 种生命周期事件:
| `PostToolUse` | 工具成功执行后 | 工具名称 | `tool_name`, `tool_input`, `tool_output` |
| `PostToolUseFailure` | 工具执行失败后 | 工具名称 | `tool_name`, `tool_input`, `error` |
| `UserPromptSubmit` | 用户提交输入前 | 无 | `prompt` |
| `Stop` | Agent 轮次结束时 | 无 | `stop_hook_active` |
| `Stop` | Agent 轮次结束时 | 无 | `stop_hook_active`, `response`, `stop_reason` |
| `StopFailure` | 轮次因错误结束时 | 错误类型 | `error_type`, `error_message` |
| `SessionStart` | 会话创建/恢复时 | 来源 (`startup`/`resume`) | `source` |
| `SessionEnd` | 会话关闭时 | 原因 | `reason` |
Expand Down
2 changes: 2 additions & 0 deletions docs/zh/release-notes/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

## 未发布

- Core:Stop 钩子 payload 现在包含 Assistant 的最终回复文本和停止原因——钩子脚本可以据此检查轮次结果

## 1.44.0 (2026-05-13)

- Shell:新增斜杠命令别名解析——别名(如 `/h`、`?`、`status`)现在能正确解析到对应的正式命令(`/help`、`/usage`);补全器和帮助输出会将别名匹配项显示为 `/name (alias)`,方便识别
Expand Down
4 changes: 4 additions & 0 deletions src/kimi_cli/hooks/events.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,10 +75,14 @@ def stop(
session_id: str,
cwd: str,
stop_hook_active: bool = False,
response: str = "",
stop_reason: str = "",
) -> dict[str, Any]:
return {
**_base("Stop", session_id, cwd),
"stop_hook_active": stop_hook_active,
"response": response,
"stop_reason": stop_reason,
}


Expand Down
12 changes: 11 additions & 1 deletion src/kimi_cli/soul/kimisoul.py
Original file line number Diff line number Diff line change
Expand Up @@ -632,6 +632,7 @@ async def run(
_track_telemetry("turn_started", mode="plan" if self._plan_mode else "agent")
user_message = Message(role="user", content=user_input)
text_input = user_message.extract_text(" ").strip()
turn_outcome: TurnOutcome | None = None

if command_call := parse_slash_command_call(text_input):
command = self._find_slash_command(command_call.name)
Expand All @@ -649,16 +650,25 @@ async def run(
)
await runner.run(self, "")
else:
await self._turn(user_message)
turn_outcome = await self._turn(user_message)
Comment thread
AkaCoder404 marked this conversation as resolved.
Comment thread
AkaCoder404 marked this conversation as resolved.

# --- Stop hook (max 1 re-trigger to prevent infinite loop) ---
if not self._stop_hook_active:
stop_response = ""
stop_reason = ""
if turn_outcome is not None:
stop_reason = turn_outcome.stop_reason
if turn_outcome.final_message is not None:
stop_response = turn_outcome.final_message.extract_text(" ").strip()[:500]

stop_results = await self._hook_engine.trigger(
"Stop",
input_data=events.stop(
session_id=self._runtime.session.id,
cwd=str(Path.cwd()),
stop_hook_active=False,
response=stop_response,
stop_reason=stop_reason,
),
Comment thread
AkaCoder404 marked this conversation as resolved.
)
for result in stop_results:
Expand Down
61 changes: 61 additions & 0 deletions tests/core/test_kimisoul_hooks.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
"""Tests for KimiSoul lifecycle hook integration."""

from __future__ import annotations

from pathlib import Path
from unittest.mock import AsyncMock

import pytest
from kosong.message import Message
from kosong.tooling.empty import EmptyToolset

import kimi_cli.soul.kimisoul as kimisoul_module
from kimi_cli.hooks.config import HookDef
from kimi_cli.hooks.engine import HookEngine
from kimi_cli.soul.agent import Agent, Runtime
from kimi_cli.soul.context import Context
from kimi_cli.soul.kimisoul import KimiSoul, TurnOutcome
from kimi_cli.wire.types import TextPart


@pytest.mark.asyncio
async def test_stop_hook_includes_response_and_stop_reason(
runtime: Runtime, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
"""Stop hook receives response text and stop_reason from TurnOutcome."""
captured_payloads: list[dict] = []

async def _capture_trigger(
event: str, *, matcher_value: str = "", input_data: dict | None = None
) -> list:
captured_payloads.append(input_data or {})
return []

agent = Agent(
name="Test Agent",
system_prompt="Test system prompt.",
toolset=EmptyToolset(),
runtime=runtime,
)
soul = KimiSoul(agent, context=Context(file_backend=tmp_path / "history.jsonl"))
soul._turn = AsyncMock( # type: ignore[method-assign]
return_value=TurnOutcome(
stop_reason="no_tool_calls",
final_message=Message(
role="assistant", content=[TextPart(text="The fix is complete.")]
),
step_count=1,
)
)
monkeypatch.setattr(kimisoul_module, "wire_send", lambda _msg: None)

hook_engine = HookEngine([HookDef(event="Stop", command="echo ok", timeout=5)])
monkeypatch.setattr(hook_engine, "trigger", _capture_trigger)
soul.set_hook_engine(hook_engine)

await soul.run([TextPart(text="fix the bug")])

stop_payloads = [p for p in captured_payloads if p.get("hook_event_name") == "Stop"]
assert len(stop_payloads) == 1
assert stop_payloads[0].get("response") == "The fix is complete."
assert stop_payloads[0].get("stop_reason") == "no_tool_calls"