直连工具结果与计划回灌不再脱敏,只做长度收口
- `direct_validation`:浏览器/命令验证回执里的 error/output 原文返回给 Codex - `direct_patch`:补丁工具输出原文返回 - `direct_delivery`:计划步文案、`contract.scope`、未过验收的缺口清单原文返回 - `direct_runtime`:浏览器事实证据(diagnostic / console.error / exception / fatal request)、陶泥儿整包完成告警、可返修错误的反馈提示原文返回 - `provider_request_builders`:回灌给模型的 `thinkingSummary` / `fallbackResponse` 原文返回 - 两个测试改为断言原文进入 prompt(`browser_diagnostics_go_verbatim_to_the_model_before_same_thread_repair`、`completed_regeneration_replays_the_exact_lost_response`) - 去掉因不再脱敏而失去用途的 `root` 形参:`update_plan`、`bound_for_completed_workflow_at` / `validate_completed_workflow_result_at` - 审计/诊断侧照旧脱敏:`record_direct_codex_failure`、`DirectTurnTerminal::failed`、`persist_agent_runtime_error`、`codex_app_server` 日志
This commit is contained in:
@@ -35,7 +35,6 @@ enum DirectPlanStepStatus {
|
||||
}
|
||||
|
||||
pub(super) async fn update_plan(
|
||||
root: &Path,
|
||||
session: &Arc<ExecutionSession>,
|
||||
input: &Value,
|
||||
) -> Result<Value, String> {
|
||||
@@ -51,13 +50,13 @@ pub(super) async fn update_plan(
|
||||
{
|
||||
return Err("direct-plan-invalid: 每步需为1到500字".into());
|
||||
}
|
||||
item.step = redact_agent_runtime_error(root, item.step.trim(), 500);
|
||||
item.step = truncate_agent_runtime_text(item.step.trim(), 500);
|
||||
}
|
||||
if let Some(explanation) = &mut plan.explanation {
|
||||
if explanation.chars().count() > 1200 || explanation.contains('\0') {
|
||||
return Err("direct-plan-invalid: 说明超过上限".into());
|
||||
}
|
||||
*explanation = redact_agent_runtime_error(root, explanation, 1200);
|
||||
*explanation = truncate_agent_runtime_text(explanation, 1200);
|
||||
}
|
||||
let value = serde_json::to_value(plan).map_err(|_| "direct-plan-invalid")?;
|
||||
let session = Arc::clone(session);
|
||||
@@ -689,7 +688,7 @@ pub(super) async fn finish_sealing(
|
||||
session.reopen_for_repair()?; return Ok(None);
|
||||
}
|
||||
let contract: FrozenContract = serde_json::from_value(ledger.contract.clone().ok_or("delivery-contract-missing")?).map_err(|_| "delivery-contract-invalid")?;
|
||||
let report = format!("本轮已完成宿主验收。\n\n范围:{}\n\n{}\n\n执行/返修批次:{}/{}。仅证明以上登记范围;固定场景不代表完整长关卡已通关。执行预算按宿主观察的占用计入;第三方并发调用可能按组占用上界计入。", redact_agent_runtime_error(&root,&contract.scope,1200),
|
||||
let report = format!("本轮已完成宿主验收。\n\n范围:{}\n\n{}\n\n执行/返修批次:{}/{}。仅证明以上登记范围;固定场景不代表完整长关卡已通关。执行预算按宿主观察的占用计入;第三方并发调用可能按组占用上界计入。", truncate_agent_runtime_text(&contract.scope,1200),
|
||||
final_check.checks.iter().map(|check|format!("- 已通过:{}",check["label"].as_str().unwrap_or("验收项"))).collect::<Vec<_>>().join("\n"), ledger.used_passes,ledger.max_runs);
|
||||
session.complete(report.clone())?;
|
||||
if mark_initial_delivered(&root,&ledger).is_err() { app_log!("首次交付标记未写入,后续保持更严格的新项目验收要求"); }
|
||||
@@ -746,7 +745,7 @@ pub(super) async fn review_reply(
|
||||
.join("\n");
|
||||
let report = format!(
|
||||
"本轮未完成宿主验收,已到有界复核上限。保留已有修改与证据;仍未通过:\n{}",
|
||||
redact_agent_runtime_error(root, &missing, 6000)
|
||||
truncate_agent_runtime_text(&missing, 6000)
|
||||
);
|
||||
let session = Arc::clone(session);
|
||||
let saved = report.clone();
|
||||
@@ -830,23 +829,21 @@ mod tests {
|
||||
let plan =
|
||||
json!({"explanation":"任务进度", "plan":[{"step":"已完成", "status":"completed"}]});
|
||||
assert_eq!(
|
||||
update_plan(root.path(), &session, &plan).await.unwrap()["acceptancePassed"],
|
||||
update_plan(&session, &plan).await.unwrap()["acceptancePassed"],
|
||||
false
|
||||
);
|
||||
assert!(!try_seal(root.path(), &session).await.unwrap());
|
||||
assert_eq!(session.snapshot().unwrap().contract, contract);
|
||||
let mut forged = plan.clone();
|
||||
forged["passed"] = json!(true);
|
||||
assert!(update_plan(root.path(), &session, &forged).await.is_err());
|
||||
assert!(update_plan(&session, &forged).await.is_err());
|
||||
assert!(update_plan(
|
||||
root.path(),
|
||||
&session,
|
||||
&json!({"plan":[{"step":"task", "status":"passed"}]})
|
||||
)
|
||||
.await
|
||||
.is_err());
|
||||
assert!(update_plan(
|
||||
root.path(),
|
||||
&session,
|
||||
&json!({"plan":[{"step":"a".repeat(501), "status":"pending"}]})
|
||||
)
|
||||
|
||||
@@ -332,7 +332,7 @@ fn run_transaction(
|
||||
.collect::<BTreeSet<_>>();
|
||||
let changed = started && !changed_paths.is_empty();
|
||||
let mut uncertain = needs_reconciliation || timed_out;
|
||||
let mut output = redact_agent_runtime_error(root, &output, 6000);
|
||||
let mut output = truncate_agent_runtime_text(&output, 6000);
|
||||
let passed = exit_code == Some(0) && !uncertain;
|
||||
if uncertain {
|
||||
session.interrupt(
|
||||
|
||||
@@ -361,23 +361,21 @@ pub(crate) struct DirectTaonierArtPackage {
|
||||
}
|
||||
|
||||
impl DirectTaonierArtPackage {
|
||||
fn redact_for_completed_workflow_at(mut self, root: &Path) -> Result<Self, String> {
|
||||
/// 告警原文给模型,只做长度收口;脱敏只发生在写审计/日志的那一份。
|
||||
fn bound_for_completed_workflow_at(mut self) -> Result<Self, String> {
|
||||
for warning in self
|
||||
.warnings
|
||||
.iter_mut()
|
||||
.chain(self.slice_warnings.iter_mut())
|
||||
{
|
||||
*warning = redact_agent_runtime_error(
|
||||
root,
|
||||
warning,
|
||||
DIRECT_TAONIER_COMPLETED_WARNING_MAX_CHARS,
|
||||
);
|
||||
*warning =
|
||||
truncate_agent_runtime_text(warning, DIRECT_TAONIER_COMPLETED_WARNING_MAX_CHARS);
|
||||
}
|
||||
self.validate_completed_workflow_result_at(root)?;
|
||||
self.validate_completed_workflow_result_at()?;
|
||||
Ok(self)
|
||||
}
|
||||
|
||||
fn validate_completed_workflow_result_at(&self, root: &Path) -> Result<(), String> {
|
||||
fn validate_completed_workflow_result_at(&self) -> Result<(), String> {
|
||||
let expected_asset_paths = DIRECT_CODEX_ART_ASSET_PATHS
|
||||
.iter()
|
||||
.map(|path| (*path).to_string())
|
||||
@@ -398,14 +396,8 @@ impl DirectTaonierArtPackage {
|
||||
return Err("陶泥儿整包重生成完成结果的告警数量超过上限".to_string());
|
||||
}
|
||||
for warning in self.warnings.iter().chain(self.slice_warnings.iter()) {
|
||||
if warning.chars().count() > DIRECT_TAONIER_COMPLETED_WARNING_MAX_CHARS
|
||||
|| redact_agent_runtime_error(
|
||||
root,
|
||||
warning,
|
||||
DIRECT_TAONIER_COMPLETED_WARNING_MAX_CHARS,
|
||||
) != *warning
|
||||
{
|
||||
return Err("陶泥儿整包重生成完成结果包含未脱敏或超限告警".to_string());
|
||||
if warning.chars().count() > DIRECT_TAONIER_COMPLETED_WARNING_MAX_CHARS {
|
||||
return Err("陶泥儿整包重生成完成结果的告警超过长度上限".to_string());
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
@@ -574,7 +566,7 @@ fn read_direct_taonier_regeneration_workflow_at(
|
||||
rollback.validate()?;
|
||||
}
|
||||
if let Some(result) = workflow.completed_result.as_ref() {
|
||||
result.validate_completed_workflow_result_at(root)?;
|
||||
result.validate_completed_workflow_result_at()?;
|
||||
}
|
||||
match workflow.state {
|
||||
DirectTaonierRegenerationWorkflowState::Completed
|
||||
@@ -1266,7 +1258,7 @@ fn complete_direct_taonier_regeneration_workflow_locked_at(
|
||||
expected: &DirectTaonierRegenerationWorkflow,
|
||||
result: DirectTaonierArtPackage,
|
||||
) -> Result<DirectTaonierArtPackage, String> {
|
||||
let result = result.redact_for_completed_workflow_at(root)?;
|
||||
let result = result.bound_for_completed_workflow_at()?;
|
||||
if !direct_taonier_strict_art_package_is_valid(root)
|
||||
|| direct_registered_taonier_slice_paths(root) != result.slice_paths
|
||||
{
|
||||
@@ -3614,7 +3606,7 @@ fn direct_browser_evidence_prompt(
|
||||
completion_error: Option<&str>,
|
||||
) -> String {
|
||||
let completion_status = completion_error
|
||||
.map(|error| redact_agent_runtime_error(root, error, 320))
|
||||
.map(|error| truncate_agent_runtime_text(error, 320))
|
||||
.unwrap_or_else(|| prompt_text!("direct.browser.noCompletionError").to_string());
|
||||
let (browser_status, viewport_lines, details, advisory) = match result {
|
||||
Some(result) => {
|
||||
@@ -3651,7 +3643,7 @@ fn direct_browser_evidence_prompt(
|
||||
.iter()
|
||||
.flat_map(|viewport| viewport.diagnostics.iter())
|
||||
.filter(|diagnostic| diagnostic.starts_with("[advisory]"))
|
||||
.map(|diagnostic| redact_agent_runtime_project_paths(root, diagnostic, 240))
|
||||
.map(|diagnostic| truncate_agent_runtime_text(diagnostic, 240))
|
||||
.collect::<Vec<_>>();
|
||||
let details = result
|
||||
.viewport_results
|
||||
@@ -3670,20 +3662,20 @@ fn direct_browser_evidence_prompt(
|
||||
.map(|diagnostic| {
|
||||
format!(
|
||||
"{viewport_name} diagnostic: {}",
|
||||
redact_agent_runtime_error(root, diagnostic, 320)
|
||||
truncate_agent_runtime_text(diagnostic, 320)
|
||||
)
|
||||
}),
|
||||
);
|
||||
details.extend(viewport.console_errors.iter().take(4).map(|message| {
|
||||
format!(
|
||||
"{viewport_name} console.error: {}",
|
||||
redact_agent_runtime_error(root, &message.text, 320)
|
||||
truncate_agent_runtime_text(&message.text, 320)
|
||||
)
|
||||
}));
|
||||
details.extend(viewport.exceptions.iter().take(4).map(|exception| {
|
||||
format!(
|
||||
"{viewport_name} exception: {}",
|
||||
redact_agent_runtime_error(root, &exception.text, 320)
|
||||
truncate_agent_runtime_text(&exception.text, 320)
|
||||
)
|
||||
}));
|
||||
details.extend(
|
||||
@@ -3696,8 +3688,8 @@ fn direct_browser_evidence_prompt(
|
||||
format!(
|
||||
"{viewport_name} fatal request: {} {} ({})",
|
||||
request.method,
|
||||
redact_agent_runtime_error(root, &request.url, 320),
|
||||
redact_agent_runtime_error(root, &request.error_text, 220),
|
||||
truncate_agent_runtime_text(&request.url, 320),
|
||||
truncate_agent_runtime_text(&request.error_text, 220),
|
||||
)
|
||||
}),
|
||||
);
|
||||
@@ -3904,7 +3896,7 @@ fn write_direct_browser_acceptance_summary(
|
||||
.diagnostics
|
||||
.iter()
|
||||
.take(12)
|
||||
.map(|detail| redact_agent_runtime_project_paths(root, detail, 320))
|
||||
.map(|detail| truncate_agent_runtime_text(detail, 320))
|
||||
.collect::<Vec<_>>();
|
||||
let rendered_assets_by_viewport = evidence
|
||||
.viewport_results
|
||||
@@ -3982,7 +3974,7 @@ fn render_direct_browser_acceptance_report(
|
||||
.iter()
|
||||
.flat_map(|viewport| viewport.diagnostics.iter())
|
||||
.filter(|diagnostic| diagnostic.starts_with("[advisory]"))
|
||||
.map(|diagnostic| redact_agent_runtime_project_paths(root, diagnostic, 180))
|
||||
.map(|diagnostic| truncate_agent_runtime_text(diagnostic, 180))
|
||||
.collect::<Vec<_>>();
|
||||
let rendered_assets = evidence
|
||||
.viewport_results
|
||||
@@ -4799,7 +4791,7 @@ async fn run_direct_game_creator_turn_inner(
|
||||
if attempt < DIRECT_CODEX_ERROR_FEEDBACK_MAX_ATTEMPTS
|
||||
&& error.is_model_repairable() =>
|
||||
{
|
||||
let detail = redact_agent_runtime_error(root, &error.to_string(), 1800);
|
||||
let detail = truncate_agent_runtime_text(&error.to_string(), 1800);
|
||||
emitter.emit(
|
||||
"running",
|
||||
Some("error-feedback"),
|
||||
@@ -4868,7 +4860,7 @@ async fn run_direct_game_creator_turn_inner(
|
||||
if attempt < DIRECT_CODEX_ERROR_FEEDBACK_MAX_ATTEMPTS
|
||||
&& error.is_model_repairable() =>
|
||||
{
|
||||
let detail = redact_agent_runtime_error(root, &error.to_string(), 1800);
|
||||
let detail = truncate_agent_runtime_text(&error.to_string(), 1800);
|
||||
attempt += 1;
|
||||
feedback_prompt = direct_codex_error_feedback_prompt(&detail);
|
||||
}
|
||||
@@ -6610,7 +6602,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn completed_regeneration_replays_the_exact_redacted_lost_response() {
|
||||
async fn completed_regeneration_replays_the_exact_lost_response() {
|
||||
let root = tempfile::tempdir().expect("temp dir");
|
||||
init_local_game_project_at(root.path(), "direct-workflow-cleanup", "整包账本清理")
|
||||
.expect("init project");
|
||||
@@ -6669,8 +6661,9 @@ mod tests {
|
||||
)
|
||||
.expect("complete workflow");
|
||||
assert_eq!(completed_result.slice_warnings, vec!["切片告警保持原样"]);
|
||||
assert!(!completed_result.warnings[0].contains("secret.example"));
|
||||
assert!(!completed_result.warnings[0].contains(root.path().to_string_lossy().as_ref()));
|
||||
// 完成结果原文交回模型:URL 与项目绝对路径不再被抹掉,只做长度收口。
|
||||
assert!(completed_result.warnings[0].contains("secret.example"));
|
||||
assert!(completed_result.warnings[0].contains(root.path().to_string_lossy().as_ref()));
|
||||
assert!(stage_paths.iter().all(|path| path.is_file()));
|
||||
let persisted = read_direct_taonier_regeneration_workflow_at(root.path())
|
||||
.expect("read completed workflow")
|
||||
@@ -7528,7 +7521,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn browser_diagnostics_are_redacted_before_same_thread_repair() {
|
||||
fn browser_diagnostics_go_verbatim_to_the_model_before_same_thread_repair() {
|
||||
let root = tempfile::tempdir().expect("temp dir");
|
||||
let browser_error = format!(
|
||||
"Uncaught ReferenceError at {}?token=secret",
|
||||
@@ -7586,9 +7579,10 @@ mod tests {
|
||||
|
||||
assert!(prompt.contains("desktop exception:"));
|
||||
assert!(prompt.contains("AGC 浏览器事实证据"));
|
||||
assert!(!prompt.contains("token=secret"), "{prompt}");
|
||||
// 事实证据原文进 prompt:URL 里的 token 与项目绝对路径不再被抹掉,只做长度收口。
|
||||
assert!(prompt.contains("token=secret"), "{prompt}");
|
||||
assert!(
|
||||
!prompt.contains(&root.path().display().to_string()),
|
||||
prompt.contains(&root.path().display().to_string()),
|
||||
"{prompt}"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3406,7 +3406,7 @@ async fn bridge_update_plan(
|
||||
))
|
||||
}
|
||||
};
|
||||
let value = super::direct_delivery::update_plan(&state.root, &session, arguments)
|
||||
let value = super::direct_delivery::update_plan(&session, arguments)
|
||||
.await
|
||||
.map_err(|cause| UpdatePlanError::PlanRejected { cause })?;
|
||||
Ok(bridge_tool_result(value.to_string(), Vec::new()))
|
||||
|
||||
@@ -495,7 +495,7 @@ async fn run_browser_with_budget(
|
||||
)
|
||||
}
|
||||
Err(error) => (
|
||||
json!({"mode":input.mode,"needsReconciliation":error.starts_with("browser-cleanup-unconfirmed:"),"error":redact_agent_runtime_error(root, &error, 1800)}),
|
||||
json!({"mode":input.mode,"needsReconciliation":error.starts_with("browser-cleanup-unconfirmed:"),"error":truncate_agent_runtime_text(&error, 1800)}),
|
||||
false,
|
||||
),
|
||||
};
|
||||
@@ -585,13 +585,13 @@ pub(super) async fn run_command(root: &Path, arguments: &Value) -> Result<Value,
|
||||
(
|
||||
json!({"kind":"command", "mode":"command", "program":input.program,"args":input.arguments,"cwd":input.cwd,"purpose":input.purpose,
|
||||
"exitCode":result.exit_code,"timedOut":result.timed_out,
|
||||
"durationMs":result.duration_ms,"output":redact_agent_runtime_error(root,&result.output,12_000),
|
||||
"durationMs":result.duration_ms,"output":truncate_agent_runtime_text(&result.output,12_000),
|
||||
"outputTruncated":result.capture_truncated,"coverage":"command-exit-status"}),
|
||||
passed,
|
||||
)
|
||||
}
|
||||
Err(error) => (
|
||||
json!({"mode":"command","needsReconciliation":error.needs_reconciliation(),"error":redact_agent_runtime_error(root,&error.to_string(),1800)}),
|
||||
json!({"mode":"command","needsReconciliation":error.needs_reconciliation(),"error":truncate_agent_runtime_text(&error.to_string(),1800)}),
|
||||
false,
|
||||
),
|
||||
};
|
||||
|
||||
+3
-6
@@ -632,13 +632,10 @@ pub(in crate::agent) fn build_game_creator_agent_background_final_reply_request(
|
||||
)?;
|
||||
let observations_json = serde_json::to_string_pretty(&prompt_observations)
|
||||
.map_err(|error| format!("序列化 Agent 工具观察失败:{error}"))?;
|
||||
// 回灌给模型的是它自己上一轮的计划原文,只做长度收口;脱敏只发生在写审计/日志时。
|
||||
let plan_json = serde_json::to_string_pretty(&serde_json::json!({
|
||||
"thinkingSummary": redact_agent_runtime_project_paths(
|
||||
root,
|
||||
&plan.thinking_summary,
|
||||
240,
|
||||
),
|
||||
"fallbackResponse": redact_agent_runtime_project_paths(root, &plan.response, 1_200),
|
||||
"thinkingSummary": truncate_agent_runtime_text(&plan.thinking_summary, 240),
|
||||
"fallbackResponse": truncate_agent_runtime_text(&plan.response, 1_200),
|
||||
}))
|
||||
.map_err(|error| format!("序列化 Agent 收束摘要失败:{error}"))?;
|
||||
let steers_json =
|
||||
|
||||
Reference in New Issue
Block a user