Cat Blog
HomeBlogTools
Search
Language
Choose site style
Choose accent color
Click Effect
Theme

Cat Blog · Updated regularly. Source code is available on GitHub.

Index 04: Hooks — 给工具链装上前后扩展点

July 29th, 2026
AI后端

Series

使用Kotlin从0开发一个ClaudeCode技术文章

Series

使用Kotlin从0开发一个ClaudeCode

Progress 4 / 21

使用Kotlin从0开发一个ClaudeCode

Previous in series

Index 03: Permission — 让智能体做事前先问一声

Next in series

Index 05: TodoWrite — 让智能体先计划后执行

目标

Index 03 让智能体学会了"先问一声"——通过 Permission 管线决定放行/拒绝。但工具执行的链路上还缺一个环节:非功能性扩展。用户可能想在每次工具执行前打日志、测耗时,或在执行后修改结果。

Index 04 的目标是给工具执行链路装上前后两个扩展点:

  1. 定义 HookEvent 统一事件模型,携带事件类型、工具调用、执行结果
  2. 定义 HookResult 三态返回值——Continue(放行)/ Block(阻止,仅 Pre)/ Replace(替换结果,仅 Post)
  3. 实现 HookManager 观察者模式——按事件类型注册多个 handler,顺序执行,fail-open 异常处理
  4. 实现两个内置 Hook——LoggingHook(无状态)和 TimingHook(有状态,演示 Pre/Post 配对)
  5. 通过回调注入 AgentLoop,agent 包零依赖 hooks 包,复刻 s03 的松耦合做法

完成后的效果——智能体每次工具调用都会留下痕迹:

TEXT
[Hook] PreToolUse: read_file (id=toolu_01)
[Hook] Tool 'read_file' executed in 3ms
[Hook] PreToolUse: bash (id=toolu_02)
[Permission] Allow? (y/n/always) y
[Hook] PostToolUse: bash

读文件自动计时,bash 命令仍要先过权限审批——Hooks 在 Permission 之后介入。


架构:可插拔的观察者管线

Index 04 在 Index 03 的工具执行链路上又插入两个可选的扩展点:

TEXT
                            AgentLoop.executeOneTool(toolCall)
                                          │
    ┌──────────────────────────────────────▼──────────────────────────────────┐
    │  hooks.onBeforeToolExecute? ──null──→ 直接进入 Pre Hook                 │
    │       │ (s03 Permission)                                                  │
    │       ▼                                                                  │
    │     false ────────────────────────→ 返回 Permission denied                │
    │       true                                                               │
    │       ▼                                                                  │
    │  hooks.onPreToolUse?  ───null───→ 直接执行工具                            │
    │       │ (s04 PreHook)                                                    │
    │       ▼                                                                  │
    │     reason? ───────────────────────→ 返回 Blocked by hook                 │
    │     null                                                                  │
    │       ▼                                                                  │
    │  ③ ToolRegistry.get(name).execute(input)                                 │
    │       │                                                                  │
    │       ▼                                                                  │
    │     ToolResult (含异常包装)                                              │
    │       │                                                                  │
    │       ▼                                                                  │
    │  hooks.onPostToolUse?  ──null──→ 直接返回原 result                       │
    │       │ (s04 PostHook)                                                   │
    │       ▼                                                                  │
    │     可被 Replace 替换的 ToolResult → 回传 LLM                            │
    └────────────────────────────────────────────────────────────────────────┘

所有可插拔回调收纳在 AgentLoopHooks 数据类中,由 ReplLoop 组装后注入。

层Index 03 状态Index 04 变更
事件模型无HookEvent(data class + Type 枚举)
返回值无HookResult(sealed class: Continue/Block/Replace)
分发器无HookManager(按 Type 分组 + 顺序执行 + fail-open)
内置实现无LoggingHook + TimingHook
智能体1 个 onBeforeToolExecute 回调引入 AgentLoopHooks 收纳全部回调
REPL组装 PermissionPipeline+构建 HookManager 并通过 hooks 注入

第一层:事件与返回值

