Refactor: Add client turn attribution to tool diagnostics and enhance test coverage
- Introduced `active_client_turn_id` for client turn attribution in diagnostics. - Updated error tracking to include `clientTurnId` in tool failure events. - Added test cases to validate behavior with and without bound client turns.
This commit is contained in:
@@ -365,16 +365,24 @@ impl DirectToolBridgeState {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 取当前回合身份,媒体资源请求按回合身份与请求指纹确定性派生 operation/idempotency id。
|
||||
fn active_resource_turn_id(&self) -> Result<String, String> {
|
||||
/// 读当前回合的客户端身份;桥没有被回合授权时给出 `None`。
|
||||
///
|
||||
/// 回合身份既用于派生确定性 operation/idempotency id,也用于给诊断事件标回合归属。
|
||||
/// 这里只如实读授权状态,缺回合不是错误:外部客户端工具桥本来就不绑定直连回合。
|
||||
fn active_client_turn_id(&self) -> Result<Option<String>, String> {
|
||||
let authorization = self
|
||||
.turn_authorization
|
||||
.lock()
|
||||
.map_err(|_| "AGC 工具桥回合授权状态不可用".to_string())?;
|
||||
authorization
|
||||
Ok(authorization
|
||||
.active
|
||||
.as_ref()
|
||||
.map(|active| active.turn_id.clone())
|
||||
.map(|active| active.turn_id.clone()))
|
||||
}
|
||||
|
||||
/// 取当前回合身份,媒体资源请求按回合身份与请求指纹确定性派生 operation/idempotency id。
|
||||
fn active_resource_turn_id(&self) -> Result<String, String> {
|
||||
self.active_client_turn_id()?
|
||||
.ok_or_else(|| "当前没有客户端签发的资源生成回合身份".to_string())
|
||||
}
|
||||
}
|
||||
@@ -3114,9 +3122,12 @@ async fn handle_direct_tool_bridge(
|
||||
} else {
|
||||
"tool-error"
|
||||
};
|
||||
// 诊断事件按回合归属:并发排障要能从错误记录认出是哪一轮调的工具。
|
||||
// 桥没被回合授权(外部客户端工具桥)或授权状态不可用时如实记为无回合。
|
||||
let client_turn_id = state.active_client_turn_id().ok().flatten();
|
||||
let _ = persist_agent_runtime_error(
|
||||
&state.root,
|
||||
None,
|
||||
client_turn_id.as_deref(),
|
||||
"agc-tools",
|
||||
"tool-execution",
|
||||
code,
|
||||
@@ -4885,6 +4896,77 @@ mod tests {
|
||||
assert!(state.authorize_regeneration_call("陶泥风格").is_err());
|
||||
}
|
||||
|
||||
/// 失败工具写的诊断事件按回合归属:并发排障要能从错误记录认出是哪一轮调的工具。
|
||||
#[tokio::test]
|
||||
async fn failed_tool_call_records_the_bound_client_turn() {
|
||||
let temporary = tempfile::tempdir().expect("turn attribution root");
|
||||
let root = temporary.path();
|
||||
init_local_game_project_at(root, "turn-attribution", "回合归属").unwrap();
|
||||
let client_turn_id = "client-turn-attribution-0001";
|
||||
let _reservation =
|
||||
TurnReservation::accept_for_test(&thread_id_for_project(root), client_turn_id);
|
||||
let state = direct_tool_bridge_state(root.to_path_buf());
|
||||
let _active_turn = state.begin_user_turn().expect("bind the client turn");
|
||||
|
||||
let response = handle_direct_tool_bridge(
|
||||
State(state),
|
||||
Json(DirectToolBridgeRequest {
|
||||
tool: "agc_unreviewed_tool".to_string(),
|
||||
arguments: json!({}),
|
||||
}),
|
||||
)
|
||||
.await
|
||||
.0;
|
||||
assert_eq!(response["isError"], true);
|
||||
|
||||
assert_eq!(
|
||||
recorded_tool_error_client_turn_ids(root),
|
||||
vec![json!(client_turn_id)]
|
||||
);
|
||||
}
|
||||
|
||||
/// 桥没有被回合授权时如实记 null:外部客户端工具桥不绑定直连回合。
|
||||
#[tokio::test]
|
||||
async fn failed_tool_call_without_a_bound_turn_records_no_client_turn() {
|
||||
let temporary = tempfile::tempdir().expect("unbound turn root");
|
||||
let root = temporary.path();
|
||||
init_local_game_project_at(root, "turn-unbound", "无回合归属").unwrap();
|
||||
|
||||
let response = handle_direct_tool_bridge(
|
||||
State(direct_tool_bridge_state(root.to_path_buf())),
|
||||
Json(DirectToolBridgeRequest {
|
||||
tool: "agc_unreviewed_tool".to_string(),
|
||||
arguments: json!({}),
|
||||
}),
|
||||
)
|
||||
.await
|
||||
.0;
|
||||
assert_eq!(response["isError"], true);
|
||||
|
||||
assert_eq!(recorded_tool_error_client_turn_ids(root), vec![Value::Null]);
|
||||
}
|
||||
|
||||
/// 读工具失败诊断里的 `clientTurnId`:字段必须存在,值可以是 `null`。
|
||||
fn recorded_tool_error_client_turn_ids(root: &Path) -> Vec<Value> {
|
||||
let mut turn_ids = std::fs::read_dir(root.join(".agent/runtime/errors"))
|
||||
.expect("runtime error directory")
|
||||
.filter_map(Result::ok)
|
||||
.map(|entry| {
|
||||
let raw = std::fs::read_to_string(entry.path()).expect("runtime error sidecar");
|
||||
let event: Value = serde_json::from_str(&raw).expect("parse runtime error sidecar");
|
||||
assert_eq!(event["source"], "agc-tools", "{raw}");
|
||||
assert_eq!(event["stage"], "tool-execution", "{raw}");
|
||||
assert_eq!(event["code"], "tool-error", "{raw}");
|
||||
event
|
||||
.get("clientTurnId")
|
||||
.cloned()
|
||||
.unwrap_or_else(|| panic!("tool error event must carry clientTurnId: {raw}"))
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
turn_ids.sort_by_key(Value::to_string);
|
||||
turn_ids
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bridge_art_resource_exposes_only_the_safe_identity_projection() {
|
||||
let asset = GameCreationAppAssetManifestEntry {
|
||||
|
||||
Reference in New Issue
Block a user