内置工具错误改成每工具一个 typed enum,工具桥与 MCP 预检收口
- 每个内置工具在 agent/tool/<tool>/error.rs 定义自己的错误 enum:一个 case 一个变体,用户(以及转述给用户的模型)可见文案写在变体的 to_user_msg() 上,捕获处只调 to_user_msg() - 跨工具重复的 case(入参形状、项目权限门禁、分页、清单读取、资源登记完成投影、客户端 Direct 回合门禁、宿主执行门禁、未登记工具名)在 agent/tool/error.rs 定义一次,各工具用包装变体 + From 复用,不复制文案 - 工具失败诊断的 code 由「反推错误文案」改成稳定的工具名,开发者信息进 metadata:tool、脱敏 arguments、directTurn、dispatchDenied - 统一错误事件去掉 retryable 字段,AGENT_RUNTIME_ERROR_SCHEMA_VERSION 升到 agent-runtime-error.v2 - 独立客户端 MCP 的 validate_* 改成调用工具桥同一份入参规则,再用该工具 enum 的 to_user_msg() 渲染,删掉重复的字符串文案 - project/verification.rs 的项目权限策略判定改为返回 typed ProjectPermissionRejection(Denied / PolicyUnavailable) - 同步更新技术方案文档的字段表与共享记忆的决策记录
This commit is contained in:
@@ -47,6 +47,7 @@ mod runtime_state;
|
||||
mod runtime_tools;
|
||||
mod skill_pack;
|
||||
mod thread_manager;
|
||||
pub mod tool;
|
||||
|
||||
use claude_code_cli::*;
|
||||
pub(crate) use claude_code_cli::{
|
||||
|
||||
@@ -2012,7 +2012,6 @@ pub(crate) fn record_direct_codex_failure(
|
||||
"direct-codex",
|
||||
stage.id(),
|
||||
error_code,
|
||||
retryable,
|
||||
&summary,
|
||||
recovery_hint,
|
||||
&detail,
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -12,7 +12,7 @@ use std::path::Path;
|
||||
use std::sync::atomic::{AtomicU64, Ordering};
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
|
||||
pub(crate) const AGENT_RUNTIME_ERROR_SCHEMA_VERSION: &str = "agent-runtime-error.v1";
|
||||
pub(crate) const AGENT_RUNTIME_ERROR_SCHEMA_VERSION: &str = "agent-runtime-error.v2";
|
||||
pub(crate) const AGENT_RUNTIME_ERROR_MAX_DETAIL_CHARS: usize = 8 * 1024;
|
||||
|
||||
/// 应用日志里 detail / metadata 的字符预算。
|
||||
@@ -39,7 +39,6 @@ pub(crate) struct AgentRuntimeErrorEvent {
|
||||
pub source: String,
|
||||
pub stage: String,
|
||||
pub code: String,
|
||||
pub retryable: bool,
|
||||
pub occurred_at_unix_nanos: String,
|
||||
pub elapsed_ms: Option<u64>,
|
||||
pub public_text: String,
|
||||
@@ -55,7 +54,6 @@ pub(crate) fn persist_agent_runtime_error(
|
||||
source: &str,
|
||||
stage: &str,
|
||||
code: &str,
|
||||
retryable: bool,
|
||||
public_text: &str,
|
||||
recovery_hint: &str,
|
||||
detail: &str,
|
||||
@@ -78,7 +76,6 @@ pub(crate) fn persist_agent_runtime_error(
|
||||
"source": source,
|
||||
"stage": stage,
|
||||
"code": code,
|
||||
"retryable": retryable,
|
||||
"occurredAtUnixNanos": occurred_at_unix_nanos.to_string(),
|
||||
"elapsedMs": elapsed_ms,
|
||||
"publicText": public_text,
|
||||
@@ -96,7 +93,6 @@ pub(crate) fn persist_agent_runtime_error(
|
||||
source,
|
||||
stage,
|
||||
code,
|
||||
retryable,
|
||||
public_text,
|
||||
recovery_hint,
|
||||
&detail_ref,
|
||||
@@ -121,7 +117,6 @@ pub(crate) fn persist_agent_runtime_error(
|
||||
source: source.to_string(),
|
||||
stage: stage.to_string(),
|
||||
code: code.to_string(),
|
||||
retryable,
|
||||
occurred_at_unix_nanos: occurred_at_unix_nanos.to_string(),
|
||||
elapsed_ms,
|
||||
public_text: public_text.to_string(),
|
||||
@@ -144,7 +139,7 @@ pub(crate) fn persist_agent_runtime_error(
|
||||
///
|
||||
/// 拆成「身份行 + 详情行」是因为整行只要出现凭据标记就会被
|
||||
/// [`crate::sanitize_diagnostic_message`] 整体替换成脱敏占位。因此身份行**只放程序生成或
|
||||
/// 调用方常量字段**(eventId / source / stage / code / retryable / clientTurnId / elapsedMs /
|
||||
/// 调用方常量字段**(eventId / source / stage / code / clientTurnId / elapsedMs /
|
||||
/// detailRef),`summary`、`hint` 这些自由文本全部放详情行:自由文本里一个裸词
|
||||
/// (例如 `credential`)就能让整行被替换,放错了就会把事件定位信息一起吃掉。
|
||||
///
|
||||
@@ -158,7 +153,6 @@ pub(crate) fn agent_runtime_error_app_log_lines(
|
||||
source: &str,
|
||||
stage: &str,
|
||||
code: &str,
|
||||
retryable: bool,
|
||||
public_text: &str,
|
||||
recovery_hint: &str,
|
||||
detail_ref: &str,
|
||||
@@ -181,7 +175,7 @@ pub(crate) fn agent_runtime_error_app_log_lines(
|
||||
.map(|value| value.to_string())
|
||||
.unwrap_or_else(|| "none".to_string());
|
||||
let identity = format!(
|
||||
"agent.runtime.error eventId={event_id} source={source} stage={stage} code={code} retryable={retryable} clientTurnId={client_turn_id} elapsedMs={elapsed_ms} detailRef={detail_ref}"
|
||||
"agent.runtime.error eventId={event_id} source={source} stage={stage} code={code} clientTurnId={client_turn_id} elapsedMs={elapsed_ms} detailRef={detail_ref}"
|
||||
);
|
||||
let detail = redact_agent_runtime_error(root, detail, AGENT_RUNTIME_ERROR_APP_LOG_DETAIL_CHARS);
|
||||
let metadata = redact_agent_runtime_error(
|
||||
@@ -245,7 +239,6 @@ mod tests {
|
||||
"direct-codex",
|
||||
"code-generation",
|
||||
"turn-idle-timeout",
|
||||
true,
|
||||
"本轮没有收到完成事件",
|
||||
"查看诊断后重试",
|
||||
"C:\\Users\\private\\project https://provider.example/a?token=secret",
|
||||
@@ -281,7 +274,6 @@ mod tests {
|
||||
"direct-codex",
|
||||
"code-generation",
|
||||
"turn-idle-timeout",
|
||||
true,
|
||||
"本轮没有收到完成事件\n附带换行",
|
||||
"查看诊断后重试",
|
||||
".agent/runtime/errors/error-1-1.json",
|
||||
@@ -329,7 +321,6 @@ mod tests {
|
||||
"agc-tools",
|
||||
"tool-execution",
|
||||
"tool-error",
|
||||
true,
|
||||
"credential rotation failed",
|
||||
"查看项目错误诊断后处理",
|
||||
".agent/runtime/errors/error-3-1.json",
|
||||
|
||||
@@ -110,7 +110,6 @@ pub(crate) fn append_game_creator_agent_runtime_terminal_public_message_at(
|
||||
"agent-runtime",
|
||||
&state.phase,
|
||||
"agent-runtime-terminal",
|
||||
false,
|
||||
&content,
|
||||
"查看项目错误诊断后处理",
|
||||
error,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use super::*;
|
||||
use crate::agent::tool::error::{ToolArgumentsRejection, ToolFailure};
|
||||
|
||||
pub(in crate::agent) fn observe_agent_runtime_memory(
|
||||
root: &Path,
|
||||
@@ -281,23 +282,14 @@ pub(in crate::agent) async fn observe_agent_runtime_account_asset_library(
|
||||
root: &Path,
|
||||
input: &serde_json::Value,
|
||||
) -> AgentRuntimeToolObservation {
|
||||
if let Some(object) = input.as_object() {
|
||||
if let Some(field) = object
|
||||
.keys()
|
||||
.find(|field| !["folderId", "query", "offset", "limit"].contains(&field.as_str()))
|
||||
{
|
||||
return AgentRuntimeToolObservation {
|
||||
tool: "asset.library.list".to_string(),
|
||||
status: "rejected".to_string(),
|
||||
summary: format!("工具参数包含未审核字段:{field}"),
|
||||
detail: None,
|
||||
};
|
||||
}
|
||||
} else {
|
||||
if let Err(rejection) = ToolArgumentsRejection::check(
|
||||
input,
|
||||
&["folderId", "query", "offset", "limit"],
|
||||
) {
|
||||
return AgentRuntimeToolObservation {
|
||||
tool: "asset.library.list".to_string(),
|
||||
status: "rejected".to_string(),
|
||||
summary: "工具参数必须是对象".to_string(),
|
||||
summary: rejection.to_user_msg(),
|
||||
detail: None,
|
||||
};
|
||||
}
|
||||
@@ -508,22 +500,11 @@ pub(in crate::agent) async fn observe_agent_runtime_asset_import(
|
||||
root: &Path,
|
||||
input: &serde_json::Value,
|
||||
) -> AgentRuntimeToolObservation {
|
||||
let Some(object) = input.as_object() else {
|
||||
if let Err(rejection) = ToolArgumentsRejection::check(input, &["assetIds", "localPaths"]) {
|
||||
return AgentRuntimeToolObservation {
|
||||
tool: "canvas.asset_import".to_string(),
|
||||
status: "rejected".to_string(),
|
||||
summary: "工具参数必须是对象".to_string(),
|
||||
detail: None,
|
||||
};
|
||||
};
|
||||
if let Some(field) = object
|
||||
.keys()
|
||||
.find(|field| !["assetIds", "localPaths"].contains(&field.as_str()))
|
||||
{
|
||||
return AgentRuntimeToolObservation {
|
||||
tool: "canvas.asset_import".to_string(),
|
||||
status: "rejected".to_string(),
|
||||
summary: format!("工具参数包含未审核字段:{field}"),
|
||||
summary: rejection.to_user_msg(),
|
||||
detail: None,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
//! `agc_apply_patch` 的错误定义:每个 case 一个变体,文案写在变体上。
|
||||
//!
|
||||
//! 这里只放错误本身;参数解析与工具执行留在 `direct_tool_bridge` 与 `direct_patch`。
|
||||
|
||||
use crate::agent::tool::error::ToolFailure;
|
||||
|
||||
/// `agc_apply_patch` 的每一种失败各占一个变体;文案写在这里,捕获处只调 `to_user_msg`。
|
||||
///
|
||||
/// 补丁被拒绝(`status != completed`)不是错误:那是补丁工具自己的结果载荷,仍然原样回给
|
||||
/// 模型,好让它看到哪一段没打上。
|
||||
#[derive(Debug)]
|
||||
pub(crate) enum ApplyPatchError {
|
||||
/// 补丁没被受理:入参不合法、作用域越界或执行许可已关闭。
|
||||
PatchRejected { cause: String },
|
||||
}
|
||||
|
||||
impl ToolFailure for ApplyPatchError {
|
||||
fn to_user_msg(&self) -> String {
|
||||
match self {
|
||||
Self::PatchRejected { cause } => cause.to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
fn redact_limit(&self) -> usize {
|
||||
1800
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
pub mod error;
|
||||
@@ -0,0 +1,99 @@
|
||||
//! 内置工具入参的通用解析:字段越界时用该工具自己的错误变体返回。
|
||||
//!
|
||||
//! 每个工具为自己的文本字段定义一个 `enum` 并实现 [`ToolTextField`],把「空值/超长/
|
||||
//! 控制字符」三个变体告诉解析器;解析逻辑只有这一份,各工具的变体名与文案仍是自己的。
|
||||
|
||||
use serde_json::Value;
|
||||
|
||||
use super::error::ToolPageRejection;
|
||||
|
||||
/// 一个会被边界校验的文本字段。
|
||||
pub(crate) trait ToolTextField<E>: Copy {
|
||||
fn key(self) -> &'static str;
|
||||
fn max_chars(self) -> usize;
|
||||
fn empty(self) -> E;
|
||||
fn too_long(self, got_chars: usize, max_chars: usize) -> E;
|
||||
fn has_control_characters(self) -> E;
|
||||
}
|
||||
|
||||
/// 必填文本字段:缺省、`null`、空串都算空值。
|
||||
pub(crate) fn required_text<F: ToolTextField<E>, E>(
|
||||
arguments: &Value,
|
||||
field: F,
|
||||
) -> Result<String, E> {
|
||||
optional_text(arguments, field)?.ok_or_else(|| field.empty())
|
||||
}
|
||||
|
||||
/// 可选文本字段:缺省或 `null` 视为未提供;给了就必须在该字段自己的边界内。
|
||||
pub(crate) fn optional_text<F: ToolTextField<E>, E>(
|
||||
arguments: &Value,
|
||||
field: F,
|
||||
) -> Result<Option<String>, E> {
|
||||
let Some(value) = arguments.get(field.key()).filter(|value| !value.is_null()) else {
|
||||
return Ok(None);
|
||||
};
|
||||
let value = value
|
||||
.as_str()
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())
|
||||
.ok_or_else(|| field.empty())?;
|
||||
if value.chars().count() > field.max_chars() {
|
||||
return Err(field.too_long(value.chars().count(), field.max_chars()));
|
||||
}
|
||||
if value.chars().any(char::is_control) {
|
||||
return Err(field.has_control_characters());
|
||||
}
|
||||
Ok(Some(value.to_string()))
|
||||
}
|
||||
|
||||
/// 一页列表结果:`offset` 是起始下标,`limit` 是这一次最多返回多少项。
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub(crate) struct ToolPage {
|
||||
pub offset: usize,
|
||||
pub limit: usize,
|
||||
}
|
||||
|
||||
/// 解析 `offset`/`limit`。分页规则在三个列表工具之间一致,只有默认值与上限不同。
|
||||
pub(crate) fn page(
|
||||
arguments: &Value,
|
||||
default_limit: usize,
|
||||
max_limit: usize,
|
||||
max_offset: Option<usize>,
|
||||
) -> Result<ToolPage, ToolPageRejection> {
|
||||
let offset = arguments
|
||||
.get("offset")
|
||||
.filter(|value| !value.is_null())
|
||||
.map(|value| {
|
||||
value
|
||||
.as_u64()
|
||||
.and_then(|value| usize::try_from(value).ok())
|
||||
.ok_or_else(|| ToolPageRejection::OffsetNotInteger {
|
||||
got: value.to_string(),
|
||||
})
|
||||
})
|
||||
.transpose()?
|
||||
.unwrap_or(0);
|
||||
if let Some(max) = max_offset.filter(|max| offset > *max) {
|
||||
return Err(ToolPageRejection::OffsetTooLarge { got: offset, max });
|
||||
}
|
||||
let limit = arguments
|
||||
.get("limit")
|
||||
.filter(|value| !value.is_null())
|
||||
.map(|value| {
|
||||
value
|
||||
.as_u64()
|
||||
.and_then(|value| usize::try_from(value).ok())
|
||||
.ok_or_else(|| ToolPageRejection::LimitNotInteger {
|
||||
got: value.to_string(),
|
||||
})
|
||||
})
|
||||
.transpose()?
|
||||
.unwrap_or(default_limit);
|
||||
if limit == 0 || limit > max_limit {
|
||||
return Err(ToolPageRejection::LimitOutOfRange {
|
||||
got: limit,
|
||||
max: max_limit,
|
||||
});
|
||||
}
|
||||
Ok(ToolPage { offset, limit })
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
//! `agc_browser_playtest` 的错误定义:每个 case 一个变体,文案写在变体上。
|
||||
//!
|
||||
//! 这里只放错误本身;参数解析与工具执行留在 `direct_tool_bridge` 与 `direct_validation`。
|
||||
|
||||
use crate::agent::tool::error::ToolFailure;
|
||||
|
||||
/// `agc_browser_playtest` 的每一种失败各占一个变体;文案写在这里,捕获处只调 `to_user_msg`。
|
||||
#[derive(Debug)]
|
||||
pub(crate) enum BrowserPlaytestError {
|
||||
/// 浏览器试玩自身失败;原因来自 `direct_validation` 的稳定前缀码。
|
||||
PlaytestFailed { cause: String },
|
||||
}
|
||||
|
||||
impl ToolFailure for BrowserPlaytestError {
|
||||
fn to_user_msg(&self) -> String {
|
||||
match self {
|
||||
Self::PlaytestFailed { cause } => cause.to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
fn redact_limit(&self) -> usize {
|
||||
1800
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
pub mod error;
|
||||
@@ -0,0 +1,57 @@
|
||||
//! `agc_cocos_execute` 及 Cocos 操作类工具的错误定义:每个 case 一个变体,文案写在变体上。
|
||||
//!
|
||||
//! 这里只放错误本身;参数解析与工具执行留在 `direct_tool_bridge`。
|
||||
|
||||
use crate::agent::tool::error::{
|
||||
ProjectPermissionRejection, ToolArgumentsRejection, ToolFailure,
|
||||
};
|
||||
|
||||
/// Cocos 执行工具的每一种失败各占一个变体;文案写在这里,捕获处只调 `to_user_msg`。
|
||||
///
|
||||
/// 已有的"待核对""执行中"两种状态不是错误:它们各自带 `retryAllowed` 载荷,仍原样回给模型。
|
||||
#[derive(Debug)]
|
||||
pub(crate) enum CocosExecuteError {
|
||||
PluginDisabled,
|
||||
PluginUnavailable,
|
||||
PermissionDenied(ProjectPermissionRejection),
|
||||
Arguments(ToolArgumentsRejection),
|
||||
UnknownOperation { operation: String },
|
||||
SchemaInvalid { cause: String },
|
||||
OperationArgumentsInvalid { cause: String },
|
||||
CodeNotText,
|
||||
CodeInvalid { cause: String },
|
||||
}
|
||||
|
||||
impl From<ToolArgumentsRejection> for CocosExecuteError {
|
||||
fn from(rejection: ToolArgumentsRejection) -> Self {
|
||||
Self::Arguments(rejection)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ProjectPermissionRejection> for CocosExecuteError {
|
||||
fn from(rejection: ProjectPermissionRejection) -> Self {
|
||||
Self::PermissionDenied(rejection)
|
||||
}
|
||||
}
|
||||
|
||||
impl ToolFailure for CocosExecuteError {
|
||||
fn to_user_msg(&self) -> String {
|
||||
match self {
|
||||
Self::PluginDisabled => "Cocos 编辑器插件已禁用。".to_string(),
|
||||
Self::PluginUnavailable => {
|
||||
"当前 Cocos 插件不可用,agc_cocos_execute 不可用。".to_string()
|
||||
}
|
||||
Self::PermissionDenied(rejection) => rejection.to_user_msg(),
|
||||
Self::Arguments(rejection) => rejection.to_user_msg(),
|
||||
Self::UnknownOperation { operation } => {
|
||||
format!("未知 Cocos 操作「{operation}」。")
|
||||
}
|
||||
Self::SchemaInvalid { cause } => format!("Cocos schema 错误:{cause}"),
|
||||
Self::OperationArgumentsInvalid { cause } => {
|
||||
format!("Cocos 参数无效:{cause}")
|
||||
}
|
||||
Self::CodeNotText => "code 必须是 JavaScript 函数体。".to_string(),
|
||||
Self::CodeInvalid { cause } => cause.to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
pub mod error;
|
||||
+196
@@ -0,0 +1,196 @@
|
||||
//! `agc_create_or_derive_resource` 的错误定义:每个 case 一个变体,文案写在变体上。
|
||||
//!
|
||||
//! 这里只放错误本身;参数解析与工具执行留在 `direct_tool_bridge`。
|
||||
|
||||
use crate::agent::direct_tool_bridge::DirectResourceGenerationKind;
|
||||
use crate::agent::tool::error::{
|
||||
ProjectManifestRejection, ProjectPermissionRejection, ResourceCompletionRejection,
|
||||
ToolArgumentsRejection, ToolFailure,
|
||||
};
|
||||
|
||||
/// `sourceLocalAssetId` 不在当前项目 manifest 时的完整指引:模型要知道下一步做什么,
|
||||
/// 只报「不属于已登记资源」会让它原地重试。
|
||||
const UNREGISTERED_SOURCE_HINT: &str = "先调用 agc_list_registered_assets 选择已有 localAssetId;若目标图片只在项目里,先用 agc_list_project_files 确认它 assetImportable=true,再用 agc_import_account_assets.localPaths 登记后重试。";
|
||||
|
||||
/// `agc_create_or_derive_resource` 的每一种失败各占一个变体;文案写在这里,捕获处只调
|
||||
/// `to_user_msg`。
|
||||
#[derive(Debug)]
|
||||
pub(crate) enum CreateOrDeriveResourceError {
|
||||
Arguments(ToolArgumentsRejection),
|
||||
PermissionDenied(ProjectPermissionRejection),
|
||||
Manifest(ProjectManifestRejection),
|
||||
Completion(ResourceCompletionRejection),
|
||||
KindEmpty,
|
||||
KindTooLong { got_chars: usize, max_chars: usize },
|
||||
KindHasControlCharacters,
|
||||
KindUnsupported { got: String },
|
||||
ModeEmpty,
|
||||
ModeTooLong { got_chars: usize, max_chars: usize },
|
||||
ModeHasControlCharacters,
|
||||
ModeUnsupported { got: String },
|
||||
SourceLocalAssetIdEmpty,
|
||||
SourceLocalAssetIdTooLong { got_chars: usize, max_chars: usize },
|
||||
SourceLocalAssetIdHasControlCharacters,
|
||||
PromptEmpty,
|
||||
PromptTooLong { got_chars: usize, max_chars: usize },
|
||||
PromptHasControlCharacters,
|
||||
PromptTooLongForKind {
|
||||
kind: DirectResourceGenerationKind,
|
||||
got_chars: usize,
|
||||
max_chars: usize,
|
||||
},
|
||||
AssetNameEmpty,
|
||||
AssetNameTooLong { got_chars: usize, max_chars: usize },
|
||||
AssetNameHasControlCharacters,
|
||||
ImageCreationRequiresRegisteredSource,
|
||||
CharacterAnimationRequiresRegisteredSource,
|
||||
CreateModeWithSource,
|
||||
DeriveModeWithoutSource,
|
||||
SourceAssetNotRegistered,
|
||||
ProjectPathUnprojectable,
|
||||
GenerationQueueUnavailable { cause: String },
|
||||
PaidDispatchRefused { cause: String },
|
||||
PendingEditsUnreadable { cause: String },
|
||||
DuplicatePendingOperations,
|
||||
TurnIdUnavailable { cause: String },
|
||||
ProjectRevisionUnavailable { cause: String },
|
||||
ResourceGenerationFailed { cause: String },
|
||||
}
|
||||
|
||||
impl From<ToolArgumentsRejection> for CreateOrDeriveResourceError {
|
||||
fn from(rejection: ToolArgumentsRejection) -> Self {
|
||||
Self::Arguments(rejection)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ProjectPermissionRejection> for CreateOrDeriveResourceError {
|
||||
fn from(rejection: ProjectPermissionRejection) -> Self {
|
||||
Self::PermissionDenied(rejection)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ProjectManifestRejection> for CreateOrDeriveResourceError {
|
||||
fn from(rejection: ProjectManifestRejection) -> Self {
|
||||
Self::Manifest(rejection)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ResourceCompletionRejection> for CreateOrDeriveResourceError {
|
||||
fn from(rejection: ResourceCompletionRejection) -> Self {
|
||||
Self::Completion(rejection)
|
||||
}
|
||||
}
|
||||
|
||||
impl ToolFailure for CreateOrDeriveResourceError {
|
||||
fn to_user_msg(&self) -> String {
|
||||
match self {
|
||||
Self::Arguments(rejection) => rejection.to_user_msg(),
|
||||
Self::PermissionDenied(rejection) => rejection.to_user_msg(),
|
||||
Self::Manifest(rejection) => rejection.to_user_msg(),
|
||||
Self::Completion(rejection) => rejection.to_user_msg(),
|
||||
Self::KindEmpty => "生成或派生资源失败:kind 不能为空。".to_string(),
|
||||
Self::KindTooLong {
|
||||
got_chars,
|
||||
max_chars,
|
||||
} => format!("生成或派生资源失败:kind 有 {got_chars} 个字符,超过上限 {max_chars}。"),
|
||||
Self::KindHasControlCharacters => {
|
||||
"生成或派生资源失败:kind 含有控制字符。".to_string()
|
||||
}
|
||||
Self::KindUnsupported { got } => format!(
|
||||
"生成或派生资源失败:kind「{got}」不是受支持的媒体资源类型,只允许 image、video、character-animation、sound-effect、background-music。"
|
||||
),
|
||||
Self::ModeEmpty => "生成或派生资源失败:mode 不能为空。".to_string(),
|
||||
Self::ModeTooLong {
|
||||
got_chars,
|
||||
max_chars,
|
||||
} => format!("生成或派生资源失败:mode 有 {got_chars} 个字符,超过上限 {max_chars}。"),
|
||||
Self::ModeHasControlCharacters => {
|
||||
"生成或派生资源失败:mode 含有控制字符。".to_string()
|
||||
}
|
||||
Self::ModeUnsupported { got } => format!(
|
||||
"生成或派生资源失败:mode「{got}」不是受支持的模式,只能是 create 或 derive。"
|
||||
),
|
||||
Self::SourceLocalAssetIdEmpty => {
|
||||
"生成或派生资源失败:sourceLocalAssetId 传了空值。".to_string()
|
||||
}
|
||||
Self::SourceLocalAssetIdTooLong {
|
||||
got_chars,
|
||||
max_chars,
|
||||
} => format!(
|
||||
"生成或派生资源失败:sourceLocalAssetId 有 {got_chars} 个字符,超过上限 {max_chars}。"
|
||||
),
|
||||
Self::SourceLocalAssetIdHasControlCharacters => {
|
||||
"生成或派生资源失败:sourceLocalAssetId 含有控制字符。".to_string()
|
||||
}
|
||||
Self::PromptEmpty => "生成或派生资源失败:prompt 不能为空。".to_string(),
|
||||
Self::PromptTooLong {
|
||||
got_chars,
|
||||
max_chars,
|
||||
} => format!(
|
||||
"生成或派生资源失败:prompt 有 {got_chars} 个字符,超过请求上限 {max_chars}。"
|
||||
),
|
||||
Self::PromptHasControlCharacters => {
|
||||
"生成或派生资源失败:prompt 含有控制字符。".to_string()
|
||||
}
|
||||
Self::PromptTooLongForKind {
|
||||
kind,
|
||||
got_chars,
|
||||
max_chars,
|
||||
} => format!(
|
||||
"生成或派生资源失败:kind={}({})的 prompt 有 {got_chars} 个字符,超过该 kind 的上限 {max_chars}。",
|
||||
kind.as_str(),
|
||||
kind.label()
|
||||
),
|
||||
Self::AssetNameEmpty => "生成或派生资源失败:assetName 不能为空。".to_string(),
|
||||
Self::AssetNameTooLong {
|
||||
got_chars,
|
||||
max_chars,
|
||||
} => format!(
|
||||
"生成或派生资源失败:assetName 有 {got_chars} 个字符,超过上限 {max_chars}。"
|
||||
),
|
||||
Self::AssetNameHasControlCharacters => {
|
||||
"生成或派生资源失败:assetName 含有控制字符。".to_string()
|
||||
}
|
||||
Self::ImageCreationRequiresRegisteredSource => {
|
||||
"生成或派生资源失败:图片编辑必须基于已登记图片资源派生。".to_string()
|
||||
}
|
||||
Self::CharacterAnimationRequiresRegisteredSource => {
|
||||
"生成或派生资源失败:角色动画必须基于已登记图片资源派生。".to_string()
|
||||
}
|
||||
Self::CreateModeWithSource => {
|
||||
"生成或派生资源失败:create 模式不能携带源资源。".to_string()
|
||||
}
|
||||
Self::DeriveModeWithoutSource => {
|
||||
"生成或派生资源失败:derive 模式必须携带 sourceLocalAssetId。".to_string()
|
||||
}
|
||||
Self::SourceAssetNotRegistered => {
|
||||
format!("生成或派生资源失败:sourceLocalAssetId 不是当前项目已登记资源。{UNREGISTERED_SOURCE_HINT}")
|
||||
}
|
||||
Self::ProjectPathUnprojectable => {
|
||||
"生成或派生资源失败:当前项目路径不能安全投影到资源生成接口。".to_string()
|
||||
}
|
||||
Self::GenerationQueueUnavailable { cause } => {
|
||||
format!("生成或派生资源失败:资源请求并发状态不可用:{cause}")
|
||||
}
|
||||
Self::PaidDispatchRefused { cause } => {
|
||||
format!("生成或派生资源失败:本轮派发许可已关闭,未发起资源生成:{cause}")
|
||||
}
|
||||
Self::PendingEditsUnreadable { cause } => {
|
||||
format!("生成或派生资源失败:读取待对账的资源编辑失败:{cause}")
|
||||
}
|
||||
Self::DuplicatePendingOperations => {
|
||||
"生成或派生资源失败:存在多个相同资源生成 operation,必须先在客户端完成对账。"
|
||||
.to_string()
|
||||
}
|
||||
Self::TurnIdUnavailable { cause } => {
|
||||
format!("生成或派生资源失败:当前没有可绑定的资源生成回合:{cause}")
|
||||
}
|
||||
Self::ProjectRevisionUnavailable { cause } => {
|
||||
format!("生成或派生资源失败:读取项目 revision 失败:{cause}")
|
||||
}
|
||||
Self::ResourceGenerationFailed { cause } => {
|
||||
format!("生成或派生资源失败:{cause}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
pub mod error;
|
||||
@@ -0,0 +1,36 @@
|
||||
//! `agc_delivery_status` 的错误定义:每个 case 一个变体,文案写在变体上。
|
||||
//!
|
||||
//! 这里只放错误本身;参数解析与工具执行留在 `direct_tool_bridge` 与 `direct_delivery`。
|
||||
|
||||
use crate::agent::tool::error::{DirectExecutionGateRejection, ToolFailure};
|
||||
|
||||
/// `agc_delivery_status` 的每一种失败各占一个变体;文案写在这里,捕获处只调 `to_user_msg`。
|
||||
#[derive(Debug)]
|
||||
pub(crate) enum DeliveryStatusError {
|
||||
/// 只读状态工具不接受任何参数。
|
||||
ArgumentsNotAccepted,
|
||||
/// 宿主交付会话取不到。
|
||||
Gate(DirectExecutionGateRejection),
|
||||
/// 权威交付状态读不出来。
|
||||
StatusUnavailable { cause: String },
|
||||
}
|
||||
|
||||
impl From<DirectExecutionGateRejection> for DeliveryStatusError {
|
||||
fn from(rejection: DirectExecutionGateRejection) -> Self {
|
||||
Self::Gate(rejection)
|
||||
}
|
||||
}
|
||||
|
||||
impl ToolFailure for DeliveryStatusError {
|
||||
fn to_user_msg(&self) -> String {
|
||||
match self {
|
||||
Self::ArgumentsNotAccepted => "交付状态工具不接受参数。".to_string(),
|
||||
Self::Gate(rejection) => rejection.to_user_msg(),
|
||||
Self::StatusUnavailable { cause } => cause.to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
fn redact_limit(&self) -> usize {
|
||||
800
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
pub mod error;
|
||||
@@ -0,0 +1,72 @@
|
||||
//! `agc_edit_image` 的错误定义:每个 case 一个变体,文案写在变体上。
|
||||
//!
|
||||
//! 这里只放错误本身;参数解析与工具执行留在 `direct_tool_bridge`。
|
||||
|
||||
use crate::agent::tool::create_or_derive_resource::error::CreateOrDeriveResourceError;
|
||||
use crate::agent::tool::error::{ToolArgumentsRejection, ToolFailure};
|
||||
|
||||
/// `agc_edit_image` 的每一种失败各占一个变体;文案写在这里,捕获处只调 `to_user_msg`。
|
||||
#[derive(Debug)]
|
||||
pub(crate) enum EditImageError {
|
||||
Arguments(ToolArgumentsRejection),
|
||||
SourceLocalAssetIdEmpty,
|
||||
SourceLocalAssetIdTooLong {
|
||||
got_chars: usize,
|
||||
max_chars: usize,
|
||||
},
|
||||
SourceLocalAssetIdHasControlCharacters,
|
||||
PromptEmpty,
|
||||
PromptTooLong { got_chars: usize, max_chars: usize },
|
||||
PromptHasControlCharacters,
|
||||
AssetNameEmpty,
|
||||
AssetNameTooLong { got_chars: usize, max_chars: usize },
|
||||
AssetNameHasControlCharacters,
|
||||
ResourceEdit(CreateOrDeriveResourceError),
|
||||
}
|
||||
|
||||
impl From<ToolArgumentsRejection> for EditImageError {
|
||||
fn from(rejection: ToolArgumentsRejection) -> Self {
|
||||
Self::Arguments(rejection)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<CreateOrDeriveResourceError> for EditImageError {
|
||||
fn from(rejection: CreateOrDeriveResourceError) -> Self {
|
||||
Self::ResourceEdit(rejection)
|
||||
}
|
||||
}
|
||||
|
||||
impl ToolFailure for EditImageError {
|
||||
fn to_user_msg(&self) -> String {
|
||||
match self {
|
||||
Self::Arguments(rejection) => rejection.to_user_msg(),
|
||||
Self::SourceLocalAssetIdEmpty => "编辑图片失败:sourceLocalAssetId 不能为空。".to_string(),
|
||||
Self::SourceLocalAssetIdTooLong {
|
||||
got_chars,
|
||||
max_chars,
|
||||
} => format!(
|
||||
"编辑图片失败:sourceLocalAssetId 有 {got_chars} 个字符,超过上限 {max_chars}。"
|
||||
),
|
||||
Self::SourceLocalAssetIdHasControlCharacters => {
|
||||
"编辑图片失败:sourceLocalAssetId 含有控制字符。".to_string()
|
||||
}
|
||||
Self::PromptEmpty => "编辑图片失败:prompt 不能为空。".to_string(),
|
||||
Self::PromptTooLong {
|
||||
got_chars,
|
||||
max_chars,
|
||||
} => format!("编辑图片失败:prompt 有 {got_chars} 个字符,超过上限 {max_chars}。"),
|
||||
Self::PromptHasControlCharacters => {
|
||||
"编辑图片失败:prompt 含有控制字符。".to_string()
|
||||
}
|
||||
Self::AssetNameEmpty => "编辑图片失败:assetName 不能为空。".to_string(),
|
||||
Self::AssetNameTooLong {
|
||||
got_chars,
|
||||
max_chars,
|
||||
} => format!("编辑图片失败:assetName 有 {got_chars} 个字符,超过上限 {max_chars}。"),
|
||||
Self::AssetNameHasControlCharacters => {
|
||||
"编辑图片失败:assetName 含有控制字符。".to_string()
|
||||
}
|
||||
Self::ResourceEdit(rejection) => rejection.to_user_msg(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
pub mod error;
|
||||
@@ -0,0 +1,80 @@
|
||||
//! `agc_unity_execute` / `agc_godot_execute` 的错误定义:每个 case 一个变体,文案写在变体上。
|
||||
//!
|
||||
//! 这里只放错误本身;参数解析与工具执行留在 `direct_tool_bridge`。
|
||||
|
||||
use crate::agent::tool::error::{
|
||||
ProjectPermissionRejection, ToolArgumentsRejection, ToolFailure,
|
||||
};
|
||||
|
||||
/// 两个编辑器执行工具之间的差异:插件身份、给用户看的编辑器名、代码语言名。
|
||||
///
|
||||
/// 差异做成枚举,错误变体只带 `EditorKind`,不在文案里手写编辑器名。
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub(crate) enum EditorKind {
|
||||
Unity,
|
||||
Godot,
|
||||
}
|
||||
|
||||
impl EditorKind {
|
||||
pub(crate) fn tool_id(self) -> &'static str {
|
||||
match self {
|
||||
Self::Unity => "unity.editor.execute",
|
||||
Self::Godot => "godot.editor.execute",
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn label(self) -> &'static str {
|
||||
match self {
|
||||
Self::Unity => "Unity",
|
||||
Self::Godot => "Godot",
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn language(self) -> &'static str {
|
||||
match self {
|
||||
Self::Unity => "C# 代码",
|
||||
Self::Godot => "GDScript 函数体",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 编辑器执行工具的每一种失败各占一个变体;文案写在这里,捕获处只调 `to_user_msg`。
|
||||
#[derive(Debug)]
|
||||
pub(crate) enum EditorExecuteError {
|
||||
PluginUnavailable { editor: EditorKind },
|
||||
Arguments(ToolArgumentsRejection),
|
||||
PermissionDenied(ProjectPermissionRejection),
|
||||
CodeNotText { editor: EditorKind },
|
||||
CodeInvalid,
|
||||
/// 编辑器适配器拒绝了这次执行,或回执不可用。
|
||||
EditorRejected { cause: String },
|
||||
}
|
||||
|
||||
impl From<ToolArgumentsRejection> for EditorExecuteError {
|
||||
fn from(rejection: ToolArgumentsRejection) -> Self {
|
||||
Self::Arguments(rejection)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ProjectPermissionRejection> for EditorExecuteError {
|
||||
fn from(rejection: ProjectPermissionRejection) -> Self {
|
||||
Self::PermissionDenied(rejection)
|
||||
}
|
||||
}
|
||||
|
||||
impl ToolFailure for EditorExecuteError {
|
||||
fn to_user_msg(&self) -> String {
|
||||
match self {
|
||||
Self::PluginUnavailable { editor } => {
|
||||
format!("当前 {} 插件不可用。", editor.label())
|
||||
}
|
||||
Self::Arguments(rejection) => rejection.to_user_msg(),
|
||||
Self::PermissionDenied(rejection) => rejection.to_user_msg(),
|
||||
Self::CodeNotText { editor } => {
|
||||
format!("code 必须是 {}。", editor.language())
|
||||
}
|
||||
Self::CodeInvalid => "code 不能为空、包含 NUL 或超过 128 KiB。".to_string(),
|
||||
Self::EditorRejected { cause } => cause.to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
pub mod error;
|
||||
@@ -0,0 +1,35 @@
|
||||
//! `agc_environment_check` 的错误定义:每个 case 一个变体,文案写在变体上。
|
||||
//!
|
||||
//! 这里只放错误本身;参数解析与工具执行留在 `direct_tool_bridge`。
|
||||
|
||||
use crate::agent::tool::error::{
|
||||
ProjectPermissionRejection, ToolArgumentsRejection, ToolFailure,
|
||||
};
|
||||
|
||||
/// `agc_environment_check` 不接受任何参数,所以它自己的失败只有权限门禁一类。
|
||||
#[derive(Debug)]
|
||||
pub(crate) enum EnvironmentCheckError {
|
||||
Arguments(ToolArgumentsRejection),
|
||||
PermissionDenied(ProjectPermissionRejection),
|
||||
}
|
||||
|
||||
impl From<ToolArgumentsRejection> for EnvironmentCheckError {
|
||||
fn from(rejection: ToolArgumentsRejection) -> Self {
|
||||
Self::Arguments(rejection)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ProjectPermissionRejection> for EnvironmentCheckError {
|
||||
fn from(rejection: ProjectPermissionRejection) -> Self {
|
||||
Self::PermissionDenied(rejection)
|
||||
}
|
||||
}
|
||||
|
||||
impl ToolFailure for EnvironmentCheckError {
|
||||
fn to_user_msg(&self) -> String {
|
||||
match self {
|
||||
Self::Arguments(rejection) => rejection.to_user_msg(),
|
||||
Self::PermissionDenied(rejection) => rejection.to_user_msg(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
pub mod error;
|
||||
@@ -0,0 +1,223 @@
|
||||
//! 内置工具失败的公共约定与跨工具复用的错误。
|
||||
//!
|
||||
//! 每个工具定义自己的错误 enum:一个 case 一个变体,文案写在变体上,捕获处只调
|
||||
//! [`ToolFailure::to_user_msg`]。跨工具重复出现的 case 在这里定义一次,各工具 enum 用
|
||||
//! 包装变体 + `From` 复用它,不复制文案。
|
||||
|
||||
use serde_json::Value;
|
||||
|
||||
/// 工具失败对用户(以及转述给用户的模型)说的那一句话。
|
||||
pub(crate) trait ToolFailure {
|
||||
fn to_user_msg(&self) -> String;
|
||||
|
||||
/// 这句话落进工具结果前允许保留的字符数上限。
|
||||
///
|
||||
/// 默认与 `bridge_tool_failure` 的历史口径一致;验证与补丁类失败会带构建/试玩原文,
|
||||
/// 历史上给的是更大的预算,所以由错误自己声明。
|
||||
fn redact_limit(&self) -> usize {
|
||||
480
|
||||
}
|
||||
}
|
||||
|
||||
/// 所有内置工具共用的入参形状校验:入参不是对象,或带了没声明的字段。
|
||||
///
|
||||
/// 只有这两个 case 是所有工具共享的;具体字段自己的语义(空值、超长、越界……)各工具
|
||||
/// 用自己 enum 的变体表达。
|
||||
#[derive(Debug)]
|
||||
pub(crate) enum ToolArgumentsRejection {
|
||||
NotObject,
|
||||
UnknownField { field: String },
|
||||
}
|
||||
|
||||
impl ToolArgumentsRejection {
|
||||
/// 按该工具声明的字段表校验入参形状。
|
||||
pub(crate) fn check(arguments: &Value, allowed: &[&str]) -> Result<(), Self> {
|
||||
let object = arguments.as_object().ok_or(Self::NotObject)?;
|
||||
match object
|
||||
.keys()
|
||||
.find(|field| !allowed.contains(&field.as_str()))
|
||||
{
|
||||
Some(field) => Err(Self::UnknownField {
|
||||
field: field.to_string(),
|
||||
}),
|
||||
None => Ok(()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ToolFailure for ToolArgumentsRejection {
|
||||
fn to_user_msg(&self) -> String {
|
||||
match self {
|
||||
Self::NotObject => "工具参数必须是对象。".to_string(),
|
||||
Self::UnknownField { field } => format!(
|
||||
"不认识的工具参数「{field}」,请只传该工具声明支持过的字段。"
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 项目权限门禁拒绝该动作,或策略本身读不出来。
|
||||
///
|
||||
/// 两种失败各占一个变体;产生方是 `project::verification` 的两个门禁函数。
|
||||
#[derive(Debug)]
|
||||
pub(crate) enum ProjectPermissionRejection {
|
||||
Denied { command_id: String },
|
||||
PolicyUnavailable { cause: String },
|
||||
}
|
||||
|
||||
impl ToolFailure for ProjectPermissionRejection {
|
||||
fn to_user_msg(&self) -> String {
|
||||
match self {
|
||||
// 这段文案是对外契约:前端(`App.tsx` 的 `agent.resume` 分支、
|
||||
// `useProjectResourceCardPreviews`)与多个 Rust 测试都按「项目权限策略拒绝执行:{id}」
|
||||
// 的子串判断,所以这里只做去重收口,不改字面。
|
||||
Self::Denied { command_id } => {
|
||||
format!("项目权限策略拒绝执行:{command_id}")
|
||||
}
|
||||
Self::PolicyUnavailable { cause } => format!(
|
||||
"读取项目权限策略失败,无法确认该动作是否被允许,本次操作未执行:{cause}"
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 列表工具共用的分页参数解析失败。
|
||||
///
|
||||
/// 三个列表工具的 `offset`/`limit` 语义完全一致,只是默认值与上限不同;上限随变体带上,
|
||||
/// 所以文案只需要一份。
|
||||
#[derive(Debug)]
|
||||
pub(crate) enum ToolPageRejection {
|
||||
OffsetNotInteger { got: String },
|
||||
OffsetTooLarge { got: usize, max: usize },
|
||||
LimitNotInteger { got: String },
|
||||
LimitOutOfRange { got: usize, max: usize },
|
||||
}
|
||||
|
||||
impl ToolFailure for ToolPageRejection {
|
||||
fn to_user_msg(&self) -> String {
|
||||
match self {
|
||||
Self::OffsetNotInteger { got } => {
|
||||
format!("工具参数 offset 必须是非负整数,当前是「{got}」。")
|
||||
}
|
||||
Self::OffsetTooLarge { got, max } => format!(
|
||||
"工具参数 offset={got} 超出上限 {max},请从更小的偏移重新分页。"
|
||||
),
|
||||
Self::LimitNotInteger { got } => {
|
||||
format!("工具参数 limit 必须是整数,当前是「{got}」。")
|
||||
}
|
||||
Self::LimitOutOfRange { got, max } => {
|
||||
format!("工具参数 limit={got} 超出范围,必须在 1 到 {max} 之间。")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 本地项目清单(manifest)读不出来:清单文件损坏、权限不足或项目尚未初始化。
|
||||
///
|
||||
/// 读清单是多个工具的公共第一步,失败语义相同,因此文案只在这里写一份。
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct ProjectManifestRejection {
|
||||
pub(crate) cause: String,
|
||||
}
|
||||
|
||||
impl ToolFailure for ProjectManifestRejection {
|
||||
fn to_user_msg(&self) -> String {
|
||||
format!("读取本地项目清单失败,无法继续:{}", self.cause)
|
||||
}
|
||||
}
|
||||
|
||||
/// 资源生成/抠图完成后的登记投影失败。
|
||||
///
|
||||
/// `agc_create_or_derive_resource` 与 `agc_remove_background` 共用同一个完成投影,
|
||||
/// 失败语义相同,因此文案只在这里写一份。
|
||||
#[derive(Debug)]
|
||||
pub(crate) enum ResourceCompletionRejection {
|
||||
MissingAsset,
|
||||
WarningsUnreadable { cause: String },
|
||||
}
|
||||
|
||||
impl ToolFailure for ResourceCompletionRejection {
|
||||
fn to_user_msg(&self) -> String {
|
||||
match self {
|
||||
Self::MissingAsset => "媒体资源生成完成但没有登记 asset。".to_string(),
|
||||
Self::WarningsUnreadable { cause } => {
|
||||
format!("读取资源编辑告警失败:{cause}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 只有客户端 Direct 回合才会放行的工具被独立客户端 MCP 调用。
|
||||
///
|
||||
/// 独立客户端 MCP 没有 Direct 回合的执行账本,不能借用其它回合的预算,所以这里直接拒绝,
|
||||
/// 而不是让工具在半途发现账本不存在。
|
||||
#[derive(Debug)]
|
||||
pub(crate) enum DirectTurnGateRejection {
|
||||
/// 该工具整体属于客户端 Direct 回合。
|
||||
ToolRequiresDirectTurn { tool: String },
|
||||
/// 验证类入口需要本回合的执行账本。
|
||||
ValidationRequiresDirectTurn,
|
||||
}
|
||||
|
||||
impl ToolFailure for DirectTurnGateRejection {
|
||||
fn to_user_msg(&self) -> String {
|
||||
match self {
|
||||
Self::ToolRequiresDirectTurn { tool } => {
|
||||
format!("工具「{tool}」属于客户端 Direct 回合,请从对应回合操作。")
|
||||
}
|
||||
Self::ValidationRequiresDirectTurn => {
|
||||
"该验证入口需要客户端 Direct 回合;独立客户端 MCP 不复用其它回合的预算。"
|
||||
.to_string()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 客户端工具桥收到没有登记过的工具名。
|
||||
///
|
||||
/// 带上收到的名字:模型拼错工具名时要能从错误里看出自己写了什么,而不是只知道"不认识"。
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct UnknownClientToolRejection {
|
||||
pub(crate) tool: String,
|
||||
}
|
||||
|
||||
impl ToolFailure for UnknownClientToolRejection {
|
||||
fn to_user_msg(&self) -> String {
|
||||
format!("未知或未审核的客户端工具「{}」。", self.tool)
|
||||
}
|
||||
}
|
||||
|
||||
/// 宿主执行门禁在派发前拒绝,或者派发后的执行回执没有落盘。
|
||||
///
|
||||
/// 这些都不是某个工具的入参问题,而是宿主这一侧的事实;任何工具都可能碰到,所以在这里
|
||||
/// 定义一次。
|
||||
#[derive(Debug)]
|
||||
pub(crate) enum DirectExecutionGateRejection {
|
||||
/// 执行许可(付费/写入/执行租约)取不到。
|
||||
PermitUnavailable { cause: String },
|
||||
/// 取执行许可的阻塞任务没有返回。
|
||||
PermitTaskLost,
|
||||
/// 回合执行会话 / 计划会话取不到。
|
||||
SessionUnavailable { cause: String },
|
||||
/// 取执行会话的阻塞任务没有返回。
|
||||
SessionTaskLost,
|
||||
/// 工具已返回,但执行回执没有可靠落盘。
|
||||
ReceiptNotPersisted,
|
||||
}
|
||||
|
||||
impl ToolFailure for DirectExecutionGateRejection {
|
||||
fn to_user_msg(&self) -> String {
|
||||
match self {
|
||||
Self::PermitUnavailable { cause } => {
|
||||
format!("本轮执行许可不可用,未派发工具调用:{cause}")
|
||||
}
|
||||
Self::PermitTaskLost => "宿主执行许可任务未返回,未派发工具调用。".to_string(),
|
||||
Self::SessionUnavailable { cause } => format!("本轮执行会话不可用:{cause}"),
|
||||
Self::SessionTaskLost => "宿主执行会话任务未返回。".to_string(),
|
||||
Self::ReceiptNotPersisted => {
|
||||
"工具已返回,但宿主执行回执未可靠落盘;请查看交付状态并核对原操作,不自动重放"
|
||||
.to_string()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user