HookEvent:单个 data class + Type 枚举

KOTLIN
data class HookEvent(
    val type: HookEvent.Type,
    val toolCall: ToolCall,
    val result: ToolResult? = null   // Pre 时 null;Post 时非空
) {
    enum class Type { PRE_TOOL_USE, POST_TOOL_USE }
}

为什么不用 sealed interface 分裂为 PreEvent/PostEvent?

考虑过两种方案:

KOTLIN
// 方案 A(采用):单个 HookEvent,用 result 是否为 null 区分
data class HookEvent(val type: Type, val toolCall: ToolCall, val result: ToolResult? = null)

// 方案 B(弃用):sealed interface 分裂
sealed interface HookEvent {
    data class Pre(val toolCall: ToolCall) : HookEvent
    data class Post(val toolCall: ToolCall, val result: ToolResult) : HookEvent
}

选 A 的理由:

  1. 与设计文档 suspend (HookEvent) -> HookResult 签名一致
  2. 字段重叠度高(都需要 toolCall),分裂会重复
  3. result: ToolResult? 已足够区分 Pre/Post,语义清晰

HookResult:sealed class 三态

KOTLIN
sealed class HookResult {
    object Continue : HookResult()                              // 放行
    data class Block(val reason: String) : HookResult()         // Pre 阻止
    data class Replace(val result: ToolResult) : HookResult()    // Post 替换
}

为什么用 sealed class 而不是 data class?

KOTLIN
// 弃用方案:单 data class
data class HookResult(val block: String? = null, val replace: ToolResult? = null)

弃用理由:存在 block != null && replace != null 的非法状态。sealed class 让三种结果互斥,when 表达式有穷尽性检查,编译期保证所有分支都被处理。

类型不匹配怎么办? PreToolUse handler 返回 Replace(语义不合法)→ HookManager 记 warn 日志并视为 Continue;PostToolUse handler 返回 Block → 同样处理。不抛异常——hook 是非功能性扩展,不应该让一个手抖写错类型的 hook 拖垮工具执行。


第二层:HookManager — 注册-分发-聚合

KOTLIN
class HookManager {
    private val handlers: MutableMap<HookEvent.Type, MutableList<HookHandler>> = mutableMapOf()

    fun on(event: HookEvent.Type, handler: HookHandler) { ... }

    suspend fun fire(event: HookEvent): List<HookResult> {
        // 顺序执行所有 handler,异常视为 Continue
    }

    // 便捷方法:返回原语供 AgentLoop 使用,避免 agent 包 import hooks 包
    suspend fun firePreToolUse(toolCall: ToolCall): String?      // null=放行
    suspend fun firePostToolUse(toolCall: ToolCall, result: ToolResult): ToolResult
}

三个关键设计决策:

1. 顺序执行,不并发

hook 有副作用(日志、计时),需要可读的执行顺序。PostToolUse 多个 Replace 时"后注册赢"的语义需要确定性。hook 数量通常 1-3 个,并发收益可忽略。

2. Fail-open:异常视为 Continue

KOTLIN
try {
    handler(event)
} catch (e: Exception) {
    logger.warn(e) { "Hook handler threw exception for ${event.type}, treating as Continue" }
    HookResult.Continue
}

这是与 s03 PermissionPipeline 的关键对比:

组件异常处理策略理由
PermissionPipelineFail-closed(异常 → DENY)安全决策保守,宁可拒绝也不放过
HookManagerFail-open(异常 → Continue)可观测性增强宽松,不让 buggy hook 拖垮主流程

对比的哲学: 安全决策应该保守,可观测性增强应该宽松。一个抛异常的安全规则可能导致工具被错误拒绝(保守可接受);一个抛异常的日志 hook 不应该让工具调用失败(宽松更合理)。

3. 便捷方法返回原语,不是 HookResult

firePreToolUse 返回 String? 而非 HookResult,firePostToolUse 返回 ToolResult 而非 List<HookResult>。

聚合规则封装在 Manager 内部:

