坏信封在 run 内就地重取,不再逃逸成一条委派
按原型(local-scripts/deisgn_agent)的口径:信封解析失败是「本回合没推进 流程」,注入原因后在同一个 run 里重来,而不是终止 run 让它落成一条 needs-repair delivery。 接入点在 final reply 返回处。解析失败时把原因作为 observation 回灌,用带 后缀的 request slot 重取 final reply,上限 2 次(对齐原型 MAX_WASTED_TURNS=3:前两次注入重试,第三次放行)。用尽后按既有路径落盘, 此时「连写三次都不合法」确实是质量问题,按普通质量返工计费与返工深度、 澄清轮次、session 段落三套不变量全部自洽,不需要放松任何一道。 只覆盖 project-planning。做游戏 / 做素材的静态委派子 Agent 逐字保持既有 行为,它们的坏信封仍按原路落成 needs-repair。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,30 @@
|
||||
use super::*;
|
||||
|
||||
/// 策划子 Agent 的 `AGC_NEEDS_USER_INPUT_V1` 信封在 final reply 里写坏时,允许在
|
||||
/// **同一个 run 内**重取几次。
|
||||
///
|
||||
/// 原型(local-scripts/deisgn_agent)把信封解析失败当成「本回合没推进流程」,注入
|
||||
/// 错误后在同一条 messages 上重来,`MAX_WASTED_TURNS=3`(前两次注入重试,第三次判
|
||||
/// 本跳失败)。这里取同一口径:2 次重取,用尽后才让它按既有路径落成 needs-repair。
|
||||
///
|
||||
/// 为什么必须在这里拦:信封一旦随 final reply 逃逸,run 就终止并变成一条
|
||||
/// needs-repair delivery,之后返工深度、澄清轮次、session 段落三套不变量都会把它
|
||||
/// 当成「新段落」,而它们编码的是同一条假设——新委派 = 新段落。实测一次字节级截断
|
||||
/// 就能吃掉整条委派唯一的返工额度,两次则直接把父 run 打进 needs-reconciliation。
|
||||
const AGENT_RUNTIME_PLAN_ENVELOPE_REPAIR_ATTEMPTS: u32 = 2;
|
||||
|
||||
/// 返回本条 final reply 里坏掉的信封的解析原因;不是策划子 Agent、或正文压根没有
|
||||
/// 信封首行时返回 None(后者是「这一轮不提问」的正常收束)。
|
||||
fn game_creator_agent_runtime_plan_envelope_parse_error(
|
||||
agent_id: &str,
|
||||
reply: &str,
|
||||
) -> Option<String> {
|
||||
if agent_id != GAME_CREATOR_PROJECT_PLANNING_AGENT_ID {
|
||||
return None;
|
||||
}
|
||||
parse_static_delegate_user_input_request(Some(reply)).err()
|
||||
}
|
||||
|
||||
struct PlanGddBlockerRuntimeProjection {
|
||||
phase: &'static str,
|
||||
current_action: &'static str,
|
||||
@@ -4274,20 +4299,64 @@ async fn run_game_creator_agent_background_task_pass_without_deadline(
|
||||
&format!("持久化最终回复请求上下文失败:{error}"),
|
||||
);
|
||||
}
|
||||
let final_reply_result = request_game_creator_agent_background_final_reply_at(
|
||||
&root,
|
||||
&agent_id,
|
||||
&runtime.session_id,
|
||||
&runtime.run_id,
|
||||
&task,
|
||||
&plan,
|
||||
final_reply_fallback.as_deref(),
|
||||
&observations,
|
||||
runtime.applied_steer_cursor,
|
||||
&final_reply_request_slot,
|
||||
response_revision,
|
||||
)
|
||||
.await;
|
||||
// 坏信封在这里就地重取,不让它随 final reply 逃逸成一条 needs-repair 委派。
|
||||
// 与 provider_tool_plan 对工具计划协议错误的定向修复同构:把解析原因作为
|
||||
// observation 回灌,下一次 final-reply 请求就带着它。
|
||||
let mut envelope_repair_attempt = 0u32;
|
||||
let final_reply_result = loop {
|
||||
let attempt_slot = if envelope_repair_attempt == 0 {
|
||||
final_reply_request_slot.clone()
|
||||
} else {
|
||||
format!("{final_reply_request_slot}-envelope-repair-{envelope_repair_attempt}")
|
||||
};
|
||||
let attempt_result = request_game_creator_agent_background_final_reply_at(
|
||||
&root,
|
||||
&agent_id,
|
||||
&runtime.session_id,
|
||||
&runtime.run_id,
|
||||
&task,
|
||||
&plan,
|
||||
final_reply_fallback.as_deref(),
|
||||
&observations,
|
||||
runtime.applied_steer_cursor,
|
||||
&attempt_slot,
|
||||
response_revision,
|
||||
)
|
||||
.await;
|
||||
let parse_error = match &attempt_result {
|
||||
Ok(RequestedAgentRuntimeFinalReplyOutcome::Ready(Some(requested_reply))) => {
|
||||
game_creator_agent_runtime_plan_envelope_parse_error(
|
||||
&agent_id,
|
||||
&requested_reply.reply,
|
||||
)
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
let Some(parse_error) = parse_error else {
|
||||
break attempt_result;
|
||||
};
|
||||
if envelope_repair_attempt >= AGENT_RUNTIME_PLAN_ENVELOPE_REPAIR_ATTEMPTS {
|
||||
break attempt_result;
|
||||
}
|
||||
envelope_repair_attempt += 1;
|
||||
let observation = AgentRuntimeToolObservation {
|
||||
tool: "runtime.plan_envelope".to_string(),
|
||||
status: "failed".to_string(),
|
||||
summary: format!(
|
||||
"AGC_NEEDS_USER_INPUT_V1 信封无法解析(第 {envelope_repair_attempt}/{AGENT_RUNTIME_PLAN_ENVELOPE_REPAIR_ATTEMPTS} 次重取):{parse_error}"
|
||||
),
|
||||
detail: Some(
|
||||
"原样重出同一个问题,不要改写题面或选项;信封必须是首行 AGC_NEEDS_USER_INPUT_V1,下一行严格 JSON 且完整闭合到最外层。不要输出 markdown、代码围栏或第三行正文。".to_string(),
|
||||
),
|
||||
};
|
||||
let observation_summary = observation.summary();
|
||||
runtime.observations.push(observation_summary);
|
||||
context_tracker.record(&observation);
|
||||
observations.push(observation);
|
||||
if stop_game_creator_agent_runtime_if_cancel_requested(&root, &mut runtime) {
|
||||
return AgentBackgroundTaskOutcome::Finished;
|
||||
}
|
||||
};
|
||||
if stop_game_creator_agent_runtime_if_cancel_requested(&root, &mut runtime) {
|
||||
return AgentBackgroundTaskOutcome::Finished;
|
||||
}
|
||||
@@ -4565,6 +4634,61 @@ async fn run_game_creator_agent_background_task_pass_without_deadline(
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod plan_envelope_repair_tests {
|
||||
use super::*;
|
||||
|
||||
const TRUNCATED: &str = "AGC_NEEDS_USER_INPUT_V1
|
||||
{\"questions\":[{\"id\":\"core_loop\",\"header\":\"第1轮·关键决定\",\"question\":\"当前要决定:?\",\"options\":[{\"label\":\"A\",\"description\":\"甲\"}]}";
|
||||
const COMPLETE: &str = "AGC_NEEDS_USER_INPUT_V1
|
||||
{\"questions\":[{\"id\":\"core_loop\",\"header\":\"第1轮·关键决定\",\"question\":\"当前要决定:?\",\"options\":[{\"label\":\"A\",\"description\":\"甲\"},{\"label\":\"B\",\"description\":\"乙\"}]}]}";
|
||||
|
||||
/// 截断的信封必须在 run 内被认出来,否则它会随 final reply 逃逸成一条
|
||||
/// needs-repair 委派,把返工额度和澄清轮次一起卷进去。
|
||||
#[test]
|
||||
fn a_truncated_planning_envelope_is_detected_before_the_run_ends() {
|
||||
let error = game_creator_agent_runtime_plan_envelope_parse_error(
|
||||
GAME_CREATOR_PROJECT_PLANNING_AGENT_ID,
|
||||
TRUNCATED,
|
||||
)
|
||||
.expect("truncated envelope must be reported");
|
||||
assert!(error.contains("JSON"), "{error}");
|
||||
}
|
||||
|
||||
/// 完整信封与普通收尾文本都不能触发重取——后者是「这一轮不提问」的正常终态。
|
||||
#[test]
|
||||
fn complete_envelopes_and_plain_replies_do_not_trigger_a_repair() {
|
||||
assert!(game_creator_agent_runtime_plan_envelope_parse_error(
|
||||
GAME_CREATOR_PROJECT_PLANNING_AGENT_ID,
|
||||
COMPLETE
|
||||
)
|
||||
.is_none());
|
||||
assert!(game_creator_agent_runtime_plan_envelope_parse_error(
|
||||
GAME_CREATOR_PROJECT_PLANNING_AGENT_ID,
|
||||
"已按默认建议补齐剩余空白,GDD 已提交待审批。"
|
||||
)
|
||||
.is_none());
|
||||
}
|
||||
|
||||
/// 只覆盖立项策划链路。做游戏 / 做素材的静态委派子 Agent 逐字保持既有行为:
|
||||
/// 它们的坏信封仍旧按原路落成 needs-repair,不在这里被拦下重取。
|
||||
#[test]
|
||||
fn other_agents_keep_the_existing_escape_path() {
|
||||
for agent_id in ["design-director", "art-director", "project-supervisor"] {
|
||||
assert!(
|
||||
game_creator_agent_runtime_plan_envelope_parse_error(agent_id, TRUNCATED).is_none(),
|
||||
"{agent_id} 不应进入策划信封重取"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 重取次数取原型的 MAX_WASTED_TURNS=3 口径:前两次注入重来,第三次放行落盘。
|
||||
#[test]
|
||||
fn the_repair_budget_matches_the_prototype() {
|
||||
assert_eq!(AGENT_RUNTIME_PLAN_ENVELOPE_REPAIR_ATTEMPTS, 2);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod plan_gdd_blocker_projection_tests {
|
||||
use super::*;
|
||||
|
||||
@@ -1812,7 +1812,7 @@ pub(crate) fn build_static_delegate_structured_result_at(
|
||||
})
|
||||
}
|
||||
|
||||
fn parse_static_delegate_user_input_request(
|
||||
pub(crate) fn parse_static_delegate_user_input_request(
|
||||
response: Option<&str>,
|
||||
) -> Result<(Option<Vec<AgentRuntimeUserInputQuestion>>, Option<String>), String> {
|
||||
let Some(response) = response.map(str::trim).filter(|value| !value.is_empty()) else {
|
||||
|
||||
Reference in New Issue
Block a user