内置工具失败改成 typed 错误穿出派发边界,事件带原始 error,软失败清零

- dispatch 返回 Result<Value, ToolCallError>:不再在工具内部吞掉错误再补一个失败 Value,handle_direct_tool_bridge 在唯一一处 match 里同时组装发给模型的内容与诊断
- 新增 ToolCallError { message, redact_limit, error, images },error 直接存序列化后的 typed 错误;每个内置工具在自己的 agent/tool/<tool>/error.rs 定义 typed enum,共用 case(参数不是对象 / 未审核字段等)抽到 agent/tool/error.rs 的 ToolFailure
- AgentRuntimeErrorEvent 增加 error 字段携带原始 typed 错误,schema 升到 agent-runtime-error.v3;应用日志详情行扩成 message=… error=… metadata=…,各带独立长度预算
- 软失败清零:import_account_assets、run_validation、browser_playtest、environment_check、apply_patch、editor_execute、cocos_execute 原先返回 Ok(失败载荷) 的分支改为 typed 错误,并把 status / dispatched / retryAllowed 等载荷带进错误,模型仍能看到
- 失败证据截图走 ToolFailure::attached_images 与 ToolCallError.images,截图放 #[serde(skip)] 字段,不混进诊断里的 error
- bridge_tool_result 去掉 is_error 入参,isError 只在组装处按 Ok/Err 分支写一次;MCP 线上 isError 字段与校验不变
- 修正上一提交遗留的两处 rustfmt:direct_tools_mcp.rs、runtime_tools/context.rs(同一 workspace 整体格式门禁要求)
- 同步更新技术方案文档字段表与共享记忆决策记录