Pre 聚合结果
全部 Continuenull(放行)
任一 Block第一个 Block 的 reason
任一 Replacewarn + 视为 Continue
Post 聚合结果
全部 Continue原 ToolResult
任一 Replace最后一个 Replace 的 result(后注册赢)
任一 Blockwarn + 视为 Continue

为什么返回原语? 因为这些值要传给 AgentLoop,而 AgentLoop 不能 import hooks 包(包依赖规则)。String? 和 ToolResult 都是 tool/标准库类型,让 agent 包零依赖。


第三层:回调注入 — 松耦合集成

与 s03 完全一致的设计哲学:AgentLoop 不 import hooks 包的任何内容。

AgentLoopHooks:回调集合容器

随着 Index 增长,AgentLoop 的可插拔回调会越来越多(s03 权限、s04 Pre/Post Hook,未来还可能有 s06 Subagent、s08 ContextCompact 的回调)。如果每个回调都作为构造函数参数,签名会迅速膨胀。

解决方案:用 data class 收纳所有回调,构造函数只接受一个 hooks 参数。

KOTLIN
data class AgentLoopHooks(
    val onBeforeToolExecute: (suspend (ToolCall) -> Boolean)? = null,
    val onPreToolUse: (suspend (ToolCall) -> String?)? = null,
    val onPostToolUse: (suspend (ToolCall, ToolResult) -> ToolResult)? = null
    // 未来新增回调只改这里,AgentLoop 构造函数签名保持稳定
)

class AgentLoop(
    private val llmProvider: LLMProvider,
    private val systemPrompt: String,
    private val config: AgentConfig = AgentConfig(),
    private val hooks: AgentLoopHooks = AgentLoopHooks()
)

收益:

  • AgentLoop 构造函数稳定在 4 个参数,不随 Index 增长膨胀
  • 默认值 AgentLoopHooks() 禁用全部扩展,保持向后兼容——s01-s02 的测试零改动
  • ReplLoop 用命名参数组装,意图清晰

回调签名设计

KOTLIN
// s03:返回 Boolean,没有原因
onBeforeToolExecute: suspend (ToolCall) -> Boolean

// s04 Pre:返回 String?,携带阻止原因
onPreToolUse: suspend (ToolCall) -> String?

// s04 Post:接收原结果,返回可能修改后的结果
onPostToolUse: suspend (ToolCall, ToolResult) -> ToolResult

为什么 onPreToolUse 返回 String? 而不是 Boolean? 因为 hook 阻止工具时需要带"原因"传给 LLM。LLM 看到 Blocked by hook: rate-limited 比 Permission denied: bash 信息量更大——前者告诉它"换个时机再试",后者只是"不行"。

为什么 onPostToolUse 返回 ToolResult 而不是 ToolResult?? 避免处理 null 的负担。handler 想保持原样就 return result,想替换就 return result.copy(...)。简单直接。

执行顺序固定

KOTLIN
private suspend fun executeOneTool(toolCall: ToolCall): ToolResult {
    // 1. 权限检查(s03)—— 安全闸门,最先执行
    if (hooks.onBeforeToolExecute != null && !hooks.onBeforeToolExecute.invoke(toolCall)) {
        return ToolResult(toolCall.id, "Permission denied: ${toolCall.name}", isError = true)
    }

    // 2. PreToolUse hooks(s04)—— 可观测/可阻止
    if (hooks.onPreToolUse != null) {
        val blockReason = hooks.onPreToolUse.invoke(toolCall)
        if (blockReason != null) {
            return ToolResult(toolCall.id, "Blocked by hook: $blockReason", isError = true)
        }
    }

    // 3. 实际执行工具
    val result = try {
        ToolRegistry.get(toolCall.name).execute(toolCall.input).copy(toolCallId = toolCall.id)
    } catch (e: Exception) {
        ToolResult(toolCall.id, "Error: ${e.message}", isError = true)
    }

    // 4. PostToolUse hooks(s04)—— 可修改结果
    return if (hooks.onPostToolUse != null) hooks.onPostToolUse.invoke(toolCall, result) else result
}

顺序的语义:

