Files
Genarrative/apps/ai-game-creator-shell/src-tauri/src/agent_native_tools.rs
T
kdletters 5822b64d7c 合并 Godot 编辑器插件与常用操作指导到主分支
接入 Godot 原生桥、受控执行、Runner 回执与编辑器操作指南
保留主分支 Cocos 和 Unity 跨工程能力及外置提示词结构
解决插件生命周期、工具目录、前端启动和文档合并冲突
2026-09-20 17:38:48 +08:00

2193 lines
90 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
use std::collections::{BTreeSet, HashSet};
use std::fmt;
use std::sync::OnceLock;
use agent_runtime_core::{CapabilityDefinition, CapabilityRegistry};
use platform_llm::{LlmFunctionTool, LlmToolCall};
use serde::de::{DeserializeOwned, Error as _, MapAccess, SeqAccess, Visitor};
use serde::Deserialize;
use serde_json::{json, Value};
use crate::agent::{
agent_runtime_native_executable_tools, AgentRuntimePlanUpdate, AgentRuntimeToolAction,
AgentRuntimeToolPlan, AGENT_RUNTIME_BACKGROUND_TOOL_ACTION_LIMIT,
AGENT_RUNTIME_CANVAS_ASSET_KINDS, AGENT_RUNTIME_PLAN_STEP_LIMIT,
};
#[cfg(test)]
use crate::GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID;
pub(crate) const AGENT_RUNTIME_UPDATE_PLAN_FUNCTION_NAME: &str = "update_agent_plan";
pub(crate) const AGENT_RUNTIME_RESPOND_FUNCTION_NAME: &str = "respond_to_user";
const AGENT_RUNTIME_NATIVE_TOOL_PREFIX: &str = "runtime_tool_";
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub(crate) enum AgentRuntimeToolPlanProtocolErrorKind {
ResponseShape,
CallIdentity,
UnknownFunction,
ArgumentsJson,
ArgumentsSchema,
BatchConstraint,
PlanSemantics,
}
impl AgentRuntimeToolPlanProtocolErrorKind {
pub(crate) fn as_str(self) -> &'static str {
match self {
Self::ResponseShape => "response-shape",
Self::CallIdentity => "call-identity",
Self::UnknownFunction => "unknown-function",
Self::ArgumentsJson => "arguments-json",
Self::ArgumentsSchema => "arguments-schema",
Self::BatchConstraint => "batch-constraint",
Self::PlanSemantics => "plan-semantics",
}
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub(crate) struct AgentRuntimeToolPlanProtocolError {
kind: AgentRuntimeToolPlanProtocolErrorKind,
detail: String,
}
impl AgentRuntimeToolPlanProtocolError {
pub(crate) fn new(
kind: AgentRuntimeToolPlanProtocolErrorKind,
detail: impl Into<String>,
) -> Self {
Self {
kind,
detail: detail.into(),
}
}
pub(crate) fn kind(&self) -> AgentRuntimeToolPlanProtocolErrorKind {
self.kind
}
}
impl fmt::Display for AgentRuntimeToolPlanProtocolError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(&self.detail)
}
}
fn protocol_error(
kind: AgentRuntimeToolPlanProtocolErrorKind,
detail: impl Into<String>,
) -> AgentRuntimeToolPlanProtocolError {
AgentRuntimeToolPlanProtocolError::new(kind, detail)
}
struct DuplicateSafeJson;
impl<'de> Deserialize<'de> for DuplicateSafeJson {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
deserializer.deserialize_any(DuplicateSafeJsonVisitor)
}
}
struct DuplicateSafeJsonVisitor;
impl<'de> Visitor<'de> for DuplicateSafeJsonVisitor {
type Value = DuplicateSafeJson;
fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str("不包含重复 object key 的 JSON value")
}
fn visit_bool<E>(self, _value: bool) -> Result<Self::Value, E> {
Ok(DuplicateSafeJson)
}
fn visit_i64<E>(self, _value: i64) -> Result<Self::Value, E> {
Ok(DuplicateSafeJson)
}
fn visit_u64<E>(self, _value: u64) -> Result<Self::Value, E> {
Ok(DuplicateSafeJson)
}
fn visit_f64<E>(self, _value: f64) -> Result<Self::Value, E> {
Ok(DuplicateSafeJson)
}
fn visit_str<E>(self, _value: &str) -> Result<Self::Value, E> {
Ok(DuplicateSafeJson)
}
fn visit_string<E>(self, _value: String) -> Result<Self::Value, E> {
Ok(DuplicateSafeJson)
}
fn visit_none<E>(self) -> Result<Self::Value, E> {
Ok(DuplicateSafeJson)
}
fn visit_unit<E>(self) -> Result<Self::Value, E> {
Ok(DuplicateSafeJson)
}
fn visit_some<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
where
D: serde::Deserializer<'de>,
{
DuplicateSafeJson::deserialize(deserializer)
}
fn visit_seq<A>(self, mut sequence: A) -> Result<Self::Value, A::Error>
where
A: SeqAccess<'de>,
{
while sequence.next_element::<DuplicateSafeJson>()?.is_some() {}
Ok(DuplicateSafeJson)
}
fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
where
A: MapAccess<'de>,
{
let mut keys = HashSet::new();
while let Some(key) = map.next_key::<String>()? {
if !keys.insert(key.clone()) {
return Err(A::Error::custom(format!("重复 JSON object key{key}")));
}
map.next_value::<DuplicateSafeJson>()?;
}
Ok(DuplicateSafeJson)
}
}
pub(crate) fn validate_agent_runtime_protocol_json(
json: &str,
description: &str,
) -> Result<(), AgentRuntimeToolPlanProtocolError> {
let mut deserializer = serde_json::Deserializer::from_str(json);
DuplicateSafeJson::deserialize(&mut deserializer)
.and_then(|_| deserializer.end())
.map_err(|error| {
let kind = match error.classify() {
serde_json::error::Category::Data => {
AgentRuntimeToolPlanProtocolErrorKind::ArgumentsSchema
}
serde_json::error::Category::Io
| serde_json::error::Category::Syntax
| serde_json::error::Category::Eof => {
AgentRuntimeToolPlanProtocolErrorKind::ArgumentsJson
}
};
protocol_error(kind, format!("{description}{error}"))
})
}
fn parse_native_arguments<T: DeserializeOwned>(
arguments: &str,
description: &str,
) -> Result<T, AgentRuntimeToolPlanProtocolError> {
validate_agent_runtime_protocol_json(arguments, description)?;
serde_json::from_str::<T>(arguments).map_err(|error| {
protocol_error(
AgentRuntimeToolPlanProtocolErrorKind::ArgumentsSchema,
format!("{description} schema 无效:{error}"),
)
})
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub(crate) struct NativeAgentRuntimeToolPlan {
pub(crate) plan: AgentRuntimeToolPlan,
pub(crate) call_ids: Vec<String>,
pub(crate) function_names: Vec<String>,
}
#[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields, rename_all = "camelCase")]
struct NativeActionArguments {
reason: String,
input: Value,
}
#[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields, rename_all = "camelCase")]
struct NativeResponseArguments {
response: String,
}
pub(crate) fn native_runtime_function_name(tool: &str) -> Option<String> {
agent_runtime_native_capability_registry()
.ok()?
.get(tool)
.map(|definition| definition.function_name().to_string())
}
fn native_runtime_function_name_for_tool(tool: &str) -> String {
format!(
"{AGENT_RUNTIME_NATIVE_TOOL_PREFIX}{}",
tool.replace('.', "_")
)
}
fn build_agent_runtime_native_capability_registry(
tools: Vec<&'static str>,
) -> Result<CapabilityRegistry<String>, String> {
let definitions = tools
.into_iter()
.map(|tool| {
CapabilityDefinition::try_new(
tool,
native_runtime_function_name_for_tool(tool),
runtime_tool_description(tool),
runtime_tool_input_schema(tool),
tool.to_string(),
)
.map_err(|error| format!("Runtime capability {tool} 无效:{error}"))
})
.collect::<Result<Vec<_>, _>>()?;
CapabilityRegistry::try_new(definitions)
.map_err(|error| format!("Runtime capability registry 无效:{error}"))
}
fn agent_runtime_native_capability_registry() -> Result<&'static CapabilityRegistry<String>, String>
{
// 三个独立开关产生八份目录,使用同一快照选缓存并构建。
static REGISTRIES: [OnceLock<Result<CapabilityRegistry<String>, String>>; 8] =
[const { OnceLock::new() }; 8];
let tools = agent_runtime_native_executable_tools();
let index = usize::from(tools.contains(&crate::builtin_plugins::AGC_COCOS_EDITOR_TOOL_NAME))
| (usize::from(tools.contains(&crate::builtin_plugins::AGC_UNITY_EDITOR_TOOL_NAME)) << 1)
| (usize::from(tools.contains(&crate::builtin_plugins::AGC_GODOT_EDITOR_TOOL_NAME)) << 2);
let cache = &REGISTRIES[index];
cache
.get_or_init(|| build_agent_runtime_native_capability_registry(tools))
.as_ref()
.map_err(Clone::clone)
}
/// 不带身份的全量目录,**只允许测试使用**。
///
/// `"__all_agents__"` 是个不对应任何真实 Agent 的哨兵:走这条路径拿到的是
/// 未按身份收窄的完整函数目录。生产代码必须调用 `_for_agent` 版本并传入真实
/// `agentId`,否则按身份收窄的工具面会被静默绕开。这里用 `#[cfg(test)]` 把「忘记改用 `_for_agent`」
/// 从运行时静默扩权变成编译期错误。
#[cfg(test)]
pub(crate) fn build_agent_runtime_native_function_tools() -> Result<Vec<LlmFunctionTool>, String> {
build_agent_runtime_native_function_tools_for_agent("__all_agents__")
}
/// Build the function catalog for a specific Agent identity.
pub(crate) fn build_agent_runtime_native_function_tools_for_agent(
agent_id: &str,
) -> Result<Vec<LlmFunctionTool>, String> {
let mut functions = vec![plan_update_function_tool(), response_function_tool()];
let mut names = BTreeSet::from([
AGENT_RUNTIME_UPDATE_PLAN_FUNCTION_NAME.to_string(),
AGENT_RUNTIME_RESPOND_FUNCTION_NAME.to_string(),
]);
for definition in agent_runtime_native_capability_registry()?.iter() {
let name = definition.function_name().to_string();
if !names.insert(name.clone()) {
return Err(format!("Runtime 原生函数名重复:{name}"));
}
let description = if let Some(reference) = editor_operation_reference(definition.id()) {
let reference = reference.replace("\r\n", "\n");
if reference.len() > 14 * 1024 {
return Err(format!(
"Runtime 编辑器操作参考超过随包预算:{}",
definition.id()
));
}
reference
} else {
definition.description().to_owned()
};
functions.push(
LlmFunctionTool::new(
name,
description,
action_function_parameters(definition.input_schema().clone()),
)
.with_strict(true),
);
}
Ok(functions)
}
pub(crate) fn build_agent_runtime_native_function_tools_for_project(
root: &std::path::Path,
agent_id: &str,
) -> Result<Vec<LlmFunctionTool>, String> {
let mut tools = build_agent_runtime_native_function_tools_for_agent(agent_id)?;
if !crate::builtin_plugins::godot_editor_agent_tool_available_for_project(root) {
let name = native_runtime_function_name_for_tool("godot.editor.execute");
tools.retain(|tool| tool.name != name);
}
Ok(tools)
}
pub(crate) fn agent_runtime_native_tool_allowed_for_agent(tool: &str) -> bool {
agent_runtime_native_capability_registry()
.ok()
.and_then(|registry| registry.get(tool.trim()))
.is_some()
}
fn validate_native_tool_identity(
runtime_tool: Option<&str>,
) -> Result<(), AgentRuntimeToolPlanProtocolError> {
if let Some(tool) = runtime_tool {
if !agent_runtime_native_tool_allowed_for_agent(tool) {
return Err(protocol_error(
AgentRuntimeToolPlanProtocolErrorKind::UnknownFunction,
format!("Agent 原生工具协议错误:未注册的原生工具 {tool}"),
));
}
}
Ok(())
}
pub(crate) fn parse_agent_runtime_native_tool_calls(
calls: &[LlmToolCall],
) -> Result<NativeAgentRuntimeToolPlan, AgentRuntimeToolPlanProtocolError> {
if calls.is_empty() {
return Err(protocol_error(
AgentRuntimeToolPlanProtocolErrorKind::ResponseShape,
"Agent 原生工具协议错误:function calls 不能为空",
));
}
let mut seen_call_ids = HashSet::new();
let mut plan_update = None;
let mut response = None;
let mut actions = Vec::new();
let mut call_ids = Vec::with_capacity(calls.len());
let mut function_names = Vec::with_capacity(calls.len());
for call in calls {
let call_id = call.id.trim();
if call_id.is_empty() || !seen_call_ids.insert(call_id.to_string()) {
return Err(protocol_error(
AgentRuntimeToolPlanProtocolErrorKind::CallIdentity,
"Agent 原生工具协议错误:call id 必须非空且唯一",
));
}
call_ids.push(call_id.to_string());
function_names.push(call.name.clone());
if call.name == AGENT_RUNTIME_UPDATE_PLAN_FUNCTION_NAME {
if plan_update.is_some() {
return Err(protocol_error(
AgentRuntimeToolPlanProtocolErrorKind::BatchConstraint,
"Agent 原生工具协议错误:一次响应只能更新一次计划",
));
}
plan_update = Some(parse_native_arguments::<AgentRuntimePlanUpdate>(
&call.arguments,
"解析原生计划更新失败",
)?);
continue;
}
if call.name == AGENT_RUNTIME_RESPOND_FUNCTION_NAME {
if response.is_some() {
return Err(protocol_error(
AgentRuntimeToolPlanProtocolErrorKind::BatchConstraint,
"Agent 原生工具协议错误:一次响应只能提交一个最终回复",
));
}
response = Some(
parse_native_arguments::<NativeResponseArguments>(
&call.arguments,
"解析原生最终回复失败",
)?
.response,
);
continue;
}
let runtime_tool = runtime_tool_for_native_function(&call.name);
validate_native_tool_identity(runtime_tool.as_deref())?;
if runtime_tool.is_none() {
return Err(protocol_error(
AgentRuntimeToolPlanProtocolErrorKind::UnknownFunction,
format!("Agent 原生工具协议错误:未知函数 {}", call.name),
));
}
let arguments = parse_native_arguments::<NativeActionArguments>(
&call.arguments,
&format!("解析原生工具 {} 参数失败", call.name),
)?;
if arguments.reason.trim().is_empty() {
return Err(protocol_error(
AgentRuntimeToolPlanProtocolErrorKind::ArgumentsSchema,
format!("Agent 原生工具协议错误:{} reason 不能为空", call.name),
));
}
let mut input = arguments.input;
if runtime_tool.as_deref() == Some("agent.delegate") {
validate_native_agent_delegate_input(&input)?;
}
if runtime_tool.as_deref() == Some("project.patchset") {
input = normalize_native_project_patchset_input(input)?;
}
let tool = runtime_tool.expect("原生函数 binding 已在参数解析前验证");
let action = AgentRuntimeToolAction {
tool,
reason: Some(arguments.reason),
input,
};
actions.push(action);
if actions.len() > AGENT_RUNTIME_BACKGROUND_TOOL_ACTION_LIMIT {
return Err(protocol_error(
AgentRuntimeToolPlanProtocolErrorKind::BatchConstraint,
format!(
"Agent 原生工具协议错误:一次最多调用 {AGENT_RUNTIME_BACKGROUND_TOOL_ACTION_LIMIT} 个动作工具"
),
));
}
}
if response.is_some() && !actions.is_empty() {
return Err(protocol_error(
AgentRuntimeToolPlanProtocolErrorKind::BatchConstraint,
"Agent 原生工具协议错误:最终回复不能与动作工具同时提交",
));
}
if response
.as_deref()
.is_some_and(|value| value.trim().is_empty())
{
return Err(protocol_error(
AgentRuntimeToolPlanProtocolErrorKind::PlanSemantics,
"Agent 原生工具协议错误:最终回复不能为空",
));
}
let response = response.unwrap_or_default();
let thinking_summary = plan_update
.as_ref()
.map(|update| update.explanation.clone())
.or_else(|| actions.first().and_then(|action| action.reason.clone()))
.unwrap_or_else(|| "根据现有观察整理最终回复".to_string());
Ok(NativeAgentRuntimeToolPlan {
plan: AgentRuntimeToolPlan {
thinking_summary,
plan_update,
plan: Vec::new(),
actions,
response,
},
call_ids,
function_names,
})
}
fn validate_native_agent_delegate_input(
input: &Value,
) -> Result<(), AgentRuntimeToolPlanProtocolError> {
const REQUIRED_FIELDS: [&str; 6] = [
"agentId",
"task",
"acceptanceCriteria",
"expectedArtifacts",
"repairOfDelegationId",
"runId",
];
const ALLOWED_FIELDS: [&str; 9] = [
"agentId",
"task",
"acceptanceCriteria",
"expectedArtifacts",
"repairOfDelegationId",
"runId",
"continuationOfDelegationId",
"questionsSha256",
"answersSha256",
];
let object = input.as_object().ok_or_else(|| {
protocol_error(
AgentRuntimeToolPlanProtocolErrorKind::ArgumentsSchema,
"Agent 原生工具协议错误:agent.delegate input 必须是 object",
)
})?;
for field in REQUIRED_FIELDS {
if !object.contains_key(field) {
return Err(protocol_error(
AgentRuntimeToolPlanProtocolErrorKind::ArgumentsSchema,
format!("Agent 原生工具协议错误:agent.delegate 缺少 {field}"),
));
}
}
if object
.keys()
.any(|field| !ALLOWED_FIELDS.contains(&field.as_str()))
{
return Err(protocol_error(
AgentRuntimeToolPlanProtocolErrorKind::ArgumentsSchema,
"Agent 原生工具协议错误:agent.delegate 包含未知字段",
));
}
validate_native_delegate_string(object.get("agentId"), "agentId", 96, false)?;
validate_native_delegate_string(object.get("task"), "task", 2_400, false)?;
// 返工/澄清续跑必须逐字继承原委派的两个数组,Runtime 从 repairOfDelegationId
// 指向的 delivery 直接读得到权威值。两个都传 null 时由 Runtime 补齐;手抄一遍
// 不带来任何信息增益,只制造反复失败的返工委派。初次委派仍然必须自己写。
let repair_hop = object
.get("repairOfDelegationId")
.is_some_and(Value::is_string);
let inherits_contract = repair_hop
&& object.get("acceptanceCriteria").is_some_and(Value::is_null)
&& object.get("expectedArtifacts").is_some_and(Value::is_null);
if !inherits_contract {
validate_native_delegate_string_list(
object.get("acceptanceCriteria"),
"acceptanceCriteria",
1,
8,
240,
)?;
validate_native_delegate_string_list(
object.get("expectedArtifacts"),
"expectedArtifacts",
0,
16,
240,
)?;
}
validate_native_delegate_string(
object.get("repairOfDelegationId"),
"repairOfDelegationId",
160,
true,
)?;
validate_native_delegate_string(object.get("runId"), "runId", 160, true)?;
if object.contains_key("continuationOfDelegationId") {
validate_native_delegate_string(
object.get("continuationOfDelegationId"),
"continuationOfDelegationId",
160,
true,
)?;
}
if object.contains_key("questionsSha256") {
validate_native_delegate_string(
object.get("questionsSha256"),
"questionsSha256",
64,
true,
)?;
}
if object.contains_key("answersSha256") {
validate_native_delegate_string(object.get("answersSha256"), "answersSha256", 64, true)?;
}
if object
.get("repairOfDelegationId")
.is_some_and(Value::is_string)
&& !object.get("runId").is_some_and(Value::is_null)
{
return Err(protocol_error(
AgentRuntimeToolPlanProtocolErrorKind::ArgumentsSchema,
"Agent 原生工具协议错误:agent.delegate 返工委派时 runId 必须为 JSON null",
));
}
// 澄清 continuation 的锚点是 continuationOfDelegationId;两个指纹可以整体省略,
// 由 Runtime 从原 delivery 取权威值补齐。省略是为了不让 Supervisor 手抄 128 个
// 十六进制字符——抄错会打到硬失败,而抄对也不带来任何 Runtime 不知道的信息。
// 三个字段一律按“JSON null 等同于缺省”处理,与本函数其余可空字段一致。
let continuation_anchor = object
.get("continuationOfDelegationId")
.is_some_and(Value::is_string);
let digest_strings = ["questionsSha256", "answersSha256"]
.iter()
.filter(|field| object.get(**field).is_some_and(Value::is_string))
.count();
if digest_strings == 1 {
return Err(protocol_error(
AgentRuntimeToolPlanProtocolErrorKind::ArgumentsSchema,
"Agent 原生工具协议错误:agent.delegate 澄清 continuation 指纹必须成对提供,或整体省略交给 Runtime 补齐",
));
}
if digest_strings == 2 && !continuation_anchor {
return Err(protocol_error(
AgentRuntimeToolPlanProtocolErrorKind::ArgumentsSchema,
"Agent 原生工具协议错误:agent.delegate 澄清 continuation 指纹必须同时提供 continuationOfDelegationId",
));
}
for field in ["questionsSha256", "answersSha256"] {
if object.get(field).is_some_and(Value::is_string)
&& object
.get(field)
.and_then(Value::as_str)
.is_none_or(|value| {
value.len() != 64 || !value.bytes().all(|byte| byte.is_ascii_hexdigit())
})
{
return Err(protocol_error(
AgentRuntimeToolPlanProtocolErrorKind::ArgumentsSchema,
format!(
"Agent 原生工具协议错误:agent.delegate {field} 必须是 64 位十六进制 SHA-256"
),
));
}
}
Ok(())
}
fn normalize_native_project_patchset_input(
input: Value,
) -> Result<Value, AgentRuntimeToolPlanProtocolError> {
const CHANGE_FIELDS: [&str; 7] = [
"operation",
"path",
"content",
"expectedSha256",
"oldText",
"newText",
"expectedReplacements",
];
let object = input.as_object().ok_or_else(|| {
protocol_error(
AgentRuntimeToolPlanProtocolErrorKind::ArgumentsSchema,
"Agent 原生工具协议错误:project.patchset input 必须是 object",
)
})?;
if object.len() != 1 || !object.contains_key("changes") {
return Err(protocol_error(
AgentRuntimeToolPlanProtocolErrorKind::ArgumentsSchema,
"Agent 原生工具协议错误:project.patchset input 字段无效",
));
}
let changes = object
.get("changes")
.and_then(Value::as_array)
.ok_or_else(|| {
protocol_error(
AgentRuntimeToolPlanProtocolErrorKind::ArgumentsSchema,
"Agent 原生工具协议错误:project.patchset changes 必须是数组",
)
})?;
let mut normalized = Vec::with_capacity(changes.len());
for (index, change) in changes.iter().enumerate() {
let change = change.as_object().ok_or_else(|| {
protocol_error(
AgentRuntimeToolPlanProtocolErrorKind::ArgumentsSchema,
format!(
"Agent 原生工具协议错误:project.patchset changes[{}] 必须是 object",
index + 1
),
)
})?;
if change.len() != CHANGE_FIELDS.len()
|| CHANGE_FIELDS
.iter()
.any(|field| !change.contains_key(*field))
|| change
.keys()
.any(|field| !CHANGE_FIELDS.contains(&field.as_str()))
{
return Err(protocol_error(
AgentRuntimeToolPlanProtocolErrorKind::ArgumentsSchema,
format!(
"Agent 原生工具协议错误:project.patchset changes[{}] 字段无效",
index + 1
),
));
}
let required_string = |field: &str| {
change.get(field).and_then(Value::as_str).ok_or_else(|| {
protocol_error(
AgentRuntimeToolPlanProtocolErrorKind::ArgumentsSchema,
format!(
"Agent 原生工具协议错误:project.patchset changes[{}].{field} 必须是字符串",
index + 1
),
)
})
};
let require_null = |fields: &[&str]| {
if let Some(field) = fields
.iter()
.find(|field| !change.get(**field).is_some_and(Value::is_null))
{
return Err(protocol_error(
AgentRuntimeToolPlanProtocolErrorKind::ArgumentsSchema,
format!(
"Agent 原生工具协议错误:project.patchset changes[{}].{field} 必须为 null",
index + 1
),
));
}
Ok(())
};
let operation = required_string("operation")?;
let path = required_string("path")?;
normalized.push(match operation {
"create" => {
require_null(&[
"expectedSha256",
"oldText",
"newText",
"expectedReplacements",
])?;
json!({
"operation": operation,
"path": path,
"content": required_string("content")?,
})
}
"update" => {
require_null(&["content"])?;
let expected_replacements = change
.get("expectedReplacements")
.and_then(Value::as_u64)
.ok_or_else(|| {
protocol_error(
AgentRuntimeToolPlanProtocolErrorKind::ArgumentsSchema,
format!(
"Agent 原生工具协议错误:project.patchset changes[{}].expectedReplacements 必须是整数",
index + 1
),
)
})?;
json!({
"operation": operation,
"path": path,
"expectedSha256": required_string("expectedSha256")?,
"oldText": required_string("oldText")?,
"newText": required_string("newText")?,
"expectedReplacements": expected_replacements,
})
}
"delete" => {
require_null(&["content", "oldText", "newText", "expectedReplacements"])?;
json!({
"operation": operation,
"path": path,
"expectedSha256": required_string("expectedSha256")?,
})
}
_ => {
return Err(protocol_error(
AgentRuntimeToolPlanProtocolErrorKind::ArgumentsSchema,
format!(
"Agent 原生工具协议错误:project.patchset changes[{}].operation 无效",
index + 1
),
));
}
});
}
Ok(json!({ "changes": normalized }))
}
fn validate_native_delegate_string(
value: Option<&Value>,
field: &str,
max_chars: usize,
nullable: bool,
) -> Result<(), AgentRuntimeToolPlanProtocolError> {
if nullable && value.is_some_and(Value::is_null) {
return Ok(());
}
let value = value.and_then(Value::as_str).ok_or_else(|| {
protocol_error(
AgentRuntimeToolPlanProtocolErrorKind::ArgumentsSchema,
format!("Agent 原生工具协议错误:agent.delegate {field} 类型无效"),
)
})?;
let chars = value.chars().count();
if value.trim().is_empty() || chars > max_chars {
return Err(protocol_error(
AgentRuntimeToolPlanProtocolErrorKind::ArgumentsSchema,
format!("Agent 原生工具协议错误:agent.delegate {field} 长度无效"),
));
}
Ok(())
}
fn validate_native_delegate_string_list(
value: Option<&Value>,
field: &str,
min_items: usize,
max_items: usize,
max_chars: usize,
) -> Result<(), AgentRuntimeToolPlanProtocolError> {
let values = value.and_then(Value::as_array).ok_or_else(|| {
protocol_error(
AgentRuntimeToolPlanProtocolErrorKind::ArgumentsSchema,
format!("Agent 原生工具协议错误:agent.delegate {field} 类型无效"),
)
})?;
if values.len() < min_items || values.len() > max_items {
return Err(protocol_error(
AgentRuntimeToolPlanProtocolErrorKind::ArgumentsSchema,
format!("Agent 原生工具协议错误:agent.delegate {field} 数量无效"),
));
}
for value in values {
validate_native_delegate_string(Some(value), field, max_chars, false)?;
}
Ok(())
}
fn runtime_tool_for_native_function(name: &str) -> Option<String> {
agent_runtime_native_capability_registry()
.ok()?
.get_by_function_name(name)
.map(|definition| definition.dispatch().clone())
}
fn plan_update_function_tool() -> LlmFunctionTool {
LlmFunctionTool::new(
AGENT_RUNTIME_UPDATE_PLAN_FUNCTION_NAME,
prompt_text!("nativeTools.update_agent_plan.description"),
plan_update_schema(),
)
.with_strict(true)
}
fn response_function_tool() -> LlmFunctionTool {
LlmFunctionTool::new(
AGENT_RUNTIME_RESPOND_FUNCTION_NAME,
prompt_text!("nativeTools.respond_to_user.description"),
json!({
"type": "object",
"required": ["response"],
"additionalProperties": false,
"properties": {
"response": { "type": "string", "minLength": 1 }
}
}),
)
.with_strict(true)
}
fn plan_update_schema() -> Value {
json!({
"type": "object",
"required": ["explanation", "steps"],
"additionalProperties": false,
"properties": {
"explanation": { "type": "string", "minLength": 1 },
"steps": {
"type": "array",
"minItems": 1,
"maxItems": AGENT_RUNTIME_PLAN_STEP_LIMIT,
"items": {
"type": "object",
"required": ["step", "status"],
"additionalProperties": false,
"properties": {
"step": { "type": "string", "minLength": 1 },
"status": {
"type": "string",
"enum": ["pending", "in_progress", "completed"]
}
}
}
}
}
})
}
fn rebase_action_input_schema_refs_in_scope(value: &mut Value, has_local_resource_id: bool) {
let Value::Object(object) = value else {
return;
};
// `$id` 会建立独立 schema resource;其内部 fragment 应继续相对该 resource
// 解析,不能按外层 function parameters 根重定位。
let has_local_resource_id = has_local_resource_id || object.contains_key("$id");
let reference = object
.get("$ref")
.and_then(Value::as_str)
.map(ToString::to_string);
if let Some(reference) = reference {
// 只有空 fragment 和 JSON Pointer fragment 相对当前 document 根。
// `#Mode` 是命名 anchor,外部 URI 也有自己的解析范围,必须保持原样。
if !has_local_resource_id && (reference == "#" || reference.starts_with("#/")) {
let rebased = if reference == "#" {
"#/properties/input".to_string()
} else {
format!("#/properties/input{}", &reference[1..])
};
object.insert("$ref".to_string(), Value::String(rebased));
}
}
// 只进入 JSON Schema 明确定义为 subschema 的位置。default、const、examples、
// enum 等关键词承载普通 JSON 数据,其中即使出现 `$ref` 也不能改写。
for keyword in [
"additionalProperties",
"unevaluatedProperties",
"propertyNames",
"additionalItems",
"unevaluatedItems",
"contains",
"not",
"if",
"then",
"else",
"contentSchema",
] {
if let Some(child) = object.get_mut(keyword) {
rebase_action_input_schema_refs_in_scope(child, has_local_resource_id);
}
}
for keyword in ["allOf", "anyOf", "oneOf", "prefixItems"] {
if let Some(Value::Array(children)) = object.get_mut(keyword) {
for child in children {
rebase_action_input_schema_refs_in_scope(child, has_local_resource_id);
}
}
}
// draft-07 的 tuple validation 允许 items 为 schema 数组;新版本则为单 schema。
if let Some(items) = object.get_mut("items") {
match items {
Value::Array(children) => {
for child in children {
rebase_action_input_schema_refs_in_scope(child, has_local_resource_id);
}
}
child => rebase_action_input_schema_refs_in_scope(child, has_local_resource_id),
}
}
for keyword in [
"$defs",
"definitions",
"properties",
"patternProperties",
"dependentSchemas",
] {
if let Some(Value::Object(children)) = object.get_mut(keyword) {
for child in children.values_mut() {
rebase_action_input_schema_refs_in_scope(child, has_local_resource_id);
}
}
}
// draft-07 dependencies 的 value 可能是 subschema,也可能是属性名数组。
if let Some(Value::Object(dependencies)) = object.get_mut("dependencies") {
for dependency in dependencies.values_mut().filter(|value| value.is_object()) {
rebase_action_input_schema_refs_in_scope(dependency, has_local_resource_id);
}
}
}
fn rebase_action_input_schema_refs(value: &mut Value) {
rebase_action_input_schema_refs_in_scope(value, false);
}
fn action_function_parameters(mut input_schema: Value) -> Value {
// Action input schema 会被包进 action.input。局部 JSON Pointer 仍从整个
// function parameters 根解析,因此必须同步重定位;否则 #/$defs/... 会悬空。
rebase_action_input_schema_refs(&mut input_schema);
json!({
"type": "object",
"required": ["reason", "input"],
"additionalProperties": false,
"properties": {
"reason": { "type": "string", "minLength": 1 },
"input": input_schema
}
})
}
fn empty_input_schema() -> Value {
json!({ "type": "object", "required": [], "additionalProperties": false, "properties": {} })
}
fn string_array_schema(max_items: usize) -> Value {
json!({
"type": "array",
"maxItems": max_items,
"items": { "type": "string" }
})
}
fn editor_operation_reference(tool: &str) -> Option<&'static str> {
match tool {
"unity.editor.execute" => Some(include_str!("../resources/agc-skills/agc-unity-editor/references/【操作指南】Unity编辑器常用操作-2026-09-20.md")),
"godot.editor.execute" => Some(include_str!("../resources/agc-skills/agc-godot-editor/references/【操作指南】Godot编辑器常用操作-2026-09-20.md")),
_ => None,
}
}
fn runtime_tool_description(tool: &str) -> &'static str {
match tool {
"user.input_request" => prompt_text!("nativeTools.user.input_request.description"),
"memory.read" => prompt_text!("nativeTools.memory.read.description"),
"memory.write" => prompt_text!("nativeTools.memory.write.description"),
"conversation.read" => prompt_text!("nativeTools.conversation.read.description"),
"asset.list" => {
prompt_text!("nativeTools.asset.list.description")
}
"asset.library.list" => {
prompt_text!("nativeTools.asset.library.list.description")
}
"canvas.asset_import" => {
prompt_text!("nativeTools.canvas.asset_import.description")
}
"project.index" => prompt_text!("nativeTools.project.index.description"),
"project.search" => prompt_text!("nativeTools.project.search.description"),
"project.verify" => prompt_text!("nativeTools.project.verify.description"),
"project.bootstrap" => prompt_text!("nativeTools.project.bootstrap.description"),
"project.checkpoint" => prompt_text!("nativeTools.project.checkpoint.description"),
"project.restore" => prompt_text!("nativeTools.project.restore.description"),
"project.diff" => prompt_text!("nativeTools.project.diff.description"),
"git.inspect" => prompt_text!("nativeTools.git.inspect.description"),
"project.git_commit" => prompt_text!("nativeTools.project.git_commit.description"),
"project.patchset" => prompt_text!("nativeTools.project.patchset.description"),
"file.list" => prompt_text!("nativeTools.file.list.description"),
"file.read" => prompt_text!("nativeTools.file.read.description"),
"file.write" => prompt_text!("nativeTools.file.write.description"),
"file.patch" => prompt_text!("nativeTools.file.patch.description"),
"file.delete" => prompt_text!("nativeTools.file.delete.description"),
"task.list" => prompt_text!("nativeTools.task.list.description"),
"task.create" => prompt_text!("nativeTools.task.create.description"),
"task.update" => prompt_text!("nativeTools.task.update.description"),
"command.exec" => prompt_text!("nativeTools.command.exec.description"),
"command.output_read" => prompt_text!("nativeTools.command.output_read.description"),
"command.start" => prompt_text!("nativeTools.command.start.description"),
"command.poll" => prompt_text!("nativeTools.command.poll.description"),
"command.stdin" => prompt_text!("nativeTools.command.stdin.description"),
"command.terminate" => prompt_text!("nativeTools.command.terminate.description"),
"command.run_limited" => prompt_text!("nativeTools.command.run_limited.description"),
"preview.start" => prompt_text!("nativeTools.preview.start.description"),
"preview.validate" => prompt_text!("nativeTools.preview.validate.description"),
"image.inspect" => prompt_text!("nativeTools.image.inspect.description"),
"canvas.asset_generate" => {
prompt_text!("nativeTools.canvas.asset_generate.description")
}
"ui.workflow.run" => {
prompt_text!("nativeTools.ui.workflow.run.description")
}
"cocos.editor.execute" => {
prompt_text!("nativeTools.cocos.editor.execute.description")
}
"unity.editor.execute" => prompt_text!("nativeTools.unity.editor.execute.description"),
"godot.editor.execute" => prompt_text!("nativeTools.godot.editor.execute.description"),
"blackboard.write" => prompt_text!("nativeTools.blackboard.write.description"),
"agent.message" => prompt_text!("nativeTools.agent.message.description"),
"agent.delegate" => {
prompt_text!("nativeTools.agent.delegate.description")
}
"agent.spawn_isolated" => prompt_text!("nativeTools.agent.spawn_isolated.description"),
"agent.goal_contract" => {
prompt_text!("nativeTools.agent.goal_contract.description")
}
"agent.acceptance_update" => {
prompt_text!("nativeTools.agent.acceptance_update.description")
}
"agent.schedule_ready" => prompt_text!("nativeTools.agent.schedule_ready.description"),
"agent.action_history" => prompt_text!("nativeTools.agent.action_history.description"),
"agent.run_status" => prompt_text!("nativeTools.agent.run_status.description"),
_ => prompt_text!("nativeTools.fallback.description"),
}
}
fn runtime_tool_input_schema(tool: &str) -> Value {
match tool {
"user.input_request" => json!({
"type": "object",
"required": ["questions"],
"additionalProperties": false,
"properties": {
"questions": {
"type": "array", "minItems": 1, "maxItems": 3,
"items": {
"type": "object",
"required": ["id", "header", "question", "options"],
"additionalProperties": false,
"properties": {
"id": { "type": "string", "minLength": 1, "maxLength": 64 },
"header": { "type": "string", "minLength": 1, "maxLength": 12 },
"question": { "type": "string", "minLength": 1, "maxLength": 400 },
"options": {
"type": "array", "minItems": 2, "maxItems": 3,
"items": {
"type": "object",
"required": ["label", "description"],
"additionalProperties": false,
"properties": {
"label": { "type": "string", "minLength": 1, "maxLength": 60 },
"description": { "type": "string", "minLength": 1, "maxLength": 240 }
}
}
}
}
}
}
}
}),
"memory.read" => json!({
"type": "object", "required": ["scope"], "additionalProperties": false,
"properties": { "scope": { "type": "string", "enum": ["session", "project", "blackboard", "agent"] } }
}),
"memory.write" => json!({
"type": "object", "required": ["scope", "title", "content", "mode"], "additionalProperties": false,
"properties": {
"scope": { "type": "string", "enum": ["agent", "project", "session", "blackboard"] },
"title": { "type": "string" },
"content": { "type": "string", "minLength": 1 },
"mode": { "type": "string", "enum": ["append", "overwrite"] }
}
}),
"conversation.read" | "asset.list" | "project.index" | "project.checkpoint"
| "task.list" | "preview.start" => empty_input_schema(),
"asset.library.list" => json!({
"type": "object",
// OpenAI strict function schemas require every declared property to
// appear in `required`. Optional values are represented as nullable
// fields and the runtime treats `null` as omitted.
"required": ["folderId", "query", "offset", "limit"],
"additionalProperties": false,
"properties": {
"folderId": { "type": ["string", "null"], "maxLength": 512 },
"query": { "type": ["string", "null"], "maxLength": 120 },
"offset": { "type": ["integer", "null"], "minimum": 0, "maximum": 500 },
"limit": { "type": ["integer", "null"], "minimum": 1, "maximum": 100 }
}
}),
"canvas.asset_import" => json!({
"type": "object",
// Both arrays are nullable so a strict-schema caller can select one
// source kind while still sending the complete object shape.
"required": ["assetIds", "localPaths"],
"additionalProperties": false,
"properties": {
"assetIds": {
"type": ["array", "null"],
"maxItems": 100,
"items": { "type": "string", "minLength": 1, "maxLength": 512 }
},
"localPaths": {
"type": ["array", "null"],
"maxItems": 100,
"items": { "type": "string", "minLength": 1, "maxLength": 512 }
}
}
}),
"project.search" => json!({
"type": "object", "required": ["query", "path", "maxResults", "caseSensitive"], "additionalProperties": false,
"properties": {
"query": { "type": "string", "minLength": 1, "maxLength": 256 },
"path": { "type": "string" },
"maxResults": { "type": "integer", "minimum": 1, "maximum": 50 },
"caseSensitive": { "type": "boolean" }
}
}),
"project.verify" => json!({
"type": "object", "required": ["script", "expectedCommand", "timeoutSeconds", "cwd"], "additionalProperties": false,
"properties": {
"script": { "type": "string", "minLength": 1 },
"expectedCommand": { "type": "string", "minLength": 1 },
"timeoutSeconds": { "type": "integer", "minimum": 1, "maximum": 600 },
"cwd": { "type": ["string", "null"], "pattern": "^[A-Za-z0-9._/-]+$" }
}
}),
"project.bootstrap" => json!({
"type": "object", "required": ["cwd", "timeoutSeconds"], "additionalProperties": false,
"properties": {
"cwd": { "type": "string", "const": "game" },
"timeoutSeconds": { "type": "integer", "minimum": 1, "maximum": 600 }
}
}),
"project.restore" => one_string_input_schema("checkpointId"),
"project.diff" => json!({
"type": "object", "required": ["checkpointId", "includeContent", "maxFiles", "maxChars"], "additionalProperties": false,
"properties": {
"checkpointId": { "type": "string", "minLength": 1 },
"includeContent": { "type": "boolean" },
"maxFiles": { "type": "integer", "minimum": 1, "maximum": 50 },
"maxChars": { "type": "integer", "minimum": 1, "maximum": 24000 }
}
}),
"git.inspect" => json!({
"type": "object", "required": ["includeDiff", "maxFiles", "maxChars"], "additionalProperties": false,
"properties": {
"includeDiff": { "type": "boolean" },
"maxFiles": { "type": "integer", "minimum": 1, "maximum": 50 },
"maxChars": { "type": "integer", "minimum": 1, "maximum": 24000 }
}
}),
"project.git_commit" => json!({
"type": "object", "required": ["message", "paths", "expectedHead", "expectedSnapshotFingerprint"], "additionalProperties": false,
"properties": {
"message": { "type": "string", "minLength": 1 },
"paths": { "type": "array", "minItems": 1, "maxItems": 12, "items": { "type": "string", "minLength": 1 } },
"expectedHead": { "type": "string", "minLength": 1 },
"expectedSnapshotFingerprint": { "type": "string", "minLength": 1 }
}
}),
"project.patchset" => project_patchset_input_schema(),
"file.list" => one_string_input_schema("path"),
"file.read" => json!({
"type": "object", "required": ["path", "startLine", "maxLines"], "additionalProperties": false,
"properties": {
"path": { "type": "string", "minLength": 1 },
"startLine": { "type": "integer", "minimum": 1 },
"maxLines": { "type": "integer", "minimum": 1, "maximum": 240 }
}
}),
"file.write" => two_string_input_schema("path", "content"),
"file.patch" => json!({
"type": "object", "required": ["path", "oldText", "newText", "expectedReplacements"], "additionalProperties": false,
"properties": {
"path": { "type": "string", "minLength": 1 },
"oldText": { "type": "string", "minLength": 1 },
"newText": { "type": "string" },
"expectedReplacements": { "type": "integer", "minimum": 1 }
}
}),
"file.delete" => one_string_input_schema("path"),
"task.create" => json!({
"type": "object",
"required": ["taskId", "title", "group", "role", "dependencies", "artifacts", "acceptanceCriteria", "status"],
"additionalProperties": false,
"properties": {
"taskId": { "type": ["string", "null"] },
"title": { "type": "string", "minLength": 1 },
"group": { "type": "string", "enum": ["design", "art", "code", "balance", "audio", "publishing"] },
"role": { "type": "string", "minLength": 1 },
"dependencies": string_array_schema(32),
"artifacts": string_array_schema(32),
"acceptanceCriteria": string_array_schema(32),
"status": { "type": "string", "enum": ["pending", "running", "waiting-for-confirmation", "completed", "failed"] }
}
}),
"task.update" => json!({
"type": "object", "required": ["taskId", "status"], "additionalProperties": false,
"properties": {
"taskId": { "type": "string", "minLength": 1 },
"status": { "type": "string", "enum": ["pending", "running", "waiting-for-confirmation", "completed", "failed"] }
}
}),
"command.exec" | "command.start" => command_start_input_schema(),
"cocos.editor.execute" | "unity.editor.execute" | "godot.editor.execute" => json!({
"type": "object",
"required": ["code"],
"additionalProperties": false,
"properties": {
"code": { "type": "string", "minLength": 1, "maxLength": 131072 }
}
}),
"command.output_read" => json!({
"type": "object", "required": ["actionId", "startLine", "maxLines"], "additionalProperties": false,
"properties": {
"actionId": { "type": "string", "minLength": 1 },
"startLine": { "type": "integer", "minimum": 1 },
"maxLines": { "type": "integer", "minimum": 1, "maximum": 160 }
}
}),
"command.poll" => json!({
"type": "object", "required": ["processId", "cursor", "maxChars", "waitMs"], "additionalProperties": false,
"properties": {
"processId": { "type": "string", "minLength": 1 },
"cursor": { "type": ["string", "null"] },
"maxChars": { "type": "integer", "minimum": 1, "maximum": 32000 },
"waitMs": { "type": "integer", "minimum": 0, "maximum": 30000 }
}
}),
"command.stdin" => json!({
"type": "object", "required": ["processId", "data", "appendNewline", "eof"], "additionalProperties": false,
"properties": {
"processId": { "type": "string", "minLength": 1 },
"data": { "type": "string" },
"appendNewline": { "type": "boolean" },
"eof": { "type": "boolean" }
}
}),
"command.terminate" => json!({
"type": "object", "required": ["processId", "cursor"], "additionalProperties": false,
"properties": {
"processId": { "type": "string", "minLength": 1 },
"cursor": { "type": ["string", "null"] }
}
}),
"command.run_limited" => json!({
"type": "object", "required": ["commandId"], "additionalProperties": false,
"properties": { "commandId": { "type": "string", "enum": ["game.static_smoke"] } }
}),
"preview.validate" => json!({
"type": "object", "required": ["viewports", "expectedText", "settleMs", "failOnConsoleError", "playtestScenario"], "additionalProperties": false,
"properties": {
"viewports": { "type": "array", "minItems": 1, "maxItems": 2, "items": { "type": "string", "enum": ["desktop", "mobile"] } },
"expectedText": string_array_schema(16),
"settleMs": { "type": "integer", "minimum": 0, "maximum": 10000 },
"failOnConsoleError": { "type": "boolean" },
"playtestScenario": { "type": ["string", "null"], "enum": ["generic-v1", "tetris-v1", "lane-defense-v1", null] }
}
}),
"image.inspect" => json!({
"type": "object", "required": ["paths", "question"], "additionalProperties": false,
"properties": {
"paths": { "type": "array", "minItems": 1, "maxItems": 2, "items": { "type": "string", "minLength": 1 } },
"question": { "type": ["string", "null"], "maxLength": 1000 }
}
}),
"canvas.asset_generate" => {
let mut asset_kinds = AGENT_RUNTIME_CANVAS_ASSET_KINDS
.iter()
.map(|kind| Value::String(kind.as_str().to_string()))
.collect::<Vec<_>>();
asset_kinds.push(Value::Null);
json!({
"type": "object",
"required": ["prompt", "outputPath", "aspectRatio", "imageSize", "assetKind", "assetLabel", "replaceExisting", "sliceMode", "gridX", "gridY", "sliceCount"],
"additionalProperties": false,
"properties": {
"prompt": { "type": "string", "minLength": 1, "maxLength": 4000 },
"outputPath": { "type": ["string", "null"], "maxLength": 240 },
"aspectRatio": { "type": ["string", "null"], "enum": ["1:1", "2:3", "3:2", "9:16", "16:9", null] },
"imageSize": { "type": ["string", "null"], "enum": ["0.5K", "1K", "2K", null] },
"assetKind": { "type": ["string", "null"], "enum": asset_kinds },
"assetLabel": { "type": ["string", "null"], "maxLength": 80 },
"replaceExisting": { "type": "boolean" },
"sliceMode": { "type": ["string", "null"], "enum": ["connected-components", "grid", null], "description": prompt_text!("nativeTools.canvas.asset_generate.parameters.sliceMode") },
"gridX": { "type": ["integer", "null"], "minimum": 1, "maximum": 32, "description": prompt_text!("nativeTools.canvas.asset_generate.parameters.gridX") },
"gridY": { "type": ["integer", "null"], "minimum": 1, "maximum": 32, "description": prompt_text!("nativeTools.canvas.asset_generate.parameters.gridY") },
"sliceCount": { "type": ["integer", "null"], "minimum": 1, "maximum": 256, "description": prompt_text!("nativeTools.canvas.asset_generate.parameters.sliceCount") }
}
})
}
"ui.workflow.run" => json!({
"type": "object",
"required": ["operation", "sourceAssetId", "pages"],
"additionalProperties": false,
"properties": {
"operation": { "type": "string", "enum": ["discover", "prepare", "recognize", "status", "finalize"] },
"sourceAssetId": { "type": "string", "minLength": 1, "maxLength": 160 },
"pages": {
"type": "array",
"maxItems": 32,
"items": {
"type": "object",
"required": ["pageId", "title", "description", "designAssetId", "spriteAssetIds", "fontAssetIds", "applicationPath"],
"additionalProperties": false,
"properties": {
"pageId": { "type": "string", "minLength": 1, "maxLength": 80, "pattern": "^[A-Za-z0-9._-]+$" },
"title": { "type": "string", "minLength": 1, "maxLength": 120 },
"description": { "type": "string", "maxLength": 400 },
"designAssetId": { "type": "string", "minLength": 1, "maxLength": 160 },
"spriteAssetIds": { "type": "array", "maxItems": 32, "items": { "type": "string", "minLength": 1, "maxLength": 160 } },
"fontAssetIds": { "type": "array", "maxItems": 16, "items": { "type": "string", "minLength": 1, "maxLength": 160 } },
"applicationPath": { "type": ["string", "null"], "maxLength": 240 }
}
}
}
}
}),
"blackboard.write" => two_string_input_schema("title", "content"),
"agent.message" => two_string_input_schema("agentId", "content"),
"agent.delegate" => json!({
"type": "object", "required": ["agentId", "task", "acceptanceCriteria", "expectedArtifacts", "repairOfDelegationId", "runId", "continuationOfDelegationId", "questionsSha256", "answersSha256"], "additionalProperties": false,
"properties": {
"agentId": { "type": "string", "minLength": 1 },
"task": { "type": "string", "minLength": 1, "maxLength": 2400 },
"acceptanceCriteria": { "type": ["array", "null"], "minItems": 1, "maxItems": 8, "items": { "type": "string", "minLength": 1, "maxLength": 240 }, "description": prompt_text!("nativeTools.agent.delegate.parameters.acceptanceCriteria") },
"expectedArtifacts": { "type": ["array", "null"], "maxItems": 16, "items": { "type": "string", "minLength": 1, "maxLength": 240 }, "description": prompt_text!("nativeTools.agent.delegate.parameters.expectedArtifacts") },
"repairOfDelegationId": { "type": ["string", "null"] },
"runId": { "type": ["string", "null"] },
"continuationOfDelegationId": { "type": ["string", "null"] },
"questionsSha256": { "type": ["string", "null"] },
"answersSha256": { "type": ["string", "null"] }
}
}),
"agent.spawn_isolated" => json!({
"type": "object", "required": ["children", "joinMode"], "additionalProperties": false,
"properties": {
"children": {
"type": "array", "minItems": 1, "maxItems": 3,
"items": {
"type": "object",
"required": ["templateAgentId", "task", "acceptanceCriteria", "expectedArtifacts", "writeScopes"],
"additionalProperties": false,
"properties": {
"templateAgentId": { "type": "string", "minLength": 1 },
"task": { "type": "string", "minLength": 1 },
"acceptanceCriteria": string_array_schema(16),
"expectedArtifacts": string_array_schema(32),
"writeScopes": string_array_schema(16)
}
}
},
"joinMode": { "type": "string", "enum": ["all"] }
}
}),
"agent.goal_contract" => json!({
"type": "object",
"required": ["outcome", "nonNegotiables", "preferences", "forbiddenAssumptions", "openQuestions", "acceptanceNodes"],
"additionalProperties": false,
"properties": {
"outcome": { "type": "string", "minLength": 1, "maxLength": 4000 },
"nonNegotiables": string_array_schema(16),
"preferences": string_array_schema(16),
"forbiddenAssumptions": string_array_schema(16),
"openQuestions": string_array_schema(16),
"acceptanceNodes": {
"type": "array", "minItems": 1, "maxItems": 32,
"items": {
"type": "object",
"required": ["criterionId", "criterion", "required", "requiredEvidence", "dependsOn"],
"additionalProperties": false,
"properties": {
"criterionId": { "type": "string", "minLength": 1, "maxLength": 80 },
"criterion": { "type": "string", "minLength": 1, "maxLength": 800 },
"required": { "type": "boolean" },
"requiredEvidence": {
"type": "array", "maxItems": 12,
"items": { "type": "string", "pattern": "^(tool:)?[a-z][a-z0-9._-]{0,79}$" }
},
"dependsOn": string_array_schema(16)
}
}
}
}
}),
"agent.acceptance_update" => json!({
"type": "object",
"required": ["contractFingerprint", "evaluations"],
"additionalProperties": false,
"properties": {
"contractFingerprint": { "type": "string", "minLength": 64, "maxLength": 64 },
"evaluations": {
"type": "array", "minItems": 1, "maxItems": 32,
"items": {
"type": "object",
"required": ["criterionId", "status", "evidence", "summary"],
"additionalProperties": false,
"properties": {
"criterionId": { "type": "string", "minLength": 1, "maxLength": 80 },
"status": { "type": "string", "enum": ["passed", "failed", "not-observed"] },
"evidence": {
"type": "array", "maxItems": 16,
"items": {
"type": "object",
"required": ["agentId", "runId", "actionId"],
"additionalProperties": false,
"properties": {
"agentId": { "type": "string", "minLength": 1, "maxLength": 96 },
"runId": { "type": "string", "minLength": 1, "maxLength": 160 },
"actionId": { "type": "string", "minLength": 1, "maxLength": 96 }
}
}
},
"summary": { "type": "string", "minLength": 1, "maxLength": 1000 }
}
}
}
}
}),
"agent.schedule_ready" => json!({
"type": "object", "required": ["limit"], "additionalProperties": false,
"properties": { "limit": { "type": "integer", "minimum": 1, "maximum": 16 } }
}),
"agent.action_history" => json!({
"type": "object", "required": ["runId", "actionId", "tool", "status", "limit"], "additionalProperties": false,
"properties": {
"runId": { "type": ["string", "null"] },
"actionId": { "type": ["string", "null"] },
"tool": { "type": ["string", "null"] },
"status": { "type": ["string", "null"] },
"limit": { "type": "integer", "minimum": 1, "maximum": 10 }
}
}),
"agent.run_status" => json!({
"type": "object", "required": ["agentId", "scope", "delegationId"], "additionalProperties": false,
"properties": {
"agentId": { "type": ["string", "null"] },
"scope": { "type": "string", "enum": ["self", "all"] },
"delegationId": { "type": ["string", "null"] }
}
}),
_ => empty_input_schema(),
}
}
fn one_string_input_schema(field: &str) -> Value {
json!({
"type": "object",
"required": [field],
"additionalProperties": false,
"properties": { (field): { "type": "string" } }
})
}
fn two_string_input_schema(first: &str, second: &str) -> Value {
json!({
"type": "object",
"required": [first, second],
"additionalProperties": false,
"properties": {
(first): { "type": "string" },
(second): { "type": "string" }
}
})
}
fn command_start_input_schema() -> Value {
json!({
"type": "object", "required": ["program", "args", "cwd", "timeoutSeconds"], "additionalProperties": false,
"properties": {
"program": { "type": "string", "minLength": 1 },
"args": { "type": "array", "items": { "type": "string" } },
"cwd": { "type": "string" },
"timeoutSeconds": { "type": "integer", "minimum": 1, "maximum": 3600 }
}
})
}
fn project_patchset_input_schema() -> Value {
json!({
"type": "object",
"required": ["changes"],
"additionalProperties": false,
"properties": {
"changes": {
"type": "array", "minItems": 1, "maxItems": 12,
"items": {
"type": "object",
"required": ["operation", "path", "content", "expectedSha256", "oldText", "newText", "expectedReplacements"],
"additionalProperties": false,
"properties": {
"operation": { "type": "string", "enum": ["create", "update", "delete"] },
"path": { "type": "string", "minLength": 1 },
"content": { "type": ["string", "null"] },
"expectedSha256": { "type": ["string", "null"], "minLength": 64, "maxLength": 64 },
"oldText": { "type": ["string", "null"], "minLength": 1 },
"newText": { "type": ["string", "null"] },
"expectedReplacements": { "type": ["integer", "null"], "minimum": 1, "maximum": 100 }
}
}
}
}
})
}
#[cfg(test)]
mod tests {
use super::*;
fn collect_openai_strict_schema_issues(schema: &Value, path: &str, issues: &mut Vec<String>) {
let Some(object) = schema.as_object() else {
return;
};
for keyword in ["oneOf", "anyOf", "allOf", "not", "uniqueItems"] {
if object.contains_key(keyword) {
issues.push(format!(
"strict schema contains unsupported {keyword} at {path}"
));
}
}
if let Some(properties) = object.get("properties").and_then(Value::as_object) {
if object.get("additionalProperties") != Some(&Value::Bool(false)) {
issues.push(format!(
"strict object must reject additional properties at {path}"
));
}
let property_names = properties.keys().cloned().collect::<BTreeSet<_>>();
let required_names = object
.get("required")
.and_then(Value::as_array)
.map(|required| {
required
.iter()
.filter_map(Value::as_str)
.map(ToString::to_string)
.collect::<BTreeSet<_>>()
});
if required_names.as_ref() != Some(&property_names) {
issues.push(format!(
"strict object must require every property at {path}: required={required_names:?}, properties={property_names:?}"
));
}
for (name, child) in properties {
collect_openai_strict_schema_issues(
child,
&format!("{path}/properties/{name}"),
issues,
);
}
}
if let Some(items) = object.get("items") {
collect_openai_strict_schema_issues(items, &format!("{path}/items"), issues);
}
}
fn valid_delegate_input(repair_of_delegation_id: Value, run_id: Value) -> Value {
json!({
"agentId": "specialist",
"task": "完成委派任务",
"acceptanceCriteria": ["定向测试通过"],
"expectedArtifacts": [],
"repairOfDelegationId": repair_of_delegation_id,
"runId": run_id,
"continuationOfDelegationId": null,
"questionsSha256": null,
"answersSha256": null,
})
}
#[test]
fn native_agent_delegate_accepts_complete_clarification_continuation_binding() {
let mut input = valid_delegate_input(json!("delegation-id"), Value::Null);
let object = input.as_object_mut().expect("delegate input object");
object.insert(
"continuationOfDelegationId".to_string(),
json!("delegation-id"),
);
object.insert("questionsSha256".to_string(), json!("a".repeat(64)));
object.insert("answersSha256".to_string(), json!("b".repeat(64)));
validate_native_agent_delegate_input(&input)
.expect("complete clarification continuation binding");
}
#[test]
fn native_agent_delegate_accepts_legacy_input_without_clarification_fields() {
let mut input = valid_delegate_input(Value::Null, Value::Null);
let object = input.as_object_mut().expect("delegate input object");
object.remove("continuationOfDelegationId");
object.remove("questionsSha256");
object.remove("answersSha256");
validate_native_agent_delegate_input(&input)
.expect("legacy delegate input without clarification fields");
}
/// 返工/续跑的两个数组必须逐字继承原委派,Runtime 读得到权威值。要求 Supervisor
/// 手抄它们只会反复失败:实测一次两轮澄清的 plan run 里,
/// 「静态委派返工必须完整继承原 acceptanceCriteria 和 expectedArtifacts」
/// 出现 4 次,每建一条 continuation 先白跑两轮工具调用。
#[test]
fn native_agent_delegate_repair_may_inherit_the_original_contract() {
let mut inherit = valid_delegate_input(json!("delegation-id"), Value::Null);
let object = inherit.as_object_mut().expect("delegate input object");
object.insert("acceptanceCriteria".to_string(), Value::Null);
object.insert("expectedArtifacts".to_string(), Value::Null);
validate_native_agent_delegate_input(&inherit).expect("repair may inherit the contract");
// 初次委派没有可继承的原 delivery,两个数组仍然必须自己写。
let mut initial = valid_delegate_input(Value::Null, json!("run-1"));
let object = initial.as_object_mut().expect("delegate input object");
object.insert("acceptanceCriteria".to_string(), Value::Null);
object.insert("expectedArtifacts".to_string(), Value::Null);
assert!(validate_native_agent_delegate_input(&initial)
.expect_err("initial delegate must still carry its own contract")
.to_string()
.contains("acceptanceCriteria"));
// 只省一半是有歧义的输入,不放行。
let mut half = valid_delegate_input(json!("delegation-id"), Value::Null);
let object = half.as_object_mut().expect("delegate input object");
object.insert("acceptanceCriteria".to_string(), Value::Null);
assert!(validate_native_agent_delegate_input(&half)
.expect_err("half-omitted contract must fail")
.to_string()
.contains("acceptanceCriteria"));
}
#[test]
fn native_agent_delegate_accepts_clarification_continuation_without_fingerprints() {
// 指纹由 Runtime 从原 delivery 补齐,Supervisor 只需要指出续跑的是哪个委派。
let mut anchor_only = valid_delegate_input(json!("delegation-id"), Value::Null);
anchor_only
.as_object_mut()
.expect("delegate input object")
.insert(
"continuationOfDelegationId".to_string(),
json!("delegation-id"),
);
validate_native_agent_delegate_input(&anchor_only)
.expect("clarification continuation without fingerprints");
}
#[test]
fn native_agent_delegate_rejects_partial_or_invalid_clarification_binding() {
let mut single_digest = valid_delegate_input(json!("delegation-id"), Value::Null);
let object = single_digest
.as_object_mut()
.expect("delegate input object");
object.insert(
"continuationOfDelegationId".to_string(),
json!("delegation-id"),
);
object.insert("questionsSha256".to_string(), json!("a".repeat(64)));
assert!(validate_native_agent_delegate_input(&single_digest)
.expect_err("single continuation fingerprint must fail")
.to_string()
.contains("必须成对提供"));
let mut orphan_digests = valid_delegate_input(json!("delegation-id"), Value::Null);
let object = orphan_digests
.as_object_mut()
.expect("delegate input object");
object.insert("questionsSha256".to_string(), json!("a".repeat(64)));
object.insert("answersSha256".to_string(), json!("b".repeat(64)));
assert!(validate_native_agent_delegate_input(&orphan_digests)
.expect_err("continuation fingerprints without anchor must fail")
.to_string()
.contains("必须同时提供 continuationOfDelegationId"));
let mut invalid_sha = valid_delegate_input(json!("delegation-id"), Value::Null);
let object = invalid_sha.as_object_mut().expect("delegate input object");
object.insert(
"continuationOfDelegationId".to_string(),
json!("delegation-id"),
);
object.insert("questionsSha256".to_string(), json!("z".repeat(64)));
object.insert("answersSha256".to_string(), json!("b".repeat(64)));
assert!(validate_native_agent_delegate_input(&invalid_sha)
.expect_err("invalid continuation sha must fail")
.to_string()
.contains("SHA-256"));
}
#[test]
fn native_agent_delegate_repair_rejects_string_run_id() {
let repair_id = "delegation-value-must-not-leak";
let run_id = "run-value-must-not-leak";
let error = validate_native_agent_delegate_input(&valid_delegate_input(
json!(repair_id),
json!(run_id),
))
.expect_err("repair delegate must not accept a string runId");
assert_eq!(
error.kind(),
AgentRuntimeToolPlanProtocolErrorKind::ArgumentsSchema
);
let detail = error.to_string();
assert_eq!(
detail,
"Agent 原生工具协议错误:agent.delegate 返工委派时 runId 必须为 JSON null"
);
assert!(!detail.contains(repair_id));
assert!(!detail.contains(run_id));
}
#[test]
fn native_agent_delegate_repair_accepts_null_run_id() {
let input = valid_delegate_input(json!("delegation-id"), Value::Null);
validate_native_agent_delegate_input(&input)
.expect("repair delegate should accept a null runId");
}
#[test]
fn native_agent_delegate_initial_accepts_string_run_id() {
let input = valid_delegate_input(Value::Null, json!("initial-run-id"));
validate_native_agent_delegate_input(&input)
.expect("initial delegate should accept a valid string runId");
}
#[test]
fn native_agent_delegate_description_explains_repair_run_identity() {
let description = runtime_tool_description("agent.delegate");
assert!(description.contains("repairOfDelegationId 指向原 delivery"));
assert!(description.contains("runId 必须为 null"));
// 广告给模型的 schema 一度把这两个数组写死成非空数组,模型于是根本无法
// 传 null,只能手抄——实测每条 continuation 固定先失败 2 次才抄对。
assert!(
description.contains("acceptanceCriteria 与 expectedArtifacts 一起传 null"),
"{description}"
);
let schema = runtime_tool_input_schema("agent.delegate");
for field in ["acceptanceCriteria", "expectedArtifacts"] {
let types = schema["properties"][field]["type"]
.as_array()
.unwrap_or_else(|| panic!("{field} 必须允许 null"));
assert!(
types.iter().any(|value| value == "null"),
"{field} 的广告 schema 必须允许 null,否则 Runtime 侧的继承分支永远走不到"
);
}
}
#[test]
fn native_goal_contract_and_acceptance_update_expose_dynamic_graph_contract() {
let goal = runtime_tool_input_schema("agent.goal_contract");
assert_eq!(
goal["required"],
json!([
"outcome",
"nonNegotiables",
"preferences",
"forbiddenAssumptions",
"openQuestions",
"acceptanceNodes"
])
);
assert_eq!(goal["properties"]["acceptanceNodes"]["minItems"], 1);
assert_eq!(goal["properties"]["acceptanceNodes"]["maxItems"], 32);
assert_eq!(
goal["properties"]["acceptanceNodes"]["items"]["properties"]["requiredEvidence"]
["items"]["pattern"],
"^(tool:)?[a-z][a-z0-9._-]{0,79}$"
);
assert_eq!(
goal["properties"]["acceptanceNodes"]["items"]["required"],
json!([
"criterionId",
"criterion",
"required",
"requiredEvidence",
"dependsOn"
])
);
let update = runtime_tool_input_schema("agent.acceptance_update");
assert_eq!(
update["required"],
json!(["contractFingerprint", "evaluations"])
);
assert_eq!(
update["properties"]["evaluations"]["items"]["properties"]["status"]["enum"],
json!(["passed", "failed", "not-observed"])
);
assert_eq!(
update["properties"]["evaluations"]["items"]["properties"]["evidence"]["items"]
["required"],
json!(["agentId", "runId", "actionId"])
);
}
#[test]
fn native_runtime_capability_registry_is_the_bidirectional_catalog() {
let registry = agent_runtime_native_capability_registry().expect("native registry");
let executable_tools = agent_runtime_native_executable_tools();
assert_eq!(registry.len(), executable_tools.len());
for tool in executable_tools {
let definition = registry.get(tool).expect("registered runtime tool");
assert_eq!(definition.id(), tool);
assert_eq!(definition.dispatch(), tool);
assert_eq!(
native_runtime_function_name(tool).as_deref(),
Some(definition.function_name())
);
assert_eq!(
runtime_tool_for_native_function(definition.function_name()).as_deref(),
Some(tool)
);
}
}
#[test]
fn godot_native_registry_follows_toggle_without_reusing_other_editor_cache() {
let _guard = crate::builtin_plugins::test_lock();
let config = tempfile::tempdir().unwrap();
crate::builtin_plugins::initialize(config.path()).unwrap();
let project = tempfile::tempdir().unwrap();
std::fs::create_dir(project.path().join("game")).unwrap();
std::fs::write(
project.path().join("game/project.godot"),
"config_version=5\n",
)
.unwrap();
let other_project = tempfile::tempdir().unwrap();
for enabled in [false, true, false, true] {
crate::builtin_plugins::set_enabled(
crate::builtin_plugins::AGC_GODOT_EDITOR_PLUGIN_ID,
enabled,
)
.unwrap();
let expected = enabled
&& cfg!(all(
windows,
target_arch = "x86_64",
feature = "godot-editor-execute"
));
assert_eq!(
native_runtime_function_name("godot.editor.execute").is_some(),
expected
);
let name = native_runtime_function_name_for_tool("godot.editor.execute");
assert_eq!(
build_agent_runtime_native_function_tools_for_project(
project.path(),
"__all_agents__"
)
.unwrap()
.iter()
.any(|tool| tool.name == name),
expected
);
assert!(!build_agent_runtime_native_function_tools_for_project(
other_project.path(),
"__all_agents__"
)
.unwrap()
.iter()
.any(|tool| tool.name == name));
}
}
#[test]
fn godot_native_schema_cannot_override_execution_identity() {
let schema = runtime_tool_input_schema("godot.editor.execute");
assert_eq!(schema["additionalProperties"], false);
assert_eq!(schema["required"], json!(["code"]));
assert_eq!(schema["properties"].as_object().unwrap().len(), 1);
}
#[test]
fn editor_guides_reach_native_tool_definitions_without_truncation() {
let _guard = crate::builtin_plugins::test_lock();
let config = tempfile::tempdir().unwrap();
crate::builtin_plugins::initialize(config.path()).unwrap();
for id in [
crate::builtin_plugins::AGC_UNITY_EDITOR_PLUGIN_ID,
crate::builtin_plugins::AGC_GODOT_EDITOR_PLUGIN_ID,
] {
crate::builtin_plugins::set_enabled(id, true).unwrap();
}
let functions = build_agent_runtime_native_function_tools().unwrap();
for (engine, skill, tool) in [
("Unity", "agc-unity-editor", "unity.editor.execute"),
("Godot", "agc-godot-editor", "godot.editor.execute"),
] {
let reference = crate::agent::read_agc_skill_resource(&format!(
"{skill}/references/【操作指南】{engine}编辑器常用操作-2026-09-20.md"
))
.unwrap();
assert_eq!(
editor_operation_reference(tool)
.unwrap()
.replace("\r\n", "\n"),
reference
);
let emitted = functions
.iter()
.find(|function| function.name == native_runtime_function_name_for_tool(tool));
let expected = match engine {
"Unity" => cfg!(all(
windows,
target_arch = "x86_64",
feature = "unity-editor-execute"
)),
"Godot" => cfg!(all(
windows,
target_arch = "x86_64",
feature = "godot-editor-execute"
)),
_ => false,
};
assert_eq!(emitted.is_some(), expected);
if let Some(function) = emitted {
let wire = serde_json::to_value(function).unwrap();
assert_eq!(
wire["description"].as_str().unwrap().replace("\r\n", "\n"),
reference
);
} else {
assert!(!functions
.iter()
.any(|function| function.description.contains(&reference)));
}
}
}
#[test]
fn strict_native_function_schemas_match_openai_subset() {
let functions =
build_agent_runtime_native_function_tools().expect("build native function tools");
let mut issues = Vec::new();
for function in functions.iter().filter(|function| function.strict) {
collect_openai_strict_schema_issues(&function.parameters, &function.name, &mut issues);
}
assert!(issues.is_empty(), "{}", issues.join("\n"));
}
#[test]
fn canvas_asset_generate_schema_uses_shared_asset_kind_catalog() {
let schema = runtime_tool_input_schema("canvas.asset_generate");
let mut expected = AGENT_RUNTIME_CANVAS_ASSET_KINDS
.iter()
.map(|kind| Value::String(kind.as_str().to_string()))
.collect::<Vec<_>>();
expected.push(Value::Null);
assert_eq!(
schema.pointer("/properties/assetKind/enum"),
Some(&Value::Array(expected))
);
}
#[test]
fn action_function_parameters_rebases_local_schema_refs_after_wrapping() {
let parameters = action_function_parameters(json!({
"type": "object",
"$defs": {
"Mode": {"type": "string", "enum": ["fast", "safe"]},
"Options": {
"type": "object",
"properties": {"mode": {"$ref": "#/$defs/Mode"}},
"required": ["mode"],
"additionalProperties": false
}
},
"properties": {
"options": {"$ref": "#/$defs/Options"},
"recursive": {"$ref": "#"},
"anchor": {"$ref": "#Mode"},
"scoped": {
"$id": "nested.json",
"$defs": {"Value": {"type": "string"}},
"properties": {"value": {"$ref": "#/$defs/Value"}}
},
"external": {"$ref": "https://schemas.example/tool.json"}
},
"required": ["options"],
"additionalProperties": false
}));
let input = &parameters["properties"]["input"];
assert_eq!(
input["properties"]["options"]["$ref"],
"#/properties/input/$defs/Options"
);
assert_eq!(
input["$defs"]["Options"]["properties"]["mode"]["$ref"],
"#/properties/input/$defs/Mode"
);
assert_eq!(
input["properties"]["recursive"]["$ref"],
"#/properties/input"
);
assert_eq!(input["properties"]["anchor"]["$ref"], "#Mode");
assert_eq!(
input["properties"]["scoped"]["properties"]["value"]["$ref"],
"#/$defs/Value"
);
assert_eq!(
input["properties"]["external"]["$ref"],
"https://schemas.example/tool.json"
);
for reference in [
input["properties"]["options"]["$ref"]
.as_str()
.expect("options ref"),
input["$defs"]["Options"]["properties"]["mode"]["$ref"]
.as_str()
.expect("mode ref"),
input["properties"]["recursive"]["$ref"]
.as_str()
.expect("recursive ref"),
] {
assert!(
parameters
.pointer(reference.trim_start_matches('#'))
.is_some(),
"rebased ref must resolve: {reference}"
);
}
}
#[test]
fn action_function_parameters_preserves_refs_inside_schema_data_keywords() {
let parameters = action_function_parameters(json!({
"type": "object",
"$defs": {
"Value": {"type": "string"}
},
"properties": {
"value": {
"$ref": "#/$defs/Value",
"default": {"$ref": "#/literal-default"},
"const": {
"nested": [{"$ref": "#/literal-const"}]
},
"examples": [
{"$ref": "#/literal-example"},
[{"$ref": "#/nested-literal-example"}]
]
}
},
"required": ["value"],
"additionalProperties": false
}));
let value = &parameters["properties"]["input"]["properties"]["value"];
assert_eq!(value["$ref"], "#/properties/input/$defs/Value");
assert_eq!(value["default"]["$ref"], "#/literal-default");
assert_eq!(value["const"]["nested"][0]["$ref"], "#/literal-const");
assert_eq!(value["examples"][0]["$ref"], "#/literal-example");
assert_eq!(value["examples"][1][0]["$ref"], "#/nested-literal-example");
}
#[test]
fn native_project_patchset_normalizes_nullable_strict_shape() {
let arguments = json!({
"reason": "原子应用三类变更",
"input": {
"changes": [
{
"operation": "create",
"path": "game/new.txt",
"content": "created",
"expectedSha256": null,
"oldText": null,
"newText": null,
"expectedReplacements": null
},
{
"operation": "update",
"path": "game/main.txt",
"content": null,
"expectedSha256": "a".repeat(64),
"oldText": "before",
"newText": "after",
"expectedReplacements": 1
},
{
"operation": "delete",
"path": "game/old.txt",
"content": null,
"expectedSha256": "b".repeat(64),
"oldText": null,
"newText": null,
"expectedReplacements": null
}
]
}
});
let parsed = parse_agent_runtime_native_tool_calls(&[LlmToolCall {
id: "patchset-call".to_string(),
name: native_runtime_function_name("project.patchset")
.expect("native patchset function name"),
arguments: serde_json::to_string(&arguments).expect("serialize arguments"),
}])
.expect("parse strict patchset call");
assert_eq!(
parsed.plan.actions[0].input,
json!({
"changes": [
{"operation": "create", "path": "game/new.txt", "content": "created"},
{
"operation": "update",
"path": "game/main.txt",
"expectedSha256": "a".repeat(64),
"oldText": "before",
"newText": "after",
"expectedReplacements": 1
},
{"operation": "delete", "path": "game/old.txt", "expectedSha256": "b".repeat(64)}
]
})
);
}
}