验证:cargo check --bin genarrative-ai-game-creator-shell --tests 通过;cargo test --bin genarrative-ai-game-creator-shell -- agent:: 951 passed / 0 failed;npm run check:encoding、git diff --check 通过;Windows 专属 cocos/unity/godot 分支在 Linux 下临时去 cfg 交叉编译通过(无真实 Windows 构建)
This commit is contained in:
2026-10-01 13:30:32 +08:00
parent 4999653a5e
commit 9372845c35
31 changed files with 1045 additions and 795 deletions
@@ -2013,6 +2013,7 @@ pub(crate) fn record_direct_codex_failure(
stage.id(),
error_code,
&detail,
serde_json::to_value(failure).unwrap_or(serde_json::Value::Null),
None,
serde_json::json!({
"legacyDiagnosticWritten": diagnostic_written,
File diff suppressed because it is too large Load Diff
@@ -822,8 +822,7 @@ fn bounded_tool_string(arguments: &Value, field: &str, max_chars: usize) -> Resu
fn validate_tool_object_fields(arguments: &Value, allowed: &[&str]) -> Result<(), String> {
// 「入参不是对象 / 带了没声明的字段」是内置工具共用的两个 case:文案只写在
// `ToolArgumentsRejection` 上,这里只负责渲染成 MCP 结果文本。
ToolArgumentsRejection::check(arguments, allowed)
.map_err(|rejection| rejection.to_user_msg())
ToolArgumentsRejection::check(arguments, allowed).map_err(|rejection| rejection.to_user_msg())
}
/// 与工具桥共用同一份 `agc_list_registered_assets` 入参规则。
@@ -9403,16 +9403,15 @@ mod canvas_generation_tests {
])
)
).await.expect("both bridge requests must finish without serializing their POSTs");
let duplicate = results.pop().expect("duplicate request result");
assert_eq!(duplicate["isError"], true, "{duplicate}");
assert!(duplicate["content"][0]["text"]
.as_str()
.unwrap()
.contains("任何远端 POST 前拒绝并发请求"));
let duplicate = results
.pop()
.expect("duplicate request result")
.expect_err("duplicate request must be rejected before any remote POST");
assert!(duplicate.message.contains("任何远端 POST 前拒绝并发请求"));
let ids = results
.into_iter()
.map(|result| {
assert_eq!(result["isError"], false, "{result}");
let result = result.expect("bridge generation request must succeed");
let text = result["content"][0]["text"]
.as_str()
.expect("bridge result text");
@@ -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.v2";
pub(crate) const AGENT_RUNTIME_ERROR_SCHEMA_VERSION: &str = "agent-runtime-error.v3";
/// sidecar 里 `message` 的字符上限。
pub(crate) const AGENT_RUNTIME_ERROR_MAX_MESSAGE_CHARS: usize = 8 * 1024;
@@ -23,6 +23,7 @@ pub(crate) const AGENT_RUNTIME_ERROR_MAX_MESSAGE_CHARS: usize = 8 * 1024;
/// 这里的预算留出身份字段的位置,保证被截掉的是正文尾部,而不是 `eventId`、`code`
/// 或 `detailRef`。
pub(crate) const AGENT_RUNTIME_ERROR_APP_LOG_MESSAGE_CHARS: usize = 1_200;
pub(crate) const AGENT_RUNTIME_ERROR_APP_LOG_ERROR_CHARS: usize = 400;
pub(crate) const AGENT_RUNTIME_ERROR_APP_LOG_METADATA_CHARS: usize = 200;
static ERROR_EVENT_SEQUENCE: AtomicU64 = AtomicU64::new(1);
@@ -38,6 +39,9 @@ pub(crate) struct AgentRuntimeErrorEvent {
pub occurred_at_unix_nanos: String,
pub elapsed_ms: Option<u64>,
pub message: String,
/// 序列化后的 typed 错误 enum(一个 case 一个变体)。这是开发者侧的结构化事实;
/// 没有 typed 错误的调用方传 `Value::Null`。
pub error: Value,
pub detail_ref: String,
pub persistence_failed: bool,
pub metadata: Value,
@@ -50,6 +54,7 @@ pub(crate) fn persist_agent_runtime_error(
stage: &str,
code: &str,
message: &str,
error: Value,
elapsed_ms: Option<u64>,
metadata: Value,
) -> Result<AgentRuntimeErrorEvent, String> {
@@ -72,6 +77,7 @@ pub(crate) fn persist_agent_runtime_error(
"occurredAtUnixNanos": occurred_at_unix_nanos.to_string(),
"elapsedMs": elapsed_ms,
"message": safe_message,
"error": error,
"metadata": metadata,
});
// 统一错误事件的项目内 sidecar 只在项目目录可见:用户提交错误报告时上传的是 AppData
@@ -86,6 +92,7 @@ pub(crate) fn persist_agent_runtime_error(
code,
&detail_ref,
message,
&error,
elapsed_ms,
&metadata,
);
@@ -109,6 +116,7 @@ pub(crate) fn persist_agent_runtime_error(
occurred_at_unix_nanos: occurred_at_unix_nanos.to_string(),
elapsed_ms,
message: message.to_string(),
error,
detail_ref,
persistence_failed: false,
metadata,
@@ -138,6 +146,7 @@ pub(crate) fn agent_runtime_error_app_log_lines(
code: &str,
detail_ref: &str,
message: &str,
error: &Value,
elapsed_ms: Option<u64>,
metadata: &Value,
) -> [String; 2] {
@@ -158,13 +167,18 @@ pub(crate) fn agent_runtime_error_app_log_lines(
message,
AGENT_RUNTIME_ERROR_APP_LOG_MESSAGE_CHARS,
));
let error = single_line_log_field(&redact_agent_runtime_error(
root,
&error.to_string(),
AGENT_RUNTIME_ERROR_APP_LOG_ERROR_CHARS,
));
let metadata = single_line_log_field(&redact_agent_runtime_error(
root,
&metadata.to_string(),
AGENT_RUNTIME_ERROR_APP_LOG_METADATA_CHARS,
));
let detail_line = format!(
"agent.runtime.error.detail eventId={event_id} message={message} metadata={metadata}"
"agent.runtime.error.detail eventId={event_id} message={message} error={error} metadata={metadata}"
);
[identity, detail_line]
}
@@ -216,13 +230,18 @@ mod tests {
"code-generation",
"turn-idle-timeout",
"本轮没有收到完成事件 C:\\Users\\private\\project https://provider.example/a?token=secret",
serde_json::json!({ "type": "timedOut", "deadline": "idle" }),
Some(1200),
serde_json::json!({"lastEvent":"item/started"}),
)
.expect("persist event");
assert_eq!(event.code, "turn-idle-timeout");
assert_eq!(event.message, "本轮没有收到完成事件 C:\\Users\\private\\project https://provider.example/a?token=secret");
let path = root.join(&event.detail_ref);
let text = std::fs::read_to_string(path).expect("diagnostic");
// typed 错误原样进 sidecar:开发侧按变体与字段定位,不解析那句中文。
assert!(text.contains("timedOut"), "{text}");
assert!(text.contains("idle"), "{text}");
assert!(text.contains("<absolute-path>"));
assert!(text.contains("<redacted-url>"));
assert!(!text.contains("token=secret"));
@@ -245,6 +264,7 @@ mod tests {
"turn-idle-timeout",
".agent/runtime/errors/error-1-1.json",
"本轮没有收到完成事件\n附带换行 C:\\Users\\private\\project https://provider.example/a?token=secret\n第二行诊断",
&serde_json::json!({ "type": "timedOut", "deadline": "idle" }),
Some(1200),
&serde_json::json!({"authorization": "Bearer secret"}),
);
@@ -276,6 +296,10 @@ mod tests {
),
"{detail}"
);
assert!(
detail.contains("error={\"deadline\":\"idle\",\"type\":\"timedOut\"}"),
"{detail}"
);
assert!(!detail.contains("Bearer secret"), "{detail}");
// 自由文本里出现裸标记词时,被整行替换的只能是详情行:身份行仍可定位事件。
@@ -288,6 +312,7 @@ mod tests {
"tool-error",
".agent/runtime/errors/error-3-1.json",
"credential rotation failed",
&Value::Null,
None,
&serde_json::json!({"tool": "agc_tools"}),
);
@@ -111,6 +111,7 @@ pub(crate) fn append_game_creator_agent_runtime_terminal_public_message_at(
&state.phase,
"agent-runtime-terminal",
error,
serde_json::Value::Null,
None,
serde_json::json!({
"agentId": state.agent_id,
@@ -282,10 +282,9 @@ pub(in crate::agent) async fn observe_agent_runtime_account_asset_library(
root: &Path,
input: &serde_json::Value,
) -> AgentRuntimeToolObservation {
if let Err(rejection) = ToolArgumentsRejection::check(
input,
&["folderId", "query", "offset", "limit"],
) {
if let Err(rejection) =
ToolArgumentsRejection::check(input, &["folderId", "query", "offset", "limit"])
{
return AgentRuntimeToolObservation {
tool: "asset.library.list".to_string(),
status: "rejected".to_string(),
@@ -3,25 +3,32 @@
//! 这里只放错误本身;参数解析与工具执行留在 `direct_tool_bridge` 与 `direct_patch`。
use crate::agent::tool::error::ToolFailure;
use serde_json::Value;
/// `agc_apply_patch` 的每一种失败各占一个变体;文案写在这里,捕获处只调 `to_user_msg`。
///
/// 补丁被拒绝(`status != completed`)不是错误:那是补丁工具自己的结果载荷,仍然原样回给
/// 模型,好让它看到哪一段没打上。
#[derive(Debug)]
#[derive(serde::Serialize, Debug)]
pub(crate) enum ApplyPatchError {
/// 补丁没被受理:入参不合法、作用域越界或执行许可已关闭。
PatchRejected { cause: String },
/// 补丁受理了但没打上或只打了一部分(`status != completed`)。
///
/// `report` 是补丁工具自己的结果载荷(逐文件命中与冲突),原样带给模型,
/// 好让它看到哪一段没打上。
PatchNotApplied { report: Value },
}
impl ToolFailure for ApplyPatchError {
fn to_user_msg(&self) -> String {
match self {
Self::PatchRejected { cause } => cause.to_string(),
Self::PatchNotApplied { report } => format!("补丁未完全应用:{report}"),
}
}
fn redact_limit(&self) -> usize {
1800
match self {
Self::PatchNotApplied { .. } => 32_000,
_ => 1800,
}
}
}
@@ -3,22 +3,43 @@
//! 这里只放错误本身;参数解析与工具执行留在 `direct_tool_bridge` 与 `direct_validation`。
use crate::agent::tool::error::ToolFailure;
use serde_json::Value;
/// `agc_browser_playtest` 的每一种失败各占一个变体;文案写在这里,捕获处只调 `to_user_msg`。
#[derive(Debug)]
#[derive(serde::Serialize, Debug)]
pub(crate) enum BrowserPlaytestError {
/// 浏览器试玩自身失败;原因来自 `direct_validation` 的稳定前缀码。
PlaytestFailed { cause: String },
/// 试玩跑完了但没通过:`report` 是试玩结果载荷(含逐视口结论与截图路径)。
///
/// 截图正文单独带在 `screenshots` 上:它要变成失败结果里的 MCP image block,
/// 但不进错误正文(base64 会把诊断 sidecar 灌爆)。
PlaytestNotPassed {
report: Value,
#[serde(skip)]
screenshots: Vec<String>,
},
}
impl ToolFailure for BrowserPlaytestError {
fn to_user_msg(&self) -> String {
match self {
Self::PlaytestFailed { cause } => cause.to_string(),
Self::PlaytestNotPassed { report, .. } => format!("浏览器试玩未通过:{report}"),
}
}
fn redact_limit(&self) -> usize {
1800
match self {
Self::PlaytestNotPassed { .. } => 32_000,
_ => 1800,
}
}
fn attached_images(&self) -> Vec<String> {
match self {
Self::PlaytestNotPassed { screenshots, .. } => screenshots.clone(),
_ => Vec::new(),
}
}
}
@@ -2,24 +2,39 @@
//!
//! 这里只放错误本身;参数解析与工具执行留在 `direct_tool_bridge`。
use crate::agent::tool::error::{
ProjectPermissionRejection, ToolArgumentsRejection, ToolFailure,
};
use crate::agent::tool::error::{ProjectPermissionRejection, ToolArgumentsRejection, ToolFailure};
use serde_json::Value;
/// Cocos 执行工具的每一种失败各占一个变体;文案写在这里,捕获处只调 `to_user_msg`。
///
/// 已有的"待核对""执行中"两种状态不是错误:它们各自带 `retryAllowed` 载荷,仍原样回给模型。
#[derive(Debug)]
#[derive(serde::Serialize, Debug)]
pub(crate) enum CocosExecuteError {
PluginDisabled,
PluginUnavailable,
PermissionDenied(ProjectPermissionRejection),
Arguments(ToolArgumentsRejection),
UnknownOperation { operation: String },
SchemaInvalid { cause: String },
OperationArgumentsInvalid { cause: String },
UnknownOperation {
operation: String,
},
SchemaInvalid {
cause: String,
},
OperationArgumentsInvalid {
cause: String,
},
CodeNotText,
CodeInvalid { cause: String },
CodeInvalid {
cause: String,
},
/// 已有 Cocos 操作在执行中:本次不发命令,也不要重放。
ExecutionBusy,
/// 先前的 execute 结果待核对:当前不再发送执行命令,也不要自动重放。
ReconciliationPending,
/// execute 已发出但回执不是 `completed`:`report` 带 `status` / `retryAllowed` 等现场事实。
ExecutionNotCompleted {
report: Value,
#[serde(skip)]
screenshots: Vec<String>,
},
}
impl From<ToolArgumentsRejection> for CocosExecuteError {
@@ -52,6 +67,32 @@ impl ToolFailure for CocosExecuteError {
}
Self::CodeNotText => "code 必须是 JavaScript 函数体。".to_string(),
Self::CodeInvalid { cause } => cause.to_string(),
Self::ExecutionBusy => {
"已有 Cocos 操作执行中,请等待回执;本次调用没有发送执行命令,也不要重放。"
.to_string()
}
Self::ReconciliationPending => {
"先前 Cocos execute 的结果待核对,当前不再发送执行命令,也不要自动重放;\
请人工核对编辑器现场。"
.to_string()
}
Self::ExecutionNotCompleted { report, .. } => {
format!("Cocos 执行未完成:{report}")
}
}
}
fn redact_limit(&self) -> usize {
match self {
Self::ExecutionNotCompleted { .. } => 32_000,
_ => 480,
}
}
fn attached_images(&self) -> Vec<String> {
match self {
Self::ExecutionNotCompleted { screenshots, .. } => screenshots.clone(),
_ => Vec::new(),
}
}
}
@@ -14,25 +14,41 @@ const UNREGISTERED_SOURCE_HINT: &str = "先调用 agc_list_registered_assets 选
/// `agc_create_or_derive_resource` 的每一种失败各占一个变体;文案写在这里,捕获处只调
/// `to_user_msg`。
#[derive(Debug)]
#[derive(serde::Serialize, Debug)]
pub(crate) enum CreateOrDeriveResourceError {
Arguments(ToolArgumentsRejection),
PermissionDenied(ProjectPermissionRejection),
Manifest(ProjectManifestRejection),
Completion(ResourceCompletionRejection),
KindEmpty,
KindTooLong { got_chars: usize, max_chars: usize },
KindTooLong {
got_chars: usize,
max_chars: usize,
},
KindHasControlCharacters,
KindUnsupported { got: String },
KindUnsupported {
got: String,
},
ModeEmpty,
ModeTooLong { got_chars: usize, max_chars: usize },
ModeTooLong {
got_chars: usize,
max_chars: usize,
},
ModeHasControlCharacters,
ModeUnsupported { got: String },
ModeUnsupported {
got: String,
},
SourceLocalAssetIdEmpty,
SourceLocalAssetIdTooLong { got_chars: usize, max_chars: usize },
SourceLocalAssetIdTooLong {
got_chars: usize,
max_chars: usize,
},
SourceLocalAssetIdHasControlCharacters,
PromptEmpty,
PromptTooLong { got_chars: usize, max_chars: usize },
PromptTooLong {
got_chars: usize,
max_chars: usize,
},
PromptHasControlCharacters,
PromptTooLongForKind {
kind: DirectResourceGenerationKind,
@@ -40,7 +56,10 @@ pub(crate) enum CreateOrDeriveResourceError {
max_chars: usize,
},
AssetNameEmpty,
AssetNameTooLong { got_chars: usize, max_chars: usize },
AssetNameTooLong {
got_chars: usize,
max_chars: usize,
},
AssetNameHasControlCharacters,
ImageCreationRequiresRegisteredSource,
CharacterAnimationRequiresRegisteredSource,
@@ -48,13 +67,25 @@ pub(crate) enum CreateOrDeriveResourceError {
DeriveModeWithoutSource,
SourceAssetNotRegistered,
ProjectPathUnprojectable,
GenerationQueueUnavailable { cause: String },
PaidDispatchRefused { cause: String },
PendingEditsUnreadable { cause: String },
GenerationQueueUnavailable {
cause: String,
},
PaidDispatchRefused {
cause: String,
},
PendingEditsUnreadable {
cause: String,
},
DuplicatePendingOperations,
TurnIdUnavailable { cause: String },
ProjectRevisionUnavailable { cause: String },
ResourceGenerationFailed { cause: String },
TurnIdUnavailable {
cause: String,
},
ProjectRevisionUnavailable {
cause: String,
},
ResourceGenerationFailed {
cause: String,
},
}
impl From<ToolArgumentsRejection> for CreateOrDeriveResourceError {
@@ -5,7 +5,7 @@
use crate::agent::tool::error::{DirectExecutionGateRejection, ToolFailure};
/// `agc_delivery_status` 的每一种失败各占一个变体;文案写在这里,捕获处只调 `to_user_msg`。
#[derive(Debug)]
#[derive(serde::Serialize, Debug)]
pub(crate) enum DeliveryStatusError {
/// 只读状态工具不接受任何参数。
ArgumentsNotAccepted,
@@ -6,14 +6,11 @@ use crate::agent::tool::create_or_derive_resource::error::CreateOrDeriveResource
use crate::agent::tool::error::{ToolArgumentsRejection, ToolFailure};
/// `agc_edit_image` 的每一种失败各占一个变体;文案写在这里,捕获处只调 `to_user_msg`。
#[derive(Debug)]
#[derive(serde::Serialize, Debug)]
pub(crate) enum EditImageError {
Arguments(ToolArgumentsRejection),
SourceLocalAssetIdEmpty,
SourceLocalAssetIdTooLong {
got_chars: usize,
max_chars: usize,
},
SourceLocalAssetIdTooLong { got_chars: usize, max_chars: usize },
SourceLocalAssetIdHasControlCharacters,
PromptEmpty,
PromptTooLong { got_chars: usize, max_chars: usize },
@@ -40,7 +37,9 @@ 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::SourceLocalAssetIdEmpty => {
"编辑图片失败:sourceLocalAssetId 不能为空。".to_string()
}
Self::SourceLocalAssetIdTooLong {
got_chars,
max_chars,
@@ -55,9 +54,7 @@ impl ToolFailure for EditImageError {
got_chars,
max_chars,
} => format!("编辑图片失败:prompt 有 {got_chars} 个字符,超过上限 {max_chars}。"),
Self::PromptHasControlCharacters => {
"编辑图片失败:prompt 含有控制字符。".to_string()
}
Self::PromptHasControlCharacters => "编辑图片失败:prompt 含有控制字符。".to_string(),
Self::AssetNameEmpty => "编辑图片失败:assetName 不能为空。".to_string(),
Self::AssetNameTooLong {
got_chars,
@@ -2,14 +2,13 @@
//!
//! 这里只放错误本身;参数解析与工具执行留在 `direct_tool_bridge`。
use crate::agent::tool::error::{
ProjectPermissionRejection, ToolArgumentsRejection, ToolFailure,
};
use crate::agent::tool::error::{ProjectPermissionRejection, ToolArgumentsRejection, ToolFailure};
use serde_json::Value;
/// 两个编辑器执行工具之间的差异:插件身份、给用户看的编辑器名、代码语言名。
///
/// 差异做成枚举,错误变体只带 `EditorKind`,不在文案里手写编辑器名。
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
#[derive(serde::Serialize, Clone, Copy, Debug, Eq, PartialEq)]
pub(crate) enum EditorKind {
Unity,
Godot,
@@ -39,15 +38,30 @@ impl EditorKind {
}
/// 编辑器执行工具的每一种失败各占一个变体;文案写在这里,捕获处只调 `to_user_msg`。
#[derive(Debug)]
#[derive(serde::Serialize, Debug)]
pub(crate) enum EditorExecuteError {
PluginUnavailable { editor: EditorKind },
PluginUnavailable {
editor: EditorKind,
},
Arguments(ToolArgumentsRejection),
PermissionDenied(ProjectPermissionRejection),
CodeNotText { editor: EditorKind },
CodeNotText {
editor: EditorKind,
},
CodeInvalid,
/// 编辑器适配器拒绝了这次执行,或回执不可用。
EditorRejected { cause: String },
EditorRejected {
cause: String,
},
/// 适配器返回了「未完成」的回执:`report` 里带 `dispatched` / `retryAllowed` 等现场事实。
ExecutionNotCompleted {
editor: EditorKind,
report: Value,
},
/// 执行任务异常退出,回执不可用:结果待核对,不要自动重放。
ExecutionUnconfirmed {
editor: EditorKind,
},
}
impl From<ToolArgumentsRejection> for EditorExecuteError {
@@ -75,6 +89,21 @@ impl ToolFailure for EditorExecuteError {
}
Self::CodeInvalid => "code 不能为空、包含 NUL 或超过 128 KiB。".to_string(),
Self::EditorRejected { cause } => cause.to_string(),
Self::ExecutionNotCompleted { editor, report } => {
format!("{} 执行未完成:{report}", editor.label())
}
Self::ExecutionUnconfirmed { editor } => format!(
"{} 执行任务异常,回执不可用;执行已发出,结果待核对,不要自动重放:\
{{\"status\":\"needs-reconciliation\",\"dispatched\":true,\"retryAllowed\":false}}",
editor.label()
),
}
}
fn redact_limit(&self) -> usize {
match self {
Self::ExecutionNotCompleted { .. } | Self::ExecutionUnconfirmed { .. } => 32_000,
_ => 480,
}
}
}
@@ -2,15 +2,18 @@
//!
//! 这里只放错误本身;参数解析与工具执行留在 `direct_tool_bridge`。
use crate::agent::tool::error::{
ProjectPermissionRejection, ToolArgumentsRejection, ToolFailure,
};
use crate::agent::tool::error::{ProjectPermissionRejection, ToolArgumentsRejection, ToolFailure};
use serde_json::Value;
/// `agc_environment_check` 不接受任何参数,所以它自己的失败只有权限门禁一类。
#[derive(Debug)]
/// `agc_environment_check` 不接受任何参数,所以它自己的失败只有权限门禁与环境未就绪两类。
#[derive(serde::Serialize, Debug)]
pub(crate) enum EnvironmentCheckError {
Arguments(ToolArgumentsRejection),
PermissionDenied(ProjectPermissionRejection),
/// 环境检查跑完了但结论不是 `ready`:`report` 是检查载荷,原样带给模型。
EnvironmentNotReady {
report: Value,
},
}
impl From<ToolArgumentsRejection> for EnvironmentCheckError {
@@ -30,6 +33,14 @@ impl ToolFailure for EnvironmentCheckError {
match self {
Self::Arguments(rejection) => rejection.to_user_msg(),
Self::PermissionDenied(rejection) => rejection.to_user_msg(),
Self::EnvironmentNotReady { report } => format!("本地环境未就绪:{report}"),
}
}
fn redact_limit(&self) -> usize {
match self {
Self::EnvironmentNotReady { .. } => 32_000,
_ => 480,
}
}
}
@@ -7,7 +7,7 @@
use serde_json::Value;
/// 工具失败对用户(以及转述给用户的模型)说的那一句话。
pub(crate) trait ToolFailure {
pub(crate) trait ToolFailure: serde::Serialize {
fn to_user_msg(&self) -> String;
/// 这句话落进工具结果前允许保留的字符数上限。
@@ -17,13 +17,21 @@ pub(crate) trait ToolFailure {
fn redact_limit(&self) -> usize {
480
}
/// 失败时仍要一并回给模型的证据图(MCP image block 的 base64 正文)。
///
/// 只有真正的证据类失败才带:浏览器验证没通过时的双端截图、Cocos 预览截图。
/// 证据图不进 `serde::Serialize` 的错误正文,避免 base64 正文灌爆诊断 sidecar。
fn attached_images(&self) -> Vec<String> {
Vec::new()
}
}
/// 所有内置工具共用的入参形状校验:入参不是对象,或带了没声明的字段。
///
/// 只有这两个 case 是所有工具共享的;具体字段自己的语义(空值、超长、越界……)各工具
/// 用自己 enum 的变体表达。
#[derive(Debug)]
#[derive(serde::Serialize, Debug)]
pub(crate) enum ToolArgumentsRejection {
NotObject,
UnknownField { field: String },
@@ -49,9 +57,9 @@ impl ToolFailure for ToolArgumentsRejection {
fn to_user_msg(&self) -> String {
match self {
Self::NotObject => "工具参数必须是对象。".to_string(),
Self::UnknownField { field } => format!(
"不认识的工具参数「{field}」,请只传该工具声明支持过的字段。"
),
Self::UnknownField { field } => {
format!("不认识的工具参数「{field}」,请只传该工具声明支持过的字段。")
}
}
}
}
@@ -59,7 +67,7 @@ impl ToolFailure for ToolArgumentsRejection {
/// 项目权限门禁拒绝该动作,或策略本身读不出来。
///
/// 两种失败各占一个变体;产生方是 `project::verification` 的两个门禁函数。
#[derive(Debug)]
#[derive(serde::Serialize, Debug)]
pub(crate) enum ProjectPermissionRejection {
Denied { command_id: String },
PolicyUnavailable { cause: String },
@@ -74,9 +82,9 @@ impl ToolFailure for ProjectPermissionRejection {
Self::Denied { command_id } => {
format!("项目权限策略拒绝执行:{command_id}")
}
Self::PolicyUnavailable { cause } => format!(
"读取项目权限策略失败,无法确认该动作是否被允许,本次操作未执行:{cause}"
),
Self::PolicyUnavailable { cause } => {
format!("读取项目权限策略失败,无法确认该动作是否被允许,本次操作未执行:{cause}")
}
}
}
}
@@ -85,7 +93,7 @@ impl ToolFailure for ProjectPermissionRejection {
///
/// 三个列表工具的 `offset`/`limit` 语义完全一致,只是默认值与上限不同;上限随变体带上,
/// 所以文案只需要一份。
#[derive(Debug)]
#[derive(serde::Serialize, Debug)]
pub(crate) enum ToolPageRejection {
OffsetNotInteger { got: String },
OffsetTooLarge { got: usize, max: usize },
@@ -99,9 +107,9 @@ impl ToolFailure for ToolPageRejection {
Self::OffsetNotInteger { got } => {
format!("工具参数 offset 必须是非负整数,当前是「{got}」。")
}
Self::OffsetTooLarge { got, max } => format!(
"工具参数 offset={got} 超出上限 {max},请从更小的偏移重新分页。"
),
Self::OffsetTooLarge { got, max } => {
format!("工具参数 offset={got} 超出上限 {max},请从更小的偏移重新分页。")
}
Self::LimitNotInteger { got } => {
format!("工具参数 limit 必须是整数,当前是「{got}」。")
}
@@ -115,7 +123,7 @@ impl ToolFailure for ToolPageRejection {
/// 本地项目清单(manifest)读不出来:清单文件损坏、权限不足或项目尚未初始化。
///
/// 读清单是多个工具的公共第一步,失败语义相同,因此文案只在这里写一份。
#[derive(Debug)]
#[derive(serde::Serialize, Debug)]
pub(crate) struct ProjectManifestRejection {
pub(crate) cause: String,
}
@@ -130,7 +138,7 @@ impl ToolFailure for ProjectManifestRejection {
///
/// `agc_create_or_derive_resource` 与 `agc_remove_background` 共用同一个完成投影,
/// 失败语义相同,因此文案只在这里写一份。
#[derive(Debug)]
#[derive(serde::Serialize, Debug)]
pub(crate) enum ResourceCompletionRejection {
MissingAsset,
WarningsUnreadable { cause: String },
@@ -151,7 +159,7 @@ impl ToolFailure for ResourceCompletionRejection {
///
/// 独立客户端 MCP 没有 Direct 回合的执行账本,不能借用其它回合的预算,所以这里直接拒绝,
/// 而不是让工具在半途发现账本不存在。
#[derive(Debug)]
#[derive(serde::Serialize, Debug)]
pub(crate) enum DirectTurnGateRejection {
/// 该工具整体属于客户端 Direct 回合。
ToolRequiresDirectTurn { tool: String },
@@ -176,7 +184,7 @@ impl ToolFailure for DirectTurnGateRejection {
/// 客户端工具桥收到没有登记过的工具名。
///
/// 带上收到的名字:模型拼错工具名时要能从错误里看出自己写了什么,而不是只知道"不认识"。
#[derive(Debug)]
#[derive(serde::Serialize, Debug)]
pub(crate) struct UnknownClientToolRejection {
pub(crate) tool: String,
}
@@ -191,7 +199,7 @@ impl ToolFailure for UnknownClientToolRejection {
///
/// 这些都不是某个工具的入参问题,而是宿主这一侧的事实;任何工具都可能碰到,所以在这里
/// 定义一次。
#[derive(Debug)]
#[derive(serde::Serialize, Debug)]
pub(crate) enum DirectExecutionGateRejection {
/// 执行许可(付费/写入/执行租约)取不到。
PermitUnavailable { cause: String },
@@ -3,13 +3,11 @@
//! 这里只放错误本身;参数解析与工具执行留在 `direct_tool_bridge`。
use crate::agent::generation::PLATFORM_ART_ASSET_GENERATION_KINDS;
use crate::agent::tool::error::{
ProjectPermissionRejection, ToolArgumentsRejection, ToolFailure,
};
use crate::agent::tool::error::{ProjectPermissionRejection, ToolArgumentsRejection, ToolFailure};
use shared_contracts::game_creation_app::GameCreationAppAssetKind;
/// `agc_generate_image` 的每一种失败各占一个变体;文案写在这里,捕获处只调 `to_user_msg`。
#[derive(Debug)]
#[derive(serde::Serialize, Debug)]
pub(crate) enum GenerateImageError {
Arguments(ToolArgumentsRejection),
PermissionDenied(ProjectPermissionRejection),
@@ -2,29 +2,54 @@
//!
//! 这里只放错误本身;参数解析与工具执行留在 `direct_tool_bridge`。
use crate::agent::tool::error::{
ProjectPermissionRejection, ToolArgumentsRejection, ToolFailure,
};
use crate::agent::tool::error::{ProjectPermissionRejection, ToolArgumentsRejection, ToolFailure};
use serde_json::Value;
/// `agc_import_account_assets` 的每一种失败各占一个变体;文案写在这里,捕获处只调
/// `to_user_msg`。
#[derive(Debug)]
#[derive(serde::Serialize, Debug)]
pub(crate) enum ImportAccountAssetsError {
Arguments(ToolArgumentsRejection),
PermissionDenied(ProjectPermissionRejection),
AssetIdsNotStringArray { got: String },
AssetIdsTooMany { max: usize },
AssetIdsNotStringArray {
got: String,
},
AssetIdsTooMany {
max: usize,
},
AssetIdsEntryEmpty,
AssetIdsEntryTooLong { got_chars: usize, max_chars: usize },
AssetIdsEntryTooLong {
got_chars: usize,
max_chars: usize,
},
AssetIdsEntryHasControlCharacters,
LocalPathsNotStringArray { got: String },
LocalPathsTooMany { max: usize },
LocalPathsNotStringArray {
got: String,
},
LocalPathsTooMany {
max: usize,
},
LocalPathsEntryEmpty,
LocalPathsEntryTooLong { got_chars: usize, max_chars: usize },
LocalPathsEntryTooLong {
got_chars: usize,
max_chars: usize,
},
LocalPathsEntryHasControlCharacters,
LocalPathsNotProjectRelative { got: String },
LocalPathsProtectedControlPath { got: String },
LocalPathsNotProjectRelative {
got: String,
},
LocalPathsProtectedControlPath {
got: String,
},
NoImportSource,
/// 一项都没登记上:`report` 是逐项失败载荷,原样带给模型。
ImportFailed {
report: Value,
},
/// 有一部分登记上了:`report` 里带成功的项与逐项失败原因,原样带给模型。
ImportPartial {
report: Value,
},
}
impl From<ToolArgumentsRejection> for ImportAccountAssetsError {
@@ -50,9 +75,7 @@ impl ToolFailure for ImportAccountAssetsError {
Self::AssetIdsTooMany { max } => {
format!("登记素材失败:assetIds 一次最多包含 {max} 项。")
}
Self::AssetIdsEntryEmpty => {
"登记素材失败:assetIds 只能包含非空字符串。".to_string()
}
Self::AssetIdsEntryEmpty => "登记素材失败:assetIds 只能包含非空字符串。".to_string(),
Self::AssetIdsEntryTooLong {
got_chars,
max_chars,
@@ -80,15 +103,24 @@ impl ToolFailure for ImportAccountAssetsError {
Self::LocalPathsEntryHasControlCharacters => {
"登记素材失败:localPaths 中存在含有控制字符的条目。".to_string()
}
Self::LocalPathsNotProjectRelative { got } => format!(
"登记素材失败:localPaths 的「{got}」不是受控项目根内的项目相对路径。"
),
Self::LocalPathsProtectedControlPath { got } => format!(
"登记素材失败:localPaths 的「{got}」命中隐藏、构建或敏感控制路径。"
),
Self::LocalPathsNotProjectRelative { got } => {
format!("登记素材失败:localPaths 的「{got}」不是受控项目根内的项目相对路径。")
}
Self::LocalPathsProtectedControlPath { got } => {
format!("登记素材失败:localPaths 的「{got}」命中隐藏、构建或敏感控制路径。")
}
Self::NoImportSource => {
"登记素材失败:至少提供一个非空的 assetIds 或 localPaths 数组。".to_string()
}
Self::ImportFailed { report } => format!("登记素材失败:{report}"),
Self::ImportPartial { report } => format!("登记素材部分成功:{report}"),
}
}
fn redact_limit(&self) -> usize {
match self {
Self::ImportFailed { .. } | Self::ImportPartial { .. } => 32_000,
_ => 480,
}
}
}
@@ -3,12 +3,12 @@
//! 这里只放错误本身;参数解析与工具执行留在 `direct_tool_bridge`。
use crate::agent::tool::error::{
ProjectPermissionRejection, ToolArgumentsRejection, ToolPageRejection, ToolFailure,
ProjectPermissionRejection, ToolArgumentsRejection, ToolFailure, ToolPageRejection,
};
/// `agc_list_account_assets` 的每一种失败各占一个变体;文案写在这里,捕获处只调
/// `to_user_msg`。
#[derive(Debug)]
#[derive(serde::Serialize, Debug)]
pub(crate) enum ListAccountAssetsError {
Arguments(ToolArgumentsRejection),
PermissionDenied(ProjectPermissionRejection),
@@ -50,9 +50,9 @@ impl ToolFailure for ListAccountAssetsError {
Self::FolderIdTooLong {
got_chars,
max_chars,
} => format!(
"列出账号素材失败:folderId 有 {got_chars} 个字符,超过上限 {max_chars}。"
),
} => {
format!("列出账号素材失败:folderId 有 {got_chars} 个字符,超过上限 {max_chars}。")
}
Self::FolderIdHasControlCharacters => {
"列出账号素材失败:folderId 含有控制字符。".to_string()
}
@@ -61,9 +61,7 @@ impl ToolFailure for ListAccountAssetsError {
got_chars,
max_chars,
} => format!("列出账号素材失败:query 有 {got_chars} 个字符,超过上限 {max_chars}。"),
Self::QueryHasControlCharacters => {
"列出账号素材失败:query 含有控制字符。".to_string()
}
Self::QueryHasControlCharacters => "列出账号素材失败:query 含有控制字符。".to_string(),
Self::AccountAssetsUnreadable { cause } => {
format!("列出账号素材失败:读取账号素材列表失败:{cause}")
}
@@ -3,8 +3,8 @@
//! 这里只放错误本身;参数解析与工具执行留在 `direct_tool_bridge`。
use crate::agent::tool::error::{
ProjectManifestRejection, ProjectPermissionRejection, ToolArgumentsRejection,
ToolPageRejection, ToolFailure,
ProjectManifestRejection, ProjectPermissionRejection, ToolArgumentsRejection, ToolFailure,
ToolPageRejection,
};
/// `agc_list_project_files` 允许的 `kind` 过滤值。
@@ -13,7 +13,7 @@ pub(crate) const LIST_PROJECT_FILES_KINDS: &[&str] = &[
];
/// `agc_list_project_files` 的每一种失败各占一个变体;文案写在这里,捕获处只调 `to_user_msg`。
#[derive(Debug)]
#[derive(serde::Serialize, Debug)]
pub(crate) enum ListProjectFilesError {
Arguments(ToolArgumentsRejection),
PermissionDenied(ProjectPermissionRejection),
@@ -3,13 +3,13 @@
//! 这里只放错误本身;参数解析与工具执行留在 `direct_tool_bridge`。
use crate::agent::tool::error::{
ProjectManifestRejection, ProjectPermissionRejection, ToolArgumentsRejection,
ToolPageRejection, ToolFailure,
ProjectManifestRejection, ProjectPermissionRejection, ToolArgumentsRejection, ToolFailure,
ToolPageRejection,
};
/// `agc_list_registered_assets` 的每一种失败各占一个变体;文案写在这里,捕获处只调
/// `to_user_msg`。
#[derive(Debug)]
#[derive(serde::Serialize, Debug)]
pub(crate) enum ListRegisteredAssetsError {
Arguments(ToolArgumentsRejection),
PermissionDenied(ProjectPermissionRejection),
@@ -2,17 +2,14 @@
//!
//! 这里只放错误本身;参数解析与工具执行留在 `direct_tool_bridge`。
use crate::agent::tool::error::{
ProjectPermissionRejection, ToolArgumentsRejection, ToolFailure,
};
use crate::agent::tool::error::{ProjectPermissionRejection, ToolArgumentsRejection, ToolFailure};
/// 美术重生成只接受客户端当前回合签发的授权;这里列出授权链自己的失败。
#[derive(Debug)]
#[derive(serde::Serialize, Debug)]
pub(crate) enum ArtRegenerationAuthorizationRejection {
StateUnavailable { cause: String },
MissingTurnAuthorization,
BoundToAnotherBrief,
CompletedResultNotSuccessful,
CompletedResultTurnExpired,
CompletedResultMismatch,
}
@@ -30,9 +27,6 @@ impl ToolFailure for ArtRegenerationAuthorizationRejection {
Self::BoundToAnotherBrief => {
"美术重生成失败:当前客户端回合已绑定另一项稳定美术重生成请求。".to_string()
}
Self::CompletedResultNotSuccessful => {
"美术重生成失败:拒绝缓存未成功的美术重生成结果。".to_string()
}
Self::CompletedResultTurnExpired => {
"美术重生成失败:美术重生成完成时客户端回合授权已失效。".to_string()
}
@@ -45,7 +39,7 @@ impl ToolFailure for ArtRegenerationAuthorizationRejection {
/// `taonier_prepare_game_art` 的每一种失败各占一个变体;文案写在这里,捕获处只调
/// `to_user_msg`。
#[derive(Debug)]
#[derive(serde::Serialize, Debug)]
pub(crate) enum PrepareGameArtError {
Arguments(ToolArgumentsRejection),
PermissionDenied(ProjectPermissionRejection),
@@ -6,7 +6,7 @@ use crate::agent::tool::error::ToolFailure;
/// `agc_read_project_context` 的每一种失败各占一个变体;文案写在这里,捕获处只调
/// `to_user_msg`。
#[derive(Debug)]
#[derive(serde::Serialize, Debug)]
pub(crate) enum ReadProjectContextError {
/// 上下文读取失败:入参不合法、路径不安全、快照复核中断或响应超预算。
/// 具体分类等 `direct_project_context` 给出 typed 出口后再拆变体。
@@ -6,7 +6,7 @@ use crate::agent::tool::error::{DirectExecutionGateRejection, ToolFailure};
/// `agc_register_delivery_contract` 的每一种失败各占一个变体;文案写在这里,捕获处只调
/// `to_user_msg`。
#[derive(Debug)]
#[derive(serde::Serialize, Debug)]
pub(crate) enum RegisterDeliveryContractError {
/// 宿主交付会话取不到。
Gate(DirectExecutionGateRejection),
@@ -9,7 +9,7 @@ use crate::agent::tool::error::{
/// `agc_remove_background` 的每一种失败各占一个变体;文案写在这里,捕获处只调
/// `to_user_msg`。
#[derive(Debug)]
#[derive(serde::Serialize, Debug)]
pub(crate) enum RemoveBackgroundError {
Arguments(ToolArgumentsRejection),
PermissionDenied(ProjectPermissionRejection),

Some files were not shown because too many files have changed in this diff Show More