情景行为
Permission DENY返回 permission denied;Pre/Post 都不触发(工具根本没尝试)
PreToolUse Block返回 hook blocked;Post 不触发(工具未实际执行)
工具执行抛异常Post 仍触发,handler 可看到 isError=true 的结果
PostToolUse Replace替换后的结果进入 messagesHistory 流向 LLM

为什么 Permission DENY 不触发 Post? 工具根本没执行,"Post Tool Use" 语义不成立。如果 hook 看到一个被拒绝的 toolCall 但 result 是某种占位值,反而容易混淆——干净的语义是"工具未实际执行则不触发 Post"。

为什么工具抛异常时 Post 仍触发? handler 可能需要观测失败、记录指标。result.isError=true 让 handler 知道这是失败结果,可以选择继续记录或改写为更友好的错误消息。

Post 修改的结果如何流向 LLM

TEXT
executeOneTool 返回 ToolResult
    ↓
executeTools 收集结果列表(可能并发)
    ↓
messagesHistory.add(Message(Role.USER, "", toolResults = toolResults))
    ↓
下一轮 LLM 请求看到修改后的 tool_result

修改后天然流向 LLM,无需额外处理。 这是 Index 02 设计的延续——tool_result 块就是工具执行的真相,谁修改了它,下一轮 LLM 就看到什么。


第四层:内置 Hook 示例

LoggingHook(无状态)

KOTLIN
class LoggingHook {
    fun registerTo(manager: HookManager) {
        val pre: HookHandler = { event ->
            logger.debug { "[Hook] PreToolUse: ${event.toolCall.name} (id=${event.toolCall.id})" }
            HookResult.Continue
        }
        val post: HookHandler = { event ->
            val err = if (event.result?.isError == true) " (error)" else ""
            logger.debug { "[Hook] PostToolUse: ${event.toolCall.name}$err" }
            HookResult.Continue
        }
        manager.on(HookEvent.Type.PRE_TOOL_USE, pre)
        manager.on(HookEvent.Type.POST_TOOL_USE, post)
    }
}

无状态,只读不写,始终 Continue。教学意义:展示最简 Hook 模式——pre 和 post 各注册一个 handler,互不影响。

TimingHook(有状态)

KOTLIN
class TimingHook {
    private val startTimes = ConcurrentHashMap<String, Long>()

    val activeTimerCount: Int get() = startTimes.size

    fun registerTo(manager: HookManager) {
        val pre: HookHandler = { event ->
            startTimes[event.toolCall.id] = System.nanoTime()
            HookResult.Continue
        }
        val post: HookHandler = { event ->
            val start = startTimes.remove(event.toolCall.id)
            if (start != null) {
                val elapsedMs = (System.nanoTime() - start) / NANOS_PER_MS
                logger.info { "[Hook] Tool '${event.toolCall.name}' executed in ${elapsedMs}ms" }
            }
            HookResult.Continue
        }
        // ...
    }
}

关键设计:用 ConcurrentHashMap 按 toolCallId 关联 Pre 和 Post。

为什么?因为 AgentLoop 支持并发工具调用(Index 02 的智能并发)。两个 read_file 同时执行时,两个 Pre 事件可能交错触发,两个 Post 事件也会交错。用 toolCall.id 作为 key 可以正确配对——Pre 记录开始时间,Post 取出对应开始时间计算耗时。

为什么 activeTimerCount 暴露给外部? 主要供测试验证。日志输出是脆弱的测试断言目标(依赖 logback 配置),而计时器数量是可验证的行为契约。


踩坑记录

1. HookHandler 是 typealias,不能当构造器调用

KOTLIN
// ❌ 编译错误:typealias 到 SuspendFunction1 没有构造器
val h = HookHandler { event -> HookResult.Continue }

// ✅ 显式标注类型
val h: HookHandler = { event -> HookResult.Continue }

HookHandler 是 typealias HookHandler = suspend (HookEvent) -> HookResult,本质是函数类型别名,不是 class。Kotlin 不允许 typealias XXX { ... } 语法。

2. Kotest 的 shouldNotBe 与 null

