7.3 ToolApprovalRequest/Response API

概述

审批通信协议由两个核心结构体定义:**请求**(`AgentResponseUpdate::ToolApprovalRequest`)和**响应**(`ToolApprovalResponse`)。它们分别由框架发出和由调用者构造。

ToolApprovalRequest — 审批请求

审批请求以 `AgentResponseUpdate::ToolApprovalRequest` 变体在流中发出。它不是一个独立的顶层结构体,而是 `AgentResponseUpdate` 枚举的一个变体:

// crates/core/src/message.rs

pub enum AgentResponseUpdate {
    // ... 其他变体 ...

    /// 工具调用需要人工审批才能执行
    ToolApprovalRequest {
        /// 唯一标识此次工具调用,与 ToolCall.id 对应
        call_id: String,
        /// 工具名称,如 "run_command", "write_file"
        name: String,
        /// 工具调用参数(已解析为 serde_json::Value)
        arguments: serde_json::Value,
        /// 工具描述文本,从 ITool::description() 获取
        description: String,
    },

    // ...
}

字段详解

| 字段 | 类型 | 说明 | |------|------|------| | `call_id` | `String` | 全局唯一的工具调用 ID。与 `ToolCallStartContent.call_id` 和 `ToolCall.id` 对应。用于匹配 `ToolApprovalResponse`。 | | `name` | `String` | 工具名称。在工具注册表中查找对应的 `ITool` 实现。 | | `arguments` | `serde_json::Value` | 已解析的参数。可能为 `String`(原始 JSON 字符串)或 `Object`(已解析的结构体)。推荐用 `arguments.as_str()` 获取字符串形式用于展示。 | | `description` | `String` | 工具的人类可读描述,从 `ITool::description()` 获取。用于 UI 展示,帮助用户做出审批决策。 |

审批请求的产生位置

审批请求在 `FunctionInvokingChatClient` 的 spawned task 中产生,发生在工具调用累积完毕后、实际执行之前:

// crates/framework/src/decorators/invoke.rs

// 1. 检查是否有任何工具需要审批
let any_requires_approval = tool_calls.iter().any(|tc| {
    tools_for_execution
        .iter()
        .any(|t| t.name() == tc.name && t.requires_approval())
});

// 2. 如果有,为每个工具调用发出 ToolApprovalRequest
if any_requires_approval {
    for tc in &tool_calls {
        let tool = tools_for_execution.iter().find(|t| t.name() == tc.name);
        let desc = tool.map(|t| t.description().to_string()).unwrap_or_default();
        let args: serde_json::Value =
            serde_json::from_str(&tc.arguments).unwrap_or(serde_json::Value::Null);

        tx.send(Ok(AgentResponseUpdate::ToolApprovalRequest {
            call_id: tc.id.clone(),
            name: tc.name.clone(),
            arguments: args,
            description: desc,
        })).await;
    }

    // 3. 跟上 Finish(AwaitingApproval) 结束流
    tx.send(Ok(AgentResponseUpdate::Finish {
        finish_reason: FinishReason::AwaitingApproval,
        usage: None,
    })).await;
}

在 AgentResponseConverter 中的处理

当 `ToolApprovalRequest` 到达 `AgentResponseConverter` 时,它被转换为可读的文本提示:

// crates/framework/src/converter.rs

AgentResponseUpdate::ToolApprovalRequest { name, arguments, .. } => {
    let args_display = arguments
        .as_str()
        .map(|s| s.to_string())
        .unwrap_or_else(|| serde_json::to_string(&arguments).unwrap_or_default());
    contents.push(Content::Text(TextContent {
        meta: self.build_meta(),
        delta: format!("\n[Approval required: {}({})]\n", name, args_display),
    }));
}

ToolApprovalResponse — 审批响应

审批响应由调用方构造,通过 `AgentRunOptions.tool_approval_responses` 传递给下一次 `agent.run()` 调用:

// crates/core/src/tool.rs

/// 调用者对 ToolApprovalRequest 的响应
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ToolApprovalResponse {
    /// 匹配对应 ToolApprovalRequest 中的 call_id
    pub call_id: String,
    /// true = 批准执行,false = 拒绝
    pub approved: bool,
    /// 拒绝原因(可选,会反馈给 LLM)
    pub reason: Option<String>,
}

字段详解

| 字段 | 类型 | 说明 | |------|------|------| | `call_id` | `String` | 必须匹配 `ToolApprovalRequest.call_id`。用于将响应关联到具体的工具调用。 | | `approved` | `bool` | `true`:批准执行 → 工具正常执行,结果注入 LLM 对话。`false`:拒绝 → 工具不执行,拒绝原因作为错误反馈给 LLM。 | | `reason` | `Option<String>` | 拒绝原因。仅在 `approved: false` 时有意义。原因会被格式化为 `"Rejected: {reason}"` 并作为 tool 角色的消息注入对话,让 LLM 了解用户为什么拒绝了该操作。 |

