tool-plan 审计记下上游终态标记,预算切断不再无声
tool-plan 这条链路此前把 finish_reason 整个丢掉了:provider_tool_plan.rs 全文没有一处引用它。platform-llm 只在**存在工具调用**且上游明确给出未完成 终态时才拒收(reject_incomplete_tool_calls),其余情形按「可用的降级结果」 放行;而这一层既不看也不记,审计里没有任何东西能把「上游说这一轮没写完」 和「模型自己写歪了」分开。 信封退化的排查就卡在这个盲点上:我一度据此判定上游根本不发 status,实际 response-streams 里的 final-reply 记录明明写着 "completed"——只是 tool-plan 那一发的标记从来没落过盘,而策划子 Agent 的信封恰恰产在 tool-plan 那一发 (stream=false 时 plan.response 直接充当 final reply,不再发 final-reply 请求)。 只记录,不改判。是否因未完成终态拒收仍旧由 platform-llm 决定,做游戏与 做素材逐字保持既有行为。 字段按固定字符集夹紧后落库:兼容网关会在这里发自定义值甚至整段文案,审计 不是转发通道,越界字符丢弃、长度夹到 32、夹空记 null。agent_db 侧同步进 PROTOCOL_FIELDS 白名单并复核夹紧结果——那张白名单是精确长度匹配,写入侧 加字段而不同步读取侧会直接把审计写失败。 tool_plan 过滤器 93/93。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -140,6 +140,32 @@ fn root_goal_contract_repair_instruction(protocol_error: &str, plan_root: bool)
|
||||
}
|
||||
}
|
||||
|
||||
/// 把上游的终态标记夹紧成可落审计的短标记。
|
||||
///
|
||||
/// tool-plan 这条链路此前把 finish_reason 整个丢掉了:真撞上 max_output_tokens 时,
|
||||
/// platform-llm 只会在**存在工具调用**且上游明确给出未完成终态时拒收,其余情形一律
|
||||
/// 当作可用的降级结果放行,而这一层既不看也不记,审计里没有任何东西能把「上游说这
|
||||
/// 一轮没写完」和「模型自己写歪了」分开。信封退化的现场排查就卡在这里。
|
||||
///
|
||||
/// 只记录,不改判:是否因未完成终态拒收仍旧由 platform-llm 决定,做游戏与做素材的
|
||||
/// 行为逐字不变。
|
||||
fn agent_runtime_tool_plan_audit_finish_reason(finish_reason: Option<&str>) -> Option<String> {
|
||||
// 兼容网关常发自定义值甚至整段文案,所以按固定字符集丢弃而不是原样透传。
|
||||
let reason = finish_reason?
|
||||
.trim()
|
||||
.to_ascii_lowercase()
|
||||
.chars()
|
||||
.filter(|character| {
|
||||
character.is_ascii_lowercase()
|
||||
|| character.is_ascii_digit()
|
||||
|| *character == '_'
|
||||
|| *character == '-'
|
||||
})
|
||||
.take(32)
|
||||
.collect::<String>();
|
||||
(!reason.is_empty()).then_some(reason)
|
||||
}
|
||||
|
||||
pub(in crate::agent) fn append_game_creator_agent_tool_plan_audit_idempotent(
|
||||
root: &Path,
|
||||
record: serde_json::Value,
|
||||
@@ -873,6 +899,9 @@ pub(in crate::agent) async fn request_game_creator_agent_background_tool_plan_at
|
||||
"responseFingerprint": response_fingerprint,
|
||||
"providerRequestIdSha256": provider_request_id_sha256,
|
||||
"protocol": protocol,
|
||||
"finishReason": agent_runtime_tool_plan_audit_finish_reason(
|
||||
response.finish_reason.as_deref(),
|
||||
),
|
||||
"functionCallCount": call_ids.len(),
|
||||
"callIdSha256s": call_id_sha256s,
|
||||
"functionNames": function_names,
|
||||
@@ -1380,6 +1409,44 @@ pub(crate) async fn request_game_creator_agent_background_tool_plan_for_test(
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tool_plan_audit_finish_reason_tests {
|
||||
use super::*;
|
||||
|
||||
/// 正常终态原样落库,缺失记 null——这两个值就是「上游到底说没说这一轮写完了」
|
||||
/// 的全部答案,此前审计里一个都没有。
|
||||
#[test]
|
||||
fn ordinary_finish_reasons_are_recorded_and_absence_stays_null() {
|
||||
assert_eq!(
|
||||
agent_runtime_tool_plan_audit_finish_reason(Some("completed")).as_deref(),
|
||||
Some("completed")
|
||||
);
|
||||
assert_eq!(
|
||||
agent_runtime_tool_plan_audit_finish_reason(Some(" INCOMPLETE ")).as_deref(),
|
||||
Some("incomplete")
|
||||
);
|
||||
assert_eq!(agent_runtime_tool_plan_audit_finish_reason(None), None);
|
||||
assert_eq!(agent_runtime_tool_plan_audit_finish_reason(Some(" ")), None);
|
||||
}
|
||||
|
||||
/// 兼容网关会在这个字段里发自定义值甚至整段文案。审计不是转发通道:越界字符
|
||||
/// 一律丢弃,长度夹到 32,夹空了记 null,绝不原样透传。
|
||||
#[test]
|
||||
fn gateway_freeform_reasons_are_clamped_rather_than_relayed() {
|
||||
assert_eq!(
|
||||
agent_runtime_tool_plan_audit_finish_reason(Some("上游异常:截断了")),
|
||||
None
|
||||
);
|
||||
assert_eq!(
|
||||
agent_runtime_tool_plan_audit_finish_reason(Some("stop\n\"};DROP")).as_deref(),
|
||||
Some("stopdrop")
|
||||
);
|
||||
let clamped = agent_runtime_tool_plan_audit_finish_reason(Some(&"a".repeat(200)))
|
||||
.expect("a long ascii reason is still recorded");
|
||||
assert_eq!(clamped.chars().count(), 32);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod supervisor_collaboration_repair_tests {
|
||||
use super::*;
|
||||
|
||||
@@ -2138,6 +2138,7 @@ pub(crate) fn append_agent_db_tool_plan_audit_idempotent(
|
||||
"responseFingerprint",
|
||||
"providerRequestIdSha256",
|
||||
"protocol",
|
||||
"finishReason",
|
||||
"functionCallCount",
|
||||
"callIdSha256s",
|
||||
"functionNames",
|
||||
@@ -2264,6 +2265,31 @@ pub(crate) fn append_agent_db_tool_plan_audit_idempotent(
|
||||
return Err(format!("Agent DB tool-plan 幂等审计字段无效:{field}"));
|
||||
}
|
||||
}
|
||||
// Provider 终态标记是上游自由文本(兼容网关常发自定义值),写入侧已夹紧成
|
||||
// 固定字符集的短标记;这里只复核夹紧结果,不接受原样透传的自由文本。
|
||||
match record.get("finishReason") {
|
||||
Some(serde_json::Value::Null) => {}
|
||||
Some(serde_json::Value::String(value)) => {
|
||||
if value.is_empty()
|
||||
|| value.chars().count() > 32
|
||||
|| !value
|
||||
.chars()
|
||||
.all(|character| {
|
||||
character.is_ascii_lowercase()
|
||||
|| character.is_ascii_digit()
|
||||
|| character == '_'
|
||||
|| character == '-'
|
||||
})
|
||||
{
|
||||
return Err(
|
||||
"Agent DB tool-plan 幂等审计字段无效:finishReason".to_string()
|
||||
);
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
return Err("Agent DB tool-plan 幂等审计字段无效:finishReason".to_string());
|
||||
}
|
||||
}
|
||||
let autonomous_source_payload_validated = record
|
||||
.get("autonomousSourcePayloadValidated")
|
||||
.and_then(serde_json::Value::as_bool)
|
||||
|
||||
@@ -52,6 +52,7 @@ fn tool_plan_protocol_audit_record(
|
||||
"responseFingerprint": "1".repeat(64),
|
||||
"providerRequestIdSha256": "2".repeat(64),
|
||||
"protocol": "native_runtime_tools",
|
||||
"finishReason": "completed",
|
||||
"functionCallCount": 0,
|
||||
"callIdSha256s": [],
|
||||
"functionNames": [],
|
||||
|
||||
Reference in New Issue
Block a user