最初写测试用 result shouldNotBe null,但 Kotest 没有内联的 shouldNotBe infix 函数处理 null,需要用 shouldNotBeNull() 扩展函数:

KOTLIN
// ❌ 编译错误
postHandler.events[0].result shouldNotBe null

// ✅ 正确
import io.kotest.matchers.nulls.shouldNotBeNull
postHandler.events[0].result.shouldNotBeNull()

3. FakeEchoTool 返回的是 JSON 字面值

KOTLIN
// FakeEchoTool.execute 的实现
val text = input["message"]?.toString() ?: ""
return ToolResult(toolCallId = "fake_id", content = text)

input["message"]?.toString() 取出的是 JSON 字面值 "x"(带引号),不是 x。写测试断言时写成 content shouldBe "x" 会失败,应该是 content shouldBe "\"x\""。

这反映了 kotlinx-serialization 的 API 行为:JsonPrimitive.toString() 返回带引号的 JSON 表示,要用 .jsonPrimitive.content 或 .contentOrNull 才能拿到原始字符串。FakeEchoTool 的实现是测试用的简化版,不需要修正——只需要修正测试断言。

4. 测试代码中避免重复断言

集成测试"Permission 拒绝优先于 Pre hook 阻止"最初写了两行一样的断言:

KOTLIN
deniedResult.content shouldContain "Permission denied"
deniedResult.content shouldContain "Permission denied"  // 重复

复制粘贴时的疏忽。删除重复行即可。

5. 异常容错策略的对称对比

实现时反复纠结"hook 异常应该 fail-open 还是 fail-closed"。最终从设计哲学上想清楚了:

组件失败时的影响策略
Permission 规则异常可能放过本该拒绝的危险操作Fail-closed(视为 DENY)
Hook handler 异常可能少记一次日志或漏掉一次计时Fail-open(视为 Continue)

判断标准:这个组件的失败会让系统更安全还是更危险? 如果失败让系统更危险(漏过危险命令),要 fail-closed;如果失败只是少了可观测性(少记日志),fail-open 更合理。

6. AgentLoopHooks:构造函数膨胀的早期重构

初版实现把三个回调都作为 AgentLoop 构造函数的独立参数:

KOTLIN
// 初版(弃用)
class AgentLoop(
    private val llmProvider: LLMProvider,
    private val systemPrompt: String,
    private val config: AgentConfig = AgentConfig(),
    private val onBeforeToolExecute: (suspend (ToolCall) -> Boolean)? = null,
    private val onPreToolUse: (suspend (ToolCall) -> String?)? = null,
    private val onPostToolUse: (suspend (ToolCall, ToolResult) -> ToolResult)? = null
)

代码评审时发现趋势:s03→s04 从 1 个回调涨到 3 个(+200%),按 CLAUDE.md 路线图,s06 Subagent、s08 ContextCompact、s11 ErrorRecovery 都可能再加回调。到 5-6 个时重构成本会显著上升——"三次法则"支持现在动。

重构方案: 引入 AgentLoopHooks data class 收纳全部回调,AgentLoop 构造函数只接受一个 hooks 参数。未来新增回调只改 data class,构造函数签名保持稳定。这是用最小代价(多一层属性访问 hooks.onPreToolUse)换来长期的可维护性。

为什么不更进一步引入 ToolExecutionInterceptor 接口或直接注入 HookManager? 前者过度抽象(目前只有一种拦截场景),后者违反 agent 包不依赖 hooks 包的约束。data class 是最克制的方案。

关于 Permission 是否合并到 Hook 的评估: 评审时也考虑过把 PermissionPipeline 作为 pre-hook 接入,最终决定不合并。两者关注点正交——Permission 是 fail-closed 的安全决策,Hook 是 fail-open 的可观测性增强;聚合规则相反(任一 DENY 优先 vs 后注册赢);ASK 路径需要用户交互和记忆存储,塞不进 HookResult.Block 的字符串。保留 s03/s04 独立是符合既有设计的。


测试:32 个新用例,131 总计

