把审批卡的修改/退回意见原文送进 Supervisor 会话
用户在审批卡上写的意见此前只落到 `.agent/planning/approvals/v{n}.json` 就断了。
Supervisor 唯一的信息源是 `agent.run_status` 返回的 claimedDelegateContract,
那里面只有 contractStatus=user-revision-requested,没有承载原文的字段。于是它
按 playbook 第 6 条「把用户原话完整附在 task 里」写了句占位「请按审批卡上用户
提交的修改意见…」,子 Agent 收到的是空指令,只能自由发挥——实测用户写「把游戏
名称改成日本语」,产出的 v2 把标题从《裂隙脉冲》改成《裂潮航印》,仍是中文。
通道本来就有:决策卡的答案早就是这么送的——`append_user_input_answer_message`
把用户选择渲成一句 role=user 消息追加到 Supervisor 会话,下一轮 prompt 由
`prompt_history_sources` 现读现取。审批决定接上同一条通道即可。
因此不动 delivery schema,也不动 playbook:
- schema 加字段要连带改 validator、run_status 投影,还得让 e43133875 的
`static_delegate_structured_result_follows_claim_snapshot` 跟着 rebase 新字段,
否则 claim 重放冲突原样复发——多一处必须手工同步的地方。
- playbook 第 6 条的规则本来就在,缺的是原文本身,不是规则。
落点用 Supervisor 的**当前活动会话**(sessionId 传 None),不用 delivery 上的
parentSessionId——那是委派发出时的快照,不保证仍是可写的活动会话。
messageId 用 responseId 派生:`project_receipt_locked` 会被
`reconcile_plan_gdd_approval_projections_locked` 在每次 hydrate 重跑,不幂等就
每刷新一次多一条。
守门一条,两个断言都验过非空转:去掉追加,第一条断言红;换成非幂等 append,
重放后消息数 2 vs 1,第二条断言红。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -74,6 +74,42 @@ fn approval_observation(receipt: &PlanGddApprovalV1) -> AgentRuntimeToolObservat
|
||||
}
|
||||
}
|
||||
|
||||
/// 审批卡上的「修改/退回」是用户说的话,落点和决策卡的答案一样:Supervisor 自己
|
||||
/// 的会话文件。`append_user_input_answer_message` 已经为澄清答案建立了这条通道,
|
||||
/// 审批决定沿用它。没有这一步 Supervisor 只能从 delivery 的
|
||||
/// `contractStatus=user-revision-requested` 知道「用户要改」,读不到要改什么——
|
||||
/// playbook 第 6 条的「把用户原话完整附在 task 里」就没有原话可附,返工委派只能
|
||||
/// 写一句占位,子 Agent 于是自由发挥。
|
||||
fn append_plan_gdd_revision_message(
|
||||
root: &Path,
|
||||
receipt: &PlanGddApprovalV1,
|
||||
) -> Result<(), String> {
|
||||
let label = if receipt.action == "reject" {
|
||||
"退回"
|
||||
} else {
|
||||
"修改"
|
||||
};
|
||||
append_local_conversation_message_for_session_idempotent_at(
|
||||
root,
|
||||
Some(GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID),
|
||||
// 落在 Supervisor 当前活动会话。delivery 上的 parentSessionId 是委派发出时
|
||||
// 的快照,不保证仍是可写的活动会话。
|
||||
None,
|
||||
LocalConversationMessage {
|
||||
role: "user".to_string(),
|
||||
content: format!(
|
||||
"我对 Fast GDD v{} 的审批:{}。意见原文:\n{}",
|
||||
receipt.version,
|
||||
label,
|
||||
receipt.comment.as_deref().unwrap_or_default()
|
||||
),
|
||||
agent_id: None,
|
||||
},
|
||||
&format!("plan-gdd-decision-{}", receipt.response_id),
|
||||
)
|
||||
.map(|_| ())
|
||||
}
|
||||
|
||||
fn receipt_decision_input(
|
||||
gdd: &PlanGddV1,
|
||||
input: &DecidePlanGddInputV1,
|
||||
@@ -1149,6 +1185,10 @@ fn project_receipt_locked(
|
||||
note_plan_gdd_projection_gap(root, receipt, "delivery-revision-mark", &error);
|
||||
recovery_pending = true;
|
||||
}
|
||||
if let Err(error) = append_plan_gdd_revision_message(root, receipt) {
|
||||
note_plan_gdd_projection_gap(root, receipt, "delivery-revision-message", &error);
|
||||
recovery_pending = true;
|
||||
}
|
||||
}
|
||||
// A replay may target an older receipt after a newer GDD has already been
|
||||
// submitted. The receipt still repairs its own audit/observation, but it
|
||||
|
||||
@@ -4192,6 +4192,51 @@ mod tests {
|
||||
cleanup_fixture(root);
|
||||
}
|
||||
|
||||
/// 审批卡上的「修改/退回」意见必须原文落进 Supervisor 会话——那是 playbook
|
||||
/// 第 6 条「把用户原话完整附在 task 里」唯一的原话来源。delivery 的
|
||||
/// `contractStatus=user-revision-requested` 只说明「用户要改」,不带内容,
|
||||
/// Supervisor 拿不到原文就只能在返工委派里写一句占位。
|
||||
///
|
||||
/// 追加必须幂等:`reconcile_plan_gdd_approval_projections_locked` 每次 hydrate
|
||||
/// 都会为全部回执重跑 `project_receipt_locked`。
|
||||
#[test]
|
||||
fn a_revision_comment_reaches_the_supervisor_conversation_once() {
|
||||
let (root, gdd, _root_runtime) = acceptance_gate_fixture(true);
|
||||
create_plan_gdd_approval_pending_at(&root, &gdd).expect("create approval pending");
|
||||
decide_plan_gdd_at(
|
||||
&root,
|
||||
&approval_input(
|
||||
&gdd,
|
||||
"revise",
|
||||
"gdd-response-00000000-0000-4000-8000-000000000050",
|
||||
Some("把游戏名称改成日本语".to_string()),
|
||||
),
|
||||
)
|
||||
.expect("commit revise receipt");
|
||||
let supervisor_messages = || {
|
||||
read_local_conversation_for_session_at(
|
||||
&root,
|
||||
Some(GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID),
|
||||
None,
|
||||
)
|
||||
.expect("read supervisor conversation")
|
||||
.messages
|
||||
};
|
||||
let after_decision = supervisor_messages();
|
||||
assert!(
|
||||
after_decision.iter().any(|message| message.role == "user"
|
||||
&& message.content.contains("把游戏名称改成日本语")),
|
||||
"用户修改意见必须原文进入 Supervisor 会话"
|
||||
);
|
||||
reconcile_plan_gdd_approval_projections_locked(&root).expect("replay receipt projections");
|
||||
assert_eq!(
|
||||
supervisor_messages().len(),
|
||||
after_decision.len(),
|
||||
"投影重放不得重复追加同一条审批意见"
|
||||
);
|
||||
cleanup_fixture(root);
|
||||
}
|
||||
|
||||
/// reject 之后能不能在同一 lineage 重做,**不由提交门的 phase 判据决定**。
|
||||
///
|
||||
/// 提交门要求 session 的 activeRunId 等于当前策划子 run,而 schema 不变量禁止
|
||||
|
||||
Reference in New Issue
Block a user