Files
picoclaw/docs/hooks/hook-json-protocol.zh.md
T
Harmoon ee29aaa871 Enhance hooks with respond action and comprehensive documentation (#2215)
* feat(hooks): add respond action for tool execution bypass

Add a new HookActionRespond that allows hooks to return tool results directly, skipping actual tool execution. This enables plugin tool injection, caching, and mocking capabilities.

- Add HookActionRespond constant and support in HookManager
- Extend ToolCallHookRequest with HookResult field
- Implement respond action handling in process hooks and agent loop
- Add comprehensive tests for respond and deny_tool actions
- Update documentation with hook actions table and examples

* docs(hooks): add JSON-RPC protocol and plugin tool injection documentation

Add comprehensive documentation for hook JSON-RPC protocol and plugin tool injection capabilities:

- Add "Hook Actions" section to README.zh.md explaining respond action for tool execution bypass
- Create hook-json-protocol.md/.zh.md detailing JSON-RPC 2.0 protocol for all hook methods
- Create plugin-tool-injection.md/.zh.md with complete examples for external tool implementation
- Document how hooks can inject tool definitions and return results via respond action
- Include Python and Go examples for weather query plugin implementation

* feat(agent): emit tool events and feedback for hook results

Add ToolExecStart event emission and tool feedback for hook results to ensure consistent behavior between normal tool execution and hook bypass scenarios. This maintains parity in event tracking and user feedback when tools are executed via hooks.

* style(agent): format whitespace in hook structs and constants

Remove trailing whitespace and standardize spacing in JSON struct tags, constants, and test data for improved code consistency.

* feat(hooks): add media support for plugin tool injection

Extend the hook respond action to support media file handling:
- Add `media` field for returning images and files from hooks
- Add `response_handled` field to control turn completion behavior
- When response_handled=true, media is automatically delivered to user
- When response_handled=false, media is passed to LLM for vision requests

This enables plugins to directly return generated images, downloaded
files, and other media content either to users or for LLM analysis.

* docs(hooks): document security implications of respond action

Add security boundary documentation explaining that the respond action
bypasses ApproveTool checks, allowing hooks to return results for any
tool without approval. Include recommendations for secure hook
implementation and code comments marking the security considerations.

Changes:
- Add "Security Boundaries" section to plugin-tool-injection docs
- Document bypass of approval checks and associated risks
- Provide security recommendations and example code
- Add inline security comments in hooks.go and loop.go

* refactor(agent): improve completeness of tool result cloning and hook processing

Extend cloneToolResult to properly copy ArtifactTags and Messages fields,
ensuring deep copies of all ToolResult data. Consolidate event emission
and user message handling to match the normal tool execution flow.

* fix(agent): align hook respond path with normal tool execution flow

The hook respond code path was missing several critical behaviors that
existed in normal tool execution:

- Add logging for tool calls with arguments preview
- Add is_tool_call metadata to user-facing messages
- Handle attachment delivery failures by setting error state and
  notifying LLM
- Set ResponseHandled=false when using bus for media delivery
- Check for steering messages and graceful interrupts after tool
  execution, skipping remaining tools when appropriate
- Poll for SubTurn results that arrived during tool execution

This ensures consistent behavior between hook-responded tool calls and
normally executed tool calls.

* test(agent): add tests for hook respond media error handling

Add comprehensive tests for the hook respond code path when media
delivery fails. Tests cover error media channel scenarios and verify
proper error state handling.

Also document that AfterTool is not called when using respond action,
as it provides the final answer directly (design decision).
2026-04-08 11:47:02 +08:00

12 KiB
Raw Blame History

Hook JSON-RPC 协议详解

所有 hook 使用 JSON-RPC 2.0 格式,每行一个 JSON 消息,通过 stdio 传输。


基础协议结构

请求(PicoClaw → Hook

{"jsonrpc":"2.0","id":1,"method":"hook.xxx","params":{...}}

响应(Hook → PicoClaw

成功:

{"jsonrpc":"2.0","id":1,"result":{...}}

错误:

{"jsonrpc":"2.0","id":1,"error":{"code":-32000,"message":"错误信息"}}

1. hook.hello(握手)

启动时必须完成握手,否则 hook 进程会被终止。

请求

{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "hook.hello",
  "params": {
    "name": "py_review_gate",
    "version": 1,
    "modes": ["observe", "tool", "approve"]
  }
}
字段 说明
name hook 名称(来自配置)
version 协议版本,当前为 1
modes hook 支持的能力模式

响应

{
  "jsonrpc": "2.0",
  "id": 1,
  "result": {
    "ok": true,
    "name": "python-review-gate"
  }
}

2. hook.before_llm

在发送请求给 LLM 之前触发。可用于注入工具。

请求

{
  "jsonrpc": "2.0",
  "id": 2,
  "method": "hook.before_llm",
  "params": {
    "meta": {
      "AgentID": "agent-1",
      "TurnID": "turn-1",
      "ParentTurnID": "",
      "SessionKey": "session-1",
      "Iteration": 0,
      "TracePath": "runTurn",
      "Source": "turn.llm.request"
    },
    "model": "claude-sonnet",
    "messages": [
      {"role": "user", "content": "hello"}
    ],
    "tools": [
      {
        "type": "function",
        "function": {
          "name": "echo",
          "description": "echo text",
          "parameters": {"type": "object"}
        }
      }
    ],
    "options": {
      "temperature": 0.7
    },
    "channel": "cli",
    "chat_id": "chat-1",
    "graceful_terminal": false
  }
}
字段 说明
meta 事件元数据,用于追踪
model 请求的模型名称
messages 对话历史
tools 可用工具定义列表
options LLM 参数(temperature、max_tokens 等)
channel 请求来源通道
chat_id 会话 ID

响应(注入工具示例)

{
  "jsonrpc": "2.0",
  "id": 2,
  "result": {
    "action": "modify",
    "request": {
      "model": "claude-sonnet",
      "messages": [{"role": "user", "content": "hello"}],
      "tools": [
        {
          "type": "function",
          "function": {
            "name": "echo",
            "description": "echo",
            "parameters": {}
          }
        },
        {
          "type": "function",
          "function": {
            "name": "my_plugin_tool",
            "description": "插件注入的工具",
            "parameters": {
              "type": "object",
              "properties": {
                "query": {"type": "string"}
              }
            }
          }
        }
      ]
    }
  }
}
字段 说明
action 决策动作(见下表)
request 修改后的请求对象

3. hook.after_llm

在收到 LLM 响应后触发。可修改响应内容。

请求

{
  "jsonrpc": "2.0",
  "id": 3,
  "method": "hook.after_llm",
  "params": {
    "meta": {
      "AgentID": "agent-1",
      "TurnID": "turn-1",
      "SessionKey": "session-1"
    },
    "model": "claude-sonnet",
    "response": {
      "role": "assistant",
      "content": "Hi!",
      "tool_calls": [
        {
          "id": "tc-1",
          "type": "function",
          "function": {
            "name": "echo",
            "arguments": "{\"text\":\"hi\"}"
          }
        }
      ]
    },
    "channel": "cli",
    "chat_id": "chat-1"
  }
}

响应

{
  "jsonrpc": "2.0",
  "id": 3,
  "result": {
    "action": "continue"
  }
}

4. hook.before_tool

在执行工具前触发。可修改工具名称和参数,或拒绝执行,或直接返回结果。

请求

{
  "jsonrpc": "2.0",
  "id": 4,
  "method": "hook.before_tool",
  "params": {
    "meta": {
      "AgentID": "agent-1",
      "TurnID": "turn-1",
      "SessionKey": "session-1"
    },
    "tool": "echo_text",
    "arguments": {
      "text": "hello"
    },
    "channel": "cli",
    "chat_id": "chat-1"
  }
}
字段 说明
tool 工具名称
arguments 工具参数

响应(改写参数)

{
  "jsonrpc": "2.0",
  "id": 4,
  "result": {
    "action": "modify",
    "call": {
      "tool": "echo_text",
      "arguments": {
        "text": "modified hello"
      }
    }
  }
}

响应(拒绝执行)

{
  "jsonrpc": "2.0",
  "id": 4,
  "result": {
    "action": "deny_tool",
    "reason": "参数不合法"
  }
}

响应(直接返回结果 - respond)

{
  "jsonrpc": "2.0",
  "id": 4,
  "result": {
    "action": "respond",
    "call": {
      "tool": "my_plugin_tool",
      "arguments": {
        "query": "hello"
      }
    },
    "result": {
      "for_llm": "Plugin tool executed successfully",
      "for_user": "",
      "silent": false,
      "is_error": false
    }
  }
}

respond action 允许 hook 直接返回工具结果,跳过实际工具执行。适用于:

  1. 插件工具注入:外部 hook 可实现工具,无需在 ToolRegistry 注册
  2. 工具结果缓存:对重复调用返回缓存结果
  3. 工具模拟:测试时返回模拟结果
字段 说明
action 必须为 respond
call 修改后的调用信息(可选)
result 直接返回的工具结果

5. hook.after_tool

在工具执行完成后触发。可修改返回给 LLM 的结果。

请求

{
  "jsonrpc": "2.0",
  "id": 5,
  "method": "hook.after_tool",
  "params": {
    "meta": {
      "AgentID": "agent-1",
      "TurnID": "turn-1",
      "SessionKey": "session-1"
    },
    "tool": "echo_text",
    "arguments": {
      "text": "hello"
    },
    "result": {
      "for_llm": "echoed: hello",
      "for_user": "",
      "silent": false,
      "is_error": false,
      "async": false,
      "media": [],
      "artifact_tags": [],
      "response_handled": false
    },
    "duration": 15000000,
    "channel": "cli",
    "chat_id": "chat-1"
  }
}
字段 说明
result.for_llm 返回给 LLM 的内容
result.for_user 发送给用户的内容
result.silent 是否静默(不发送给用户)
result.is_error 是否为错误
result.async 是否异步执行
result.media 媒体引用列表
result.artifact_tags 本地产物路径标签
result.response_handled 是否已处理响应
duration 执行耗时(纳秒)

响应

{
  "jsonrpc": "2.0",
  "id": 5,
  "result": {
    "action": "continue"
  }
}

6. hook.approve_tool

审批型 hook,用于决定是否允许执行敏感工具。

请求

{
  "jsonrpc": "2.0",
  "id": 6,
  "method": "hook.approve_tool",
  "params": {
    "meta": {
      "AgentID": "agent-1",
      "TurnID": "turn-1",
      "SessionKey": "session-1"
    },
    "tool": "bash",
    "arguments": {
      "command": "rm -rf /"
    },
    "channel": "cli",
    "chat_id": "chat-1"
  }
}

响应(批准)

{
  "jsonrpc": "2.0",
  "id": 6,
  "result": {
    "approved": true
  }
}

响应(拒绝)

{
  "jsonrpc": "2.0",
  "id": 6,
  "result": {
    "approved": false,
    "reason": "危险命令,禁止执行"
  }
}

7. hook.eventnotification

观察型事件,仅广播,无需响应。id0 或不存在。

{
  "jsonrpc": "2.0",
  "method": "hook.event",
  "params": {
    "Kind": "tool_exec_start",
    "Meta": {
      "AgentID": "agent-1",
      "TurnID": "turn-1"
    },
    "Payload": {
      "Tool": "echo_text",
      "Arguments": {"text": "hello"}
    }
  }
}

常见 Kind 值:

  • turn_start / turn_end
  • llm_request / llm_response
  • tool_exec_start / tool_exec_end / tool_exec_skipped
  • steering_injected
  • interrupt_received
  • error

action 可选值

action 适用 hook 效果
continue 所有拦截型 放行,不做修改
modify before_llm, before_tool, after_llm, after_tool 改写请求/响应后放行
respond before_tool 直接返回工具结果,跳过实际执行
deny_tool before_tool 拒绝执行该工具
abort_turn 所有拦截型 中止当前 turn,返回错误
hard_abort 所有拦截型 强制终止整个 agent loop

完整流程示例

{"jsonrpc":"2.0","id":1,"method":"hook.hello","params":{"name":"my_hook","version":1,"modes":["tool","approve"]}}
{"jsonrpc":"2.0","id":1,"result":{"ok":true,"name":"my_hook"}}
{"jsonrpc":"2.0","id":2,"method":"hook.before_llm","params":{"model":"claude-sonnet","messages":[{"role":"user","content":"hello"}],"tools":[]}}
{"jsonrpc":"2.0","id":2,"result":{"action":"continue"}}
{"jsonrpc":"2.0","id":3,"method":"hook.before_tool","params":{"tool":"bash","arguments":{"command":"ls"}}}
{"jsonrpc":"2.0","id":3,"result":{"action":"continue"}}
{"jsonrpc":"2.0","id":4,"method":"hook.approve_tool","params":{"tool":"bash","arguments":{"command":"ls"}}}
{"jsonrpc":"2.0","id":4,"result":{"approved":true}}
{"jsonrpc":"2.0","id":5,"method":"hook.after_tool","params":{"tool":"bash","arguments":{"command":"ls"},"result":{"for_llm":"file1.txt\nfile2.txt"},"duration":5000000}}
{"jsonrpc":"2.0","id":5,"result":{"action":"continue"}}
{"jsonrpc":"2.0","id":6,"method":"hook.after_llm","params":{"model":"claude-sonnet","response":{"role":"assistant","content":"已列出文件"}}}
{"jsonrpc":"2.0","id":6,"result":{"action":"continue"}}

通过 before_llmbefore_tool 实现插件工具注入

插件工具注入的标准流程:

  1. before_llm 中注入工具定义,让 LLM 知道有这个工具可用
  2. before_tool 中使用 respond action 直接返回工具执行结果

before_llm 注入工具定义

def handle_before_llm(params: dict) -> dict:
    tools = params.get("tools", [])
    
    # 添加插件工具定义
    tools.append({
        "type": "function",
        "function": {
            "name": "my_plugin_tool",
            "description": "插件提供的工具",
            "parameters": {
                "type": "object",
                "properties": {
                    "input": {"type": "string", "description": "输入内容"}
                },
                "required": ["input"]
            }
        }
    })
    
    return {
        "action": "modify",
        "request": {
            "model": params["model"],
            "messages": params["messages"],
            "tools": tools,
            "options": params.get("options", {})
        }
    }

before_tool 返回执行结果

def handle_before_tool(params: dict) -> dict:
    tool = params.get("tool", "")
    
    if tool == "my_plugin_tool":
        # 在这里实现工具逻辑
        args = params.get("arguments", {})
        input_text = args.get("input", "")
        
        # 直接返回结果,无需在 ToolRegistry 注册
        return {
            "action": "respond",
            "result": {
                "for_llm": f"插件工具执行成功,输入: {input_text}",
                "silent": False,
                "is_error": False
            }
        }
    
    return {"action": "continue"}

通过这种方式,外部 hook 可以完全实现插件工具,无需在 PicoClaw 内部注册任何工具实现。