Index 04 新增 23 个测试(12 HookManager + 6 BuiltInHooks + 5 AgentLoop 集成),加上 s03 代码审查时新增的部分,总用例从 99 增长到 131。

被测组件测试方式用例数
HookManagerFakeHookHandler + ThrowingHookHandler12
LoggingHook + TimingHook行为契约(不验证日志输出)6
AgentLoop hook 集成FakeLLMProvider + FakeEchoTool+5

HookManagerTest 核心用例

KOTLIN
"handler throwing exception is treated as Continue (fail-open)" {
    val manager = HookManager()
    val afterThrow = FakeHookHandler(HookResult.Continue)
    manager.on(HookEvent.Type.PRE_TOOL_USE, ThrowingHookHandler())
    manager.on(HookEvent.Type.PRE_TOOL_USE, afterThrow)

    val results = manager.fire(HookEvent(HookEvent.Type.PRE_TOOL_USE, makeToolCall()))

    // 抛异常的 handler 视为 Continue;后续 handler 仍执行
    results shouldBe listOf(HookResult.Continue, HookResult.Continue)
    afterThrow.callCount shouldBe 1
}

AgentLoop 集成测试关键断言

KOTLIN
"permission denial takes precedence over pre-hook block" {
    val loop = AgentLoop(
        // ...
        hooks = AgentLoopHooks(
            onBeforeToolExecute = { false }, // Permission 拒绝
            onPreToolUse = { "should-not-reach" } // 此 hook 不应被调用
        )
    )

    loop.run("Test")

    // 历史中应是 Permission denied,而非 Blocked by hook
    deniedResult.content shouldContain "Permission denied"
}

这个测试验证了执行顺序:Permission 先执行,拒绝后 Pre hook 根本不会被调用。

BuiltInHooksTest 不验证日志输出

TimingHook 的测试不读日志(脆弱),而是检查 activeTimerCount:

KOTLIN
"TimingHook consumes timer on PostToolUse" {
    manager.firePreToolUse(makeToolCall(id = "t1"))
    manager.firePostToolUse(makeToolCall(id = "t1"), makeResult())
    timing.activeTimerCount shouldBe 0
}

文件清单

TEXT
src/main/kotlin/com/sepcai/code/
├── hooks/                              # [新建] Hook 包
│   ├── HookEvent.kt                    # 事件模型 + Type 枚举
│   ├── HookResult.kt                   # sealed class: Continue/Block/Replace
│   ├── HookHandler.kt                  # typealias suspend (HookEvent) -> HookResult
│   ├── HookManager.kt                  # 分发器 + firePreToolUse/firePostToolUse
│   └── builtins/
│       ├── LoggingHook.kt              # 无状态日志 hook
│       └── TimingHook.kt              # 有状态计时 hook(ConcurrentHashMap)
├── agent/
│   ├── AgentLoopHooks.kt              # [新建] 回调集合 data class,收纳全部可插拔回调
│   └── AgentLoop.kt                   # [修改] 构造函数改用 hooks 参数,executeOneTool 重构
└── repl/
    └── ReplLoop.kt                    # [修改] 构建 HookManager 通过 AgentLoopHooks 注入,欢迎语改 Index 04

src/test/kotlin/com/sepcai/code/
├── agent/
│   └── AgentLoopTest.kt               # [修改] +5 个 hook 集成测试(改用 hooks 命名参数)
└── hooks/                             # [新建]
    ├── HookManagerTest.kt             # 12 个测试
    └── builtins/
        └── BuiltInHooksTest.kt        # 6 个测试

9 个源文件(7 新建 + 2 修改)· 2 个新测试文件 + 1 个测试修改 · 131 个测试 · 全部通过。


下一站

阶段一(最小闭环)已经全部完成。从 s01 到 s04,智能体从"只会说话"成长为"能读会写、安全可控、可观测的工具调用系统"。

但智能体仍然没有"规划能力"——它接到一个复杂任务,会一步一步试探着做,而不是先想清楚再做。Index 04 的 PostToolUse hook 虽然能观测结果,但智能体本身没有"我接下来要做什么"的概念。

