Tool Call (Capability Manifest)
Tool Call 是 Agent 让客户端代为执行某个动作的协议——读本地文件、发系统通知、 调用第三方 API、操作其他 app。和聊天里发消息不同,tool call 走的是 “显式权限 + 可审计”路径。
依据:Capability Manifest 规范 v1.0 §“Tool Call Flow”。
步骤 1:在 Capability Manifest 声明
{ "schema_version": "1.0", "agent_version": "1.0.0", "tools": [ { "name": "fetch_url", "description_i18n_key": "agent.tools.fetch_url.desc", "input_schema": { "type": "object", "properties": { "url": { "type": "string", "format": "uri" }, "method": { "enum": ["GET", "POST"], "default": "GET" } }, "required": ["url"], "additionalProperties": false }, "permission_scope": "network:http", "timeout_ms": 10000 } ], "permission_scopes": [ { "id": "network:http", "label_i18n_key": "agent.scopes.network_http.label", "sensitivity": "medium" } ]}步骤 2:Agent 发 tool call
import { ulid } from "ulid";
async function callFetchUrl(conversationId: string, url: string) { const callId = ulid();
// 注册等待项 const promise = new Promise<unknown>((resolve, reject) => { pendingCalls.set(callId, { resolve, reject }); });
await agent.sendArtifact(conversationId, { artifact: { artifact_id: callId, subtype: "tool_call", payload: { call_id: callId, tool_name: "fetch_url", arguments: { url, method: "GET" }, permission_scope: "network:http", timeout_ms: 10000, }, }, });
return promise;}
const pendingCalls = new Map<string, { resolve: (v: unknown) => void; reject: (e: Error) => void }>();步骤 3:客户端按 sensitivity 提示用户
| sensitivity | 客户端行为 |
|---|---|
low | 静默执行(无弹窗) |
medium | 首次弹窗 + 同设备同会话 24 小时内静默 |
high | 每次弹窗,30 秒未确认 → denied: user_timeout |
UI 弹窗显示:
- 工具名(用
description_i18n_key翻译后的描述) - 调用参数(结构化展示,可折叠)
- 权限 scope(用
permission_scopes[].label_i18n_key) - “允许” / “拒绝” / (高敏感度有)“始终拒绝” 按钮
步骤 4:Agent 接收 response
agent.addEventHandler(async (event) => { if (event.type !== "artifact_response") return; if (event.payload.ref_artifact_subtype !== "tool_response") return;
const { call_id, status, result, reason } = event.payload; const handler = pendingCalls.get(call_id); if (!handler) return; pendingCalls.delete(call_id);
if (status === "ok") { handler.resolve(result); } else { handler.reject(new Error(`tool ${status}: ${reason}`)); }});status 枚举:
| status | 含义 |
|---|---|
ok | 工具执行成功,result 字段是工具返回值 |
denied | 用户拒绝,reason 给具体原因(见下) |
error | 客户端执行失败(网络错 / 异常),reason 是错误描述 |
denied.reason 枚举(Capability Manifest 规范 §10):
| reason | 触发 |
|---|---|
user_rejected | 用户主动点了拒绝 |
user_timeout | high sensitivity 30 秒内未响应 |
tool_not_supported_in_group | V1 群聊不支持 tool call |
permission_revoked | 用户在权限管理界面撤销了对应 scope |
一个完整的最小例子
把上面三段串起来:
// 1. 业务调用const html = await callFetchUrl(conversationId, "https://example.com");console.log(html);执行流:
[T+0ms] Agent: sendArtifact tool_call[T+50ms] Client: 收到 → schema 校验通过 → sensitivity=medium → 弹窗[T+200ms] User: 点 "允许"[T+220ms] Client: fetch("https://example.com")[T+800ms] Client: 收到响应 → 构造 artifact_response { status: "ok", result: { body, headers, status_code } }[T+850ms] Agent: addEventHandler 捕获 → resolve promise[T+851ms] callFetchUrl 返回 html如果用户拒绝:
[T+200ms] User: 点 "拒绝"[T+201ms] Client: artifact_response { status: "denied", reason: "user_rejected" }[T+220ms] Agent: reject promise → 业务侧 try-catch 拿到 error工具超时
timeout_ms 字段约束客户端执行时长:
// Manifest 声明 timeout_ms: 10000// 客户端 fetch 用了 12 秒// 客户端立即返回 artifact_response { status: "error", reason: "tool_timeout" }Agent 端业务侧应该再加一层全局超时(防止客户端不响应):
async function callFetchUrlWithTimeout(conversationId: string, url: string, ms = 30000) { return Promise.race([ callFetchUrl(conversationId, url), new Promise((_, reject) => setTimeout(() => reject(new Error("tool call total timeout")), ms), ), ]);}工具批量调用
串行(推荐 high sensitivity):
const r1 = await callFetchUrl(conversationId, "https://a.com");const r2 = await callFetchUrl(conversationId, "https://b.com");并行(low sensitivity 静默执行场景,注意 UX):
const [r1, r2] = await Promise.all([ callFetchUrl(conversationId, "https://a.com"), callFetchUrl(conversationId, "https://b.com"),]);high sensitivity 工具并行会同时弹两个窗,UX 极差——只在 low sensitivity 用。
群聊不支持
V1 群聊(H2G)的 tool call 一律返回 denied: tool_not_supported_in_group。
原因:群里多人 broadcasting 工具 prompt 会破坏隐私语义(其他成员能看到 prompt 内容)。
如果你需要群里能”代某用户执行工具”,当前 workaround 是让 Agent 在群里发链接 / 提示, 让用户在 H2A 私聊里继续执行。
V2 路线:group-scoped tool call(管理员定义谁能 trigger,结果只可见 trigger 用户)。
LLM 集成(OpenAI 函数调用对接)
LLM 自带的 function calling(OpenAI / Anthropic / 等)映射到 Hashee tool call:
import OpenAI from "openai";const openai = new OpenAI();
agent.addMessageHandler(async (msg) => { const completion = await openai.chat.completions.create({ model: "gpt-4o-mini", messages: [{ role: "user", content: msg.payload.text }], tools: [{ type: "function", function: { name: "fetch_url", description: "Fetch a URL and return its body", parameters: { type: "object", properties: { url: { type: "string" }}, required: ["url"]}, }, }], });
const toolCall = completion.choices[0].message.tool_calls?.[0]; if (toolCall?.function.name === "fetch_url") { const args = JSON.parse(toolCall.function.arguments); try { // 触发 Hashee tool call → 用户授权 → 客户端执行 → 结果返回 const html = await callFetchUrl(msg.conversation_id, args.url); // 把结果喂回 LLM const final = await openai.chat.completions.create({ model: "gpt-4o-mini", messages: [ { role: "user", content: msg.payload.text }, completion.choices[0].message, { role: "tool", tool_call_id: toolCall.id, content: JSON.stringify({ html }) }, ], }); await agent.send(msg.conversation_id, { type: "text", text: final.choices[0].message.content ?? "", }); } catch (err) { await agent.send(msg.conversation_id, { type: "text", text: `工具调用失败:${err.message}`, }); } }});演示视频
视频文字版逐节描述
- 00:00 – 00:10 — Hashee app 用户问 Agent:“https://example.com 上写了什么?”
Agent 终端
inbound text=...→ 触发 LLM → LLM 决策调fetch_url。 - 00:10 – 00:20 — Hashee app 弹出 tool call 卡片:标题”网络请求”,参数
{ url: "https://example.com", method: "GET" },权限 scope “网络访问 (medium)”, 按钮”允许 / 拒绝”。用户点”允许”。 - 00:20 – 00:30 — 客户端 fetch;卡片变成”执行中…“loading。约 1 秒后 结果回到 Agent。
- 00:30 – 00:45 — Agent 把 html 喂回 LLM 二轮调用,得到结构化总结, 以 text 消息回复用户:“example.com 是 IANA 提供的示例域名,主要用于…”
- 00:45 – 00:55 — 演示拒绝路径:用户问”再请求一次 https://other.com”,
弹窗出现 → 用户点”拒绝”。Agent 收到
denied: user_rejected→ 回复 “好的,已取消请求。” - 00:55 – 01:05 — 演示 timeout:模拟 high sensitivity 工具,弹窗显示
30 秒倒计时;倒计时归零后客户端自动返回
denied: user_timeout。
下一步
- Capability Manifest 声明 — 完整 schema 与 sensitivity 规则
- Artifact 高级 — artifact_update 与 revision
- 群组消息 — 群聊 tool 限制与替代方案