跳转到内容

Tool Call (Capability Manifest)

Tool Call 是 Agent 让客户端代为执行某个动作的协议——读本地文件、发系统通知、 调用第三方 API、操作其他 app。和聊天里发消息不同,tool call 走的是 “显式权限 + 可审计”路径。

依据:Capability Manifest 规范 v1.0 §“Tool Call Flow”。

步骤 1:在 Capability Manifest 声明

参考 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_timeouthigh sensitivity 30 秒内未响应
tool_not_supported_in_groupV1 群聊不支持 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

下一步