Index 05: TodoWrite 将引入待办管理,让智能体先计划后执行。核心设计:

  • TodoItem 数据模型——状态(pending/in_progress/completed)+ 优先级
  • TodoManager 管理器——增删改查,支持 LLM 通过工具调用更新
  • TodoWriteTool 内置工具——让 LLM 显式声明计划,避免走一步看一步

阶段一打下了基础(AgentLoop + Tool + Permission + Hook),阶段二开始让智能体真正具备智能体的能力——规划、子智能体、技能加载、上下文压缩、跨会话记忆。

Hooks 与 TodoWrite 的衔接点:PostToolUse hook 可以观测工具执行后的状态变化,未来可以触发 TodoManager 自动更新待办状态("read_file 完成 → 标记对应的 todo 为 completed")。但这是后续 Index 的任务,s04 只负责把扩展点准备好。

Table of Contents

Current section:目标

  • 1. 目标
  • 2. 架构:可插拔的观察者管线
  • 3. 第一层:事件与返回值
  • 4. HookEvent:单个 data class + Type 枚举
  • 5. HookResult:sealed class 三态
  • 6. 第二层:HookManager — 注册-分发-聚合
  • 7. 1. 顺序执行,不并发
  • 8. 2. Fail-open:异常视为 Continue
  • 9. 3. 便捷方法返回原语,不是 HookResult
  • 10. 第三层:回调注入 — 松耦合集成
  • 11. AgentLoopHooks:回调集合容器
  • 12. 回调签名设计
  • 13. 执行顺序固定
  • 14. Post 修改的结果如何流向 LLM
  • 15. 第四层:内置 Hook 示例
  • 16. LoggingHook(无状态)
  • 17. TimingHook(有状态)
  • 18. 踩坑记录
  • 19. 1. HookHandler 是 typealias,不能当构造器调用
  • 20. 2. Kotest 的 shouldNotBe 与 null
  • 21. 3. FakeEchoTool 返回的是 JSON 字面值
  • 22. 4. 测试代码中避免重复断言
  • 23. 5. 异常容错策略的对称对比
  • 24. 6. AgentLoopHooks:构造函数膨胀的早期重构
  • 25. 测试:32 个新用例,131 总计
  • 26. HookManagerTest 核心用例
  • 27. AgentLoop 集成测试关键断言
  • 28. BuiltInHooksTest 不验证日志输出
  • 29. 文件清单
  • 30. 下一站
Back to top

Related Posts

View all posts
Index 21: Terminal UX —— Claude Code 风格终端交互

Index 21: Terminal UX —— Claude Code 风格终端交互

August 21st, 2026

Index 21 将 cat-code 终端交互升级为 Claude Code 风格,解决旧 REPL 黑箱、无中断及输入体验差的问题。通过 JLine3 与 Mordant 实现历史补全、实时工具可见性、Spinner 状态及 Esc 中断。核心采用 UI 与 AgentLoop 解耦的事件流架构,支持内联权限菜单与优雅降级。同时配置 logback 收敛控制台日志,确保 TUI 清爽且功能无损,显著提升可用性。

Index 20: Comprehensive Agent —— 全机制集成(收口)

Index 20: Comprehensive Agent —— 全机制集成(收口)

August 13th, 2026

作为收官之作,把 s01-s19 的二十个核心机制整合为一个全面智能体:统一的状态查询与运行周期、自治认领与工作区隔离的贯通,以及 /status 命令的全局可视化。全机制协同运转,标志着 Cat-Code 从零完整复刻 Claude Code 核心能力的收官。

Index 19: MCP Plugin —— 多传输 / 通道路由 / 工具池组装

Index 19: MCP Plugin —— 多传输 / 通道路由 / 工具池组装

August 13th, 2026

引入 MCP(Model Context Protocol)插件机制,通过多传输适配与通道路由,把外部 MCP 服务器的工具动态接入智能体的工具池。工具注册从静态编译期扩展为运行时动态组装,让智能体能力随外部服务即插即用。