传递机制

审批响应通过 `AgentRunOptions` → `ChatClientRunOptions` → `FunctionInvokingChatClient` 的传播链路到达执行层:

AgentRunOptions.tool_approval_responses
    ↓ AgentRunOptions::to_chat_client_run_options()
ChatClientRunOptions.tool_approval_responses
    ↓ ChatClientAgent.run() → client_opts
FunctionInvokingChatClient.run(messages, options)
    ↓ 检查 options.tool_approval_responses
审批恢复逻辑
// crates/core/src/run_options.rs

impl AgentRunOptions {
    pub fn to_chat_client_run_options(&self) -> ChatClientRunOptions {
        ChatClientRunOptions {
            // ... 其他字段 ...
            tool_approval_responses: self.tool_approval_responses.clone(),
            cancelled: self.cancelled.clone(),
        }
    }
}

代码示例

示例 1:基本审批流程

use rust_agent_core::{
    AgentRunOptions, ToolApprovalResponse,
    ApprovalRequiredTool, FinishReason,
};
use rust_agent_client::{DeepSeekChatClient, ChatClientOptions};
use rust_agent_framework::{AgentBuilder, FunctionInvokingChatClient};
use std::sync::Arc;

#[tokio::main]
async fn main() {
    // 构建会话
    let session = /* 创建 ISession */;

    // 构建带审批的 Agent
    let agent = AgentBuilder::new("my-agent")
        .with_chat_client(/* chat_client */)
        .with_tool(ApprovalRequiredTool::new(Arc::new(RunCommand::default())))
        .with_session(session.clone())
        .build()
        .unwrap();

    // 第一轮:触发审批
    let mut stream = agent.run(
        vec![ChatMessage::user("帮我删除 /tmp/logs 目录")],
        Some(session.clone()),
        None,
    ).await.unwrap();

    let mut approval_requests = Vec::new();
    while let Some(chunk) = stream.next().await {
        match chunk.unwrap() {
            AgentResponseResult { finish_reason: Some(FinishReason::AwaitingApproval), .. } => {
                break; // 审批暂停
            }
            result => {
                // 收集 ToolApprovalRequest(通过 Content::Text 的文本提示)
                for content in &result.contents {
                    if let Content::Text(t) = content {
                        if t.delta.contains("[Approval required:") {
                            // 解析审批请求信息
                            println!("审批请求: {}", t.delta);
                        }
                    }
                }
            }
        }
    }

    // 构造审批响应
    let response = ToolApprovalResponse {
        call_id: "call_1".to_string(), // 从审批请求中获取
        approved: true,
        reason: None,
    };

    // 第二轮:恢复执行
    let options = AgentRunOptions::new()
        .with_tool_approval_responses(vec![response]);

    let mut stream = agent.run(
        vec![ChatMessage::user("继续")],
        Some(session.clone()),
        Some(options),
    ).await.unwrap();

    // 消费恢复后的流
    while let Some(chunk) = stream.next().await {
        // 处理最终结果
    }
}

示例 2:拒绝工具调用

// 拒绝并给出原因
let response = ToolApprovalResponse {
    call_id: "call_2".to_string(),
    approved: false,
    reason: Some("该命令会删除生产数据库,请使用更安全的命令".to_string()),
};

let options = AgentRunOptions::new()
    .with_tool_approval_responses(vec![response]);

拒绝后,`FunctionInvokingChatClient` 会生成如下消息注入对话:

{
    "role": "tool",
    "tool_call_id": "call_2",
    "content": "Rejected: 该命令会删除生产数据库,请使用更安全的命令"
}

LLM 收到这条消息后可以理解用户拒绝了该操作以及原因,从而给出替代方案。

示例 3:多工具审批

// 两个工具调用都需要审批
let responses = vec![
    ToolApprovalResponse {
        call_id: "call_echo".to_string(),
        approved: true,
        reason: None,
    },
    ToolApprovalResponse {
        call_id: "call_rm".to_string(),
        approved: false,
        reason: Some("删除文件前请先备份".to_string()),
    },
];

let options = AgentRunOptions::new()
    .with_tool_approval_responses(responses);

数据流总结

graph LR
    A[LLM 输出<br/>tool_calls] --> B{FICC 检查<br/>requires_approval?}
    B -->|false| C[直接执行工具]
    B -->|true| D[发出 ToolApprovalRequest<br/>+ AwaitingApproval]
    D --> E[调用方收集<br/>审批决定]
    E --> F[构造 ToolApprovalResponse]
    F --> G[AgentRunOptions<br/>.tool_approval_responses]
    G --> H[agent.run() 恢复]
    H --> I{FICC 处理}
    I -->|approved: true| J[执行工具<br/>追加 tool(result)]
    I -->|approved: false| K[追加 tool(rejected) 消息]
    J --> L[继续 LLM 循环]
    K --> L