Back to Picoclaw

Hook JSON-RPC 协议详解

docs/architecture/hooks/hook-json-protocol.zh.md

0.2.811.5 KB
Original Source

Hook JSON-RPC 协议详解

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


基础协议结构

请求(PicoClaw → Hook)

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

响应(Hook → PicoClaw)

成功:

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

错误:

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

1. hook.hello(握手)

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

请求

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

响应

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

2. hook.before_llm

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

请求

json
{
  "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可用工具定义列表
optionsLLM 参数(temperature、max_tokens 等)
channel请求来源通道
chat_id会话 ID

响应(注入工具示例)

json
{
  "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 响应后触发。可修改响应内容。

请求

json
{
  "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"
  }
}

响应

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

4. hook.before_tool

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

请求

json
{
  "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工具参数

响应(改写参数)

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

响应(拒绝执行)

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

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

json
{
  "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 的结果。

请求

json
{
  "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执行耗时(纳秒)

响应

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

6. hook.approve_tool

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

请求

json
{
  "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"
  }
}

响应(批准)

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

响应(拒绝)

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

7. hook.event(notification)

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

json
{
  "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所有拦截型放行,不做修改
modifybefore_llm, before_tool, after_llm, after_tool改写请求/响应后放行
respondbefore_tool直接返回工具结果,跳过实际执行
deny_toolbefore_tool拒绝执行该工具
abort_turn所有拦截型中止当前 turn,返回错误
hard_abort所有拦截型强制终止整个 agent loop

完整流程示例

json
{"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 注入工具定义

python
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 返回执行结果

python
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 内部注册任何工具实现。