澄清卡不再叠一张通用确认卡,策划根委派免确认
子 Agent 要提问时,Runtime 在 parent-wake 屏障处把问题包成 user.input_request pending(ensure_static_delegate_user_input_wait_at_locked),同一份问题再投影成 userInputRequest。这个 pending 只是问题的载体,user.input_request 也从来不在 GAME_CREATION_APP_COMMANDS 的 confirm 集合里,没有任何确认语义。但三个渲染面都 无条件把 pendingToolAction 也画成通用待确认卡,于是同一个请求出现两遍:上面是问答 卡,下面是拿 tool 名当标题、拿 questionCount/questionsSha256 这种取证摘要当副标题 的确认卡,还和问答卡在聊天视图里糊在一起。 加 agentRuntimePendingActionIsUserInputCarrier,在开发者 Runtime 面板、项目总控 概览卡和聊天视图三处统一把通用确认面判掉,只留问答卡。 立项策划根 Run 的 agent.delegate 同时改为免确认。plan 根的工具面按阶段收窄到「当前 唯一能推进链路的动作」,Delegate 阶段就只广告 agent.delegate 一个工具,让用户确认 「要不要执行唯一能做的那件事」没有决策含量,返工那一轮同理;这条链上真正由人把关的 关口是 §13.0 的 Fast GDD 审批卡,不动。source 只作弱判据,命中后必须过 validate_project_supervisor_plan_root_binding_at 强判据,否则做游戏那条根的委派确认 会被漂移或伪造的 binding 悄悄放开;绑定读盘只发生在本来就要确认的 agent.delegate 这一个组合上。 新增两条用例:Rust 侧一起断言 plan 根 delegate 免确认、plan 根上 agent.spawn_isolated 照旧确认、gui 根 delegate 照旧确认;前端侧断言载体 pending 只渲染问答卡,且 user.input_request 与 questionsSha256 都不出现在界面上。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -106,6 +106,35 @@ pub(crate) fn game_creator_agent_runtime_tool_policy_rule_for_run(
|
||||
if matches!(blocked, Some(AgentRuntimeToolPolicyBlock::Denied(_))) {
|
||||
return blocked;
|
||||
}
|
||||
// 立项策划根 Run 的委派不再要人工确认。plan 根的工具面本身按阶段收窄
|
||||
// (`agent_runtime_plan_root_supervisor_tools_for_stage`):Delegate 阶段只广告
|
||||
// `agent.delegate` 这一个工具,它就是当前唯一能推进链路的动作;让用户确认「要不要
|
||||
// 执行唯一能做的那件事」没有决策含量,返工那一轮同理。这条链上真正由人把关的关口
|
||||
// 是 §13.0 的 Fast GDD 审批卡,那个不动。
|
||||
//
|
||||
// source 只是弱判据,一旦它说 plan 就必须过 `validate_project_supervisor_plan_root_binding_at`
|
||||
// 这个强判据——否则做游戏那条链的委派确认会被漂移或伪造的 binding 悄悄放开。绑定
|
||||
// 读盘只发生在「本来就要确认的 agent.delegate」这一个组合上,不给其它工具的每次
|
||||
// 策略判定加磁盘读。
|
||||
if command_id == "agent.delegate"
|
||||
&& matches!(
|
||||
blocked,
|
||||
Some(AgentRuntimeToolPolicyBlock::RequiresConfirmation(_))
|
||||
)
|
||||
{
|
||||
match read_game_creator_agent_runtime_run_profile_binding(root, agent_id, run_id) {
|
||||
Ok(Some(binding)) if binding.source == AGENT_RUNTIME_SUPERVISOR_PLAN_SOURCE => {
|
||||
if let Err(error) =
|
||||
validate_project_supervisor_plan_root_binding_at(root, agent_id, run_id)
|
||||
{
|
||||
return Some(AgentRuntimeToolPolicyBlock::Denied(error));
|
||||
}
|
||||
return None;
|
||||
}
|
||||
Ok(_) => {}
|
||||
Err(error) => return Some(AgentRuntimeToolPolicyBlock::Denied(error)),
|
||||
}
|
||||
}
|
||||
let (run_profile, _) = match agent_runtime_run_profile_identity_at(
|
||||
root,
|
||||
agent_id,
|
||||
|
||||
@@ -714,6 +714,95 @@ fn gui_root_delegate_and_spawn_isolated_are_unaffected_by_plan_root_symmetry() {
|
||||
fs::remove_dir_all(root).ok();
|
||||
}
|
||||
|
||||
/// 立项策划根 Run 的委派免确认,并且这条豁免既不外溢到做游戏那条根,也不外溢到别的工具。
|
||||
///
|
||||
/// plan 根的工具面按阶段收窄到「当前唯一能推进链路的动作」,Delegate 阶段就只有
|
||||
/// `agent.delegate`;再要用户点一次确认没有决策含量,人的关口留在 Fast GDD 审批卡。
|
||||
/// 判据取自持久 binding 的强判据,所以这里连同 gui 根一起断言:同一份策略下,做游戏的
|
||||
/// 委派必须照旧停在待确认。
|
||||
#[test]
|
||||
fn plan_root_delegate_is_auto_while_other_roots_and_tools_still_confirm() {
|
||||
let root = unique_project_path();
|
||||
init_local_game_project_at(&root, "plan-root-delegate-auto", "立项策划根委派免确认")
|
||||
.expect("project init");
|
||||
write_project_permission_policy_at(
|
||||
&root,
|
||||
ProjectPermissionPolicy {
|
||||
denied_commands: Vec::new(),
|
||||
confirm_commands: vec![
|
||||
"agent.delegate".to_string(),
|
||||
"agent.spawn_isolated".to_string(),
|
||||
],
|
||||
agent_policies: BTreeMap::new(),
|
||||
},
|
||||
)
|
||||
.expect("write policy");
|
||||
|
||||
let plan_run_id = "plan-root-delegate-run";
|
||||
bind_game_creator_agent_runtime_run_profile_at(
|
||||
&root,
|
||||
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
|
||||
plan_run_id,
|
||||
AGENT_RUNTIME_SUPERVISOR_PLAN_SOURCE,
|
||||
Some(AGENT_RUNTIME_RUN_PROFILE_STANDARD),
|
||||
None,
|
||||
)
|
||||
.expect("bind plan root");
|
||||
assert!(
|
||||
game_creator_agent_runtime_tool_policy_rule_for_run(
|
||||
&root,
|
||||
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
|
||||
plan_run_id,
|
||||
None,
|
||||
None,
|
||||
"agent.delegate",
|
||||
)
|
||||
.is_none(),
|
||||
"立项策划根的 agent.delegate 不应再要人工确认"
|
||||
);
|
||||
assert!(
|
||||
matches!(
|
||||
game_creator_agent_runtime_tool_policy_rule_for_run(
|
||||
&root,
|
||||
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
|
||||
plan_run_id,
|
||||
None,
|
||||
None,
|
||||
"agent.spawn_isolated",
|
||||
),
|
||||
Some(AgentRuntimeToolPolicyBlock::RequiresConfirmation(_))
|
||||
),
|
||||
"免确认只给 agent.delegate,plan 根上其它 confirm 工具必须照旧"
|
||||
);
|
||||
|
||||
let gui_run_id = "gui-root-delegate-run";
|
||||
bind_game_creator_agent_runtime_run_profile_at(
|
||||
&root,
|
||||
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
|
||||
gui_run_id,
|
||||
AGENT_RUNTIME_SUPERVISOR_GUI_SOURCE,
|
||||
Some(AGENT_RUNTIME_RUN_PROFILE_STANDARD),
|
||||
None,
|
||||
)
|
||||
.expect("bind gui root");
|
||||
assert!(
|
||||
matches!(
|
||||
game_creator_agent_runtime_tool_policy_rule_for_run(
|
||||
&root,
|
||||
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
|
||||
gui_run_id,
|
||||
None,
|
||||
None,
|
||||
"agent.delegate",
|
||||
),
|
||||
Some(AgentRuntimeToolPolicyBlock::RequiresConfirmation(_))
|
||||
),
|
||||
"做游戏那条根的委派确认不能被 plan 根的豁免带走"
|
||||
);
|
||||
|
||||
fs::remove_dir_all(root).ok();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn background_agent_runtime_asset_generation_respects_project_policy() {
|
||||
let root = unique_project_path();
|
||||
|
||||
@@ -857,6 +857,23 @@ export function agentRuntimeNeedsUserInput(
|
||||
);
|
||||
}
|
||||
|
||||
export const AGENT_RUNTIME_USER_INPUT_REQUEST_TOOL = 'user.input_request';
|
||||
|
||||
// `user.input_request` 的 pending 不是等待批准的动作,而是问题本身的载体:子 Agent 以
|
||||
// needs-user-input 终态退出后,Runtime 在 parent-wake 屏障处按信封原文构造这个 pending
|
||||
// (`ensure_static_delegate_user_input_wait_at_locked`),同一份问题再投影成
|
||||
// `userInputRequest`。它也从来不在 `GAME_CREATION_APP_COMMANDS` 的 confirm 集合里,
|
||||
// 没有任何「确认/拒绝」语义。
|
||||
//
|
||||
// 通用待确认卡按 `tool` + `inputSummary` 渲染,套到它身上就是把同一个请求画两遍——标题
|
||||
// 是原始工具名,副标题是 `questionCount=… · questionsSha256=…` 这种给日志看的取证摘要。
|
||||
// 真正的交互面是问答卡,所以这里统一把通用确认面判掉。
|
||||
export function agentRuntimePendingActionIsUserInputCarrier(
|
||||
action: AgentRuntimePendingToolActionSummary | null | undefined,
|
||||
) {
|
||||
return action?.tool === AGENT_RUNTIME_USER_INPUT_REQUEST_TOOL;
|
||||
}
|
||||
|
||||
export function matchingAgentRuntimeForSteer(
|
||||
runtimes: Array<AgentRuntimeState | null | undefined>,
|
||||
agentId: string,
|
||||
|
||||
@@ -29,6 +29,7 @@ import {
|
||||
agentRuntimeCanRetry,
|
||||
agentRuntimeNeedsUserInput,
|
||||
agentRuntimeNextStepFromPhase,
|
||||
agentRuntimePendingActionIsUserInputCarrier,
|
||||
agentRuntimePlanStepText,
|
||||
agentRuntimeWaitingOnFromPhase,
|
||||
createAgentRuntimeUserInputResponseId,
|
||||
@@ -398,14 +399,18 @@ export function AgentRuntimeStatusPanel({
|
||||
!agentGoalStatusIsPaused(runtime.phase) &&
|
||||
!pendingToolAction &&
|
||||
Boolean(onRetryRuntimeTask);
|
||||
const pendingActionIsUserInputCarrier =
|
||||
agentRuntimePendingActionIsUserInputCarrier(pendingToolAction);
|
||||
const canConfirm =
|
||||
Boolean(runtime.runId) &&
|
||||
Boolean(pendingToolAction?.actionId) &&
|
||||
!pendingActionIsUserInputCarrier &&
|
||||
agentRuntimeCanConfirm(runtime.status) &&
|
||||
Boolean(onConfirmRuntimeTask);
|
||||
const canReject =
|
||||
Boolean(runtime.runId) &&
|
||||
Boolean(pendingToolAction?.actionId) &&
|
||||
!pendingActionIsUserInputCarrier &&
|
||||
agentRuntimeCanConfirm(runtime.status) &&
|
||||
Boolean(onRejectRuntimeTask);
|
||||
const canCompact =
|
||||
@@ -776,6 +781,8 @@ export function ProjectSupervisorRuntimePanel({
|
||||
const pendingActionPresentation = pendingToolAction
|
||||
? projectSupervisorPendingActionPresentation(pendingToolAction)
|
||||
: null;
|
||||
const pendingActionIsUserInputCarrier =
|
||||
agentRuntimePendingActionIsUserInputCarrier(pendingToolAction);
|
||||
const userInputRequest = runtime?.userInputRequest ?? null;
|
||||
const needsUserInput = agentRuntimeNeedsUserInput(runtime);
|
||||
const needsSupervisorReconciliation = Boolean(
|
||||
@@ -953,6 +960,7 @@ export function ProjectSupervisorRuntimePanel({
|
||||
) : null}
|
||||
{pendingToolAction &&
|
||||
pendingActionPresentation &&
|
||||
!pendingActionIsUserInputCarrier &&
|
||||
!readOnly &&
|
||||
!needsSupervisorReconciliation ? (
|
||||
<div
|
||||
@@ -1287,9 +1295,14 @@ export function ProjectSupervisorRuntimeControls({
|
||||
) => void | Promise<void>;
|
||||
}) {
|
||||
const pendingToolAction = runtime?.pendingToolAction ?? null;
|
||||
const confirmableToolAction = agentRuntimePendingActionIsUserInputCarrier(
|
||||
pendingToolAction,
|
||||
)
|
||||
? null
|
||||
: pendingToolAction;
|
||||
const userInputRequest = runtime?.userInputRequest ?? null;
|
||||
const needsUserInput = agentRuntimeNeedsUserInput(runtime);
|
||||
if (!pendingToolAction && !userInputRequest && !needsUserInput) {
|
||||
if (!confirmableToolAction && !userInputRequest && !needsUserInput) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -1307,12 +1320,12 @@ export function ProjectSupervisorRuntimeControls({
|
||||
待回答问题未能读取,请稍后重试。
|
||||
</p>
|
||||
) : null}
|
||||
{pendingToolAction ? (
|
||||
{confirmableToolAction ? (
|
||||
<div className="pending-command" aria-label="项目总控 Agent 待确认动作">
|
||||
<span>
|
||||
{pendingToolAction.tool}
|
||||
{pendingToolAction.inputSummary ? (
|
||||
<small>{pendingToolAction.inputSummary}</small>
|
||||
{confirmableToolAction.tool}
|
||||
{confirmableToolAction.inputSummary ? (
|
||||
<small>{confirmableToolAction.inputSummary}</small>
|
||||
) : null}
|
||||
</span>
|
||||
<button
|
||||
|
||||
@@ -730,6 +730,52 @@ export function registerSupervisorRuntimeTests() {
|
||||
).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('renders only the question card when the pending action is the user input carrier', async () => {
|
||||
const runId = 'supervisor-user-input-carrier-run';
|
||||
const request = agentRuntimeUserInputRequest({
|
||||
agentId: 'project-supervisor',
|
||||
sessionId: 'supervisor-session-active',
|
||||
runId,
|
||||
actionId: 'action-user-input-carrier',
|
||||
});
|
||||
// Runtime 在 parent-wake 屏障处把子 Agent 的澄清问题包成 user.input_request pending,
|
||||
// 同一份问题再投影成 userInputRequest。pending 只是载体,没有确认语义,通用待确认卡
|
||||
// 套上去就是把同一个请求画两遍,还会把 questionsSha256 这类取证摘要推到用户面前。
|
||||
const harness = createProjectSupervisorRuntimeHarness({
|
||||
initialRuntime: {
|
||||
runId,
|
||||
status: 'waiting-for-user-input',
|
||||
phase: 'waiting-for-user-input',
|
||||
currentTask: '准备首版角色规范图',
|
||||
currentAction: '等待用户补充关键信息',
|
||||
waitingOn: '你的澄清回答',
|
||||
nextStep: '提交全部回答后继续同一 Run',
|
||||
userInputRequest: request,
|
||||
pendingToolAction: {
|
||||
actionId: request.actionId,
|
||||
actionFingerprint: 'fingerprint-user-input-carrier',
|
||||
tool: 'user.input_request',
|
||||
inputSummary:
|
||||
'questionCount=1 · optionCount=2 · questionChars=302 · questionsSha256=6aecb0cf',
|
||||
reason: '代 Supervisor 汇总子 Agent 的澄清问题',
|
||||
requestedAt: 6000,
|
||||
},
|
||||
updatedAt: 6000,
|
||||
},
|
||||
});
|
||||
window.__TAURI__ = {
|
||||
core: { invoke: harness.invoke },
|
||||
event: { listen: harness.listen },
|
||||
};
|
||||
renderAppAt('/');
|
||||
await openMainProject(harness.projectPath);
|
||||
|
||||
await screen.findByLabelText('Needs input');
|
||||
expect(screen.queryByLabelText('项目总控 Agent 待确认动作')).toBeNull();
|
||||
expect(screen.queryByText('user.input_request')).toBeNull();
|
||||
expect(screen.queryByText(/questionsSha256=/)).toBeNull();
|
||||
});
|
||||
|
||||
it('recovers a Project Supervisor transient reply from runtime polling without persisting it', async () => {
|
||||
const runId = 'supervisor-response-stream-recovery-run';
|
||||
const initialRuntime = {
|
||||
|
||||
Reference in New Issue
Block a user