GDD 审批卡不再被两张伪卡压住
立项策划提交 Fast GDD 后,界面上同时冒出三样东西:一句「待回答问题未能读取,请稍后 重试」、一张 plan.submit_gdd 的拒绝/确认卡,以及被压在下面的 GddApprovalCard。前两样 都是伪的。 plan.submit_gdd 的 pending 是审批卡的载体,不是等待批准的动作——planning/pending.json 的 submission.pendingActionId 指向的正是它,交互面是审批卡的批准/修改/退回。它和 user.input_request 是同一性质,而且运行时策略明确拒绝把它转成通用确认 pending (「Runtime-owned create-only 提交」),那个确认按钮点下去必然失败。所以把载体判据从只认 user.input_request 扩成两个工具都认,并改名 agentRuntimePendingActionHasDedicatedCard。 同时补上第四个渲染面:协作 Agent 列表也无条件把 pendingToolAction 画成通用确认卡,上一次 只改了开发者面板、总控概览卡和聊天视图三处。 「待回答问题未能读取」则是另一回事:审批等待复用了 waiting-for-user-input 这个 phase,但 它没有 userInputRequest,只看 phase 就会把一次正常的等待报成读取失败。给 ProjectSupervisorRuntimePanel 加一个 planGddAwaitingDecision 入参,由两个调用处按 planGddState.pendingApproval 传下来,等审批时不再报这句错。 新增用例同时断言三条:审批未决时不出现「待回答问题未能读取」、不出现 plan.submit_gdd 确认卡、审批卡的批准按钮在位。两条判据都做过 A/B——各自关掉后对应的伪卡都会冒出来。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -858,20 +858,34 @@ export function agentRuntimeNeedsUserInput(
|
||||
}
|
||||
|
||||
export const AGENT_RUNTIME_USER_INPUT_REQUEST_TOOL = 'user.input_request';
|
||||
export const AGENT_RUNTIME_PLAN_SUBMIT_GDD_TOOL = 'plan.submit_gdd';
|
||||
|
||||
// `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 集合里,
|
||||
// 没有任何「确认/拒绝」语义。
|
||||
// 有两个 pending 动作不是「等待批准的动作」,而是某张专用卡的载体:Runtime 把内容停在
|
||||
// pending 上,真正的交互面是另一个组件。
|
||||
//
|
||||
// 通用待确认卡按 `tool` + `inputSummary` 渲染,套到它身上就是把同一个请求画两遍——标题
|
||||
// 是原始工具名,副标题是 `questionCount=… · questionsSha256=…` 这种给日志看的取证摘要。
|
||||
// 真正的交互面是问答卡,所以这里统一把通用确认面判掉。
|
||||
export function agentRuntimePendingActionIsUserInputCarrier(
|
||||
// - `user.input_request`:子 Agent 以 needs-user-input 终态退出后,Runtime 在 parent-wake
|
||||
// 屏障处按信封原文构造这个 pending(`ensure_static_delegate_user_input_wait_at_locked`),
|
||||
// 同一份问题再投影成 `userInputRequest`,交互面是问答卡。
|
||||
// - `plan.submit_gdd`:策划子 Agent 提交 Fast GDD 后,`planning/pending.json` 的
|
||||
// `submission.pendingActionId` 指向的正是这个 pending,交互面是 `GddApprovalCard`
|
||||
// 的批准 / 修改 / 退回。
|
||||
//
|
||||
// 两者都没有「确认 / 拒绝」语义:`user.input_request` 从来不在
|
||||
// `GAME_CREATION_APP_COMMANDS` 的 confirm 集合里;`plan.submit_gdd` 更是被运行时策略明确
|
||||
// 拒绝转成通用确认 pending(「Runtime-owned create-only 提交」),点确认必然失败。
|
||||
//
|
||||
// 通用待确认卡按 `tool` + `inputSummary` 渲染,套到它们身上就是把同一个请求画两遍——标题
|
||||
// 是原始工具名,副标题是 `questionCount=… · questionsSha256=…` 这种给日志看的取证摘要,
|
||||
// 还会把真正该看的那张卡压在下面。所以这里统一把通用确认面判掉。
|
||||
const AGENT_RUNTIME_DEDICATED_CARD_TOOLS = new Set([
|
||||
AGENT_RUNTIME_USER_INPUT_REQUEST_TOOL,
|
||||
AGENT_RUNTIME_PLAN_SUBMIT_GDD_TOOL,
|
||||
]);
|
||||
|
||||
export function agentRuntimePendingActionHasDedicatedCard(
|
||||
action: AgentRuntimePendingToolActionSummary | null | undefined,
|
||||
) {
|
||||
return action?.tool === AGENT_RUNTIME_USER_INPUT_REQUEST_TOOL;
|
||||
return Boolean(action && AGENT_RUNTIME_DEDICATED_CARD_TOOLS.has(action.tool));
|
||||
}
|
||||
|
||||
export function matchingAgentRuntimeForSteer(
|
||||
|
||||
@@ -29,7 +29,7 @@ import {
|
||||
agentRuntimeCanRetry,
|
||||
agentRuntimeNeedsUserInput,
|
||||
agentRuntimeNextStepFromPhase,
|
||||
agentRuntimePendingActionIsUserInputCarrier,
|
||||
agentRuntimePendingActionHasDedicatedCard,
|
||||
agentRuntimePlanStepText,
|
||||
agentRuntimeWaitingOnFromPhase,
|
||||
createAgentRuntimeUserInputResponseId,
|
||||
@@ -399,18 +399,18 @@ export function AgentRuntimeStatusPanel({
|
||||
!agentGoalStatusIsPaused(runtime.phase) &&
|
||||
!pendingToolAction &&
|
||||
Boolean(onRetryRuntimeTask);
|
||||
const pendingActionIsUserInputCarrier =
|
||||
agentRuntimePendingActionIsUserInputCarrier(pendingToolAction);
|
||||
const pendingActionHasDedicatedCard =
|
||||
agentRuntimePendingActionHasDedicatedCard(pendingToolAction);
|
||||
const canConfirm =
|
||||
Boolean(runtime.runId) &&
|
||||
Boolean(pendingToolAction?.actionId) &&
|
||||
!pendingActionIsUserInputCarrier &&
|
||||
!pendingActionHasDedicatedCard &&
|
||||
agentRuntimeCanConfirm(runtime.status) &&
|
||||
Boolean(onConfirmRuntimeTask);
|
||||
const canReject =
|
||||
Boolean(runtime.runId) &&
|
||||
Boolean(pendingToolAction?.actionId) &&
|
||||
!pendingActionIsUserInputCarrier &&
|
||||
!pendingActionHasDedicatedCard &&
|
||||
agentRuntimeCanConfirm(runtime.status) &&
|
||||
Boolean(onRejectRuntimeTask);
|
||||
const canCompact =
|
||||
@@ -675,6 +675,7 @@ export function ProjectSupervisorRuntimePanel({
|
||||
runtimeByAgentId,
|
||||
controlBusy,
|
||||
readOnly = false,
|
||||
planGddAwaitingDecision = false,
|
||||
professionalResultsByAgentId,
|
||||
onToolAction,
|
||||
onSupervisorRetry,
|
||||
@@ -687,6 +688,15 @@ export function ProjectSupervisorRuntimePanel({
|
||||
runtimeByAgentId: Record<string, AgentRuntimeState | undefined>;
|
||||
controlBusy: boolean;
|
||||
readOnly?: boolean;
|
||||
/**
|
||||
* `GddApprovalCard` 是否正在等用户做决定(`planning/pending.json` 有
|
||||
* `awaiting_decision` 的 pending)。
|
||||
*
|
||||
* 立项策划的审批等待复用了 `waiting-for-user-input` 这个 phase,但它没有
|
||||
* `userInputRequest`——真正的交互面是审批卡。只看 phase 的话这里会退到「待回答问题未能
|
||||
* 读取」那句错误,把一次正常的等待报成读取失败,还压在审批卡上面。
|
||||
*/
|
||||
planGddAwaitingDecision?: boolean;
|
||||
professionalResultsByAgentId: Record<
|
||||
string,
|
||||
ProjectAgentResultSummary | undefined
|
||||
@@ -781,8 +791,8 @@ export function ProjectSupervisorRuntimePanel({
|
||||
const pendingActionPresentation = pendingToolAction
|
||||
? projectSupervisorPendingActionPresentation(pendingToolAction)
|
||||
: null;
|
||||
const pendingActionIsUserInputCarrier =
|
||||
agentRuntimePendingActionIsUserInputCarrier(pendingToolAction);
|
||||
const pendingActionHasDedicatedCard =
|
||||
agentRuntimePendingActionHasDedicatedCard(pendingToolAction);
|
||||
const userInputRequest = runtime?.userInputRequest ?? null;
|
||||
const needsUserInput = agentRuntimeNeedsUserInput(runtime);
|
||||
const needsSupervisorReconciliation = Boolean(
|
||||
@@ -960,7 +970,7 @@ export function ProjectSupervisorRuntimePanel({
|
||||
) : null}
|
||||
{pendingToolAction &&
|
||||
pendingActionPresentation &&
|
||||
!pendingActionIsUserInputCarrier &&
|
||||
!pendingActionHasDedicatedCard &&
|
||||
!readOnly &&
|
||||
!needsSupervisorReconciliation ? (
|
||||
<div
|
||||
@@ -1001,7 +1011,11 @@ export function ProjectSupervisorRuntimePanel({
|
||||
? agentRuntimePlanStepText(progress.active)
|
||||
: '';
|
||||
const professionalPendingAction =
|
||||
professionalRuntime.pendingToolAction ?? null;
|
||||
agentRuntimePendingActionHasDedicatedCard(
|
||||
professionalRuntime.pendingToolAction,
|
||||
)
|
||||
? null
|
||||
: (professionalRuntime.pendingToolAction ?? null);
|
||||
const professionalResult =
|
||||
professionalResultsByAgentId[professionalRuntime.agentId];
|
||||
const dynamicArtRetryUnsupported =
|
||||
@@ -1230,7 +1244,7 @@ export function ProjectSupervisorRuntimePanel({
|
||||
controlBusy={controlBusy}
|
||||
onSubmit={onUserInput}
|
||||
/>
|
||||
) : needsUserInput && !readOnly ? (
|
||||
) : needsUserInput && !planGddAwaitingDecision && !readOnly ? (
|
||||
<p className="agent-runtime-user-input-missing" role="status">
|
||||
待回答问题未能读取,请稍后重试。
|
||||
</p>
|
||||
@@ -1295,7 +1309,7 @@ export function ProjectSupervisorRuntimeControls({
|
||||
) => void | Promise<void>;
|
||||
}) {
|
||||
const pendingToolAction = runtime?.pendingToolAction ?? null;
|
||||
const confirmableToolAction = agentRuntimePendingActionIsUserInputCarrier(
|
||||
const confirmableToolAction = agentRuntimePendingActionHasDedicatedCard(
|
||||
pendingToolAction,
|
||||
)
|
||||
? null
|
||||
|
||||
@@ -150,7 +150,10 @@ export function ProjectSupervisorView({
|
||||
) : null}
|
||||
</div>
|
||||
{directCodex ? null : (
|
||||
<ProjectSupervisorRuntimePanel {...runtimePanelProps} />
|
||||
<ProjectSupervisorRuntimePanel
|
||||
{...runtimePanelProps}
|
||||
planGddAwaitingDecision={Boolean(planGddState?.pendingApproval)}
|
||||
/>
|
||||
)}
|
||||
{pendingCommand ? (
|
||||
<div className="pending-command">
|
||||
|
||||
@@ -866,6 +866,7 @@ export function ProjectWorkspaceChatPane({
|
||||
error={projectSupervisorRuntimeError}
|
||||
runtimeByAgentId={agentRuntimeById}
|
||||
controlBusy={chatAgentBusy}
|
||||
planGddAwaitingDecision={Boolean(planGddState?.pendingApproval)}
|
||||
professionalResultsByAgentId={professionalAgentResultsById}
|
||||
onToolAction={handleProjectSupervisorToolAction}
|
||||
onSupervisorRetry={handleProjectSupervisorRetry}
|
||||
|
||||
@@ -174,4 +174,61 @@ export function registerPlanGddApprovalTests() {
|
||||
expect(textarea.value).toBe('战斗节奏太慢,请压缩到三个回合');
|
||||
expect(harness.planGddDecisionCalls).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('shows only the approval card while the GDD decision is outstanding', async () => {
|
||||
// 审批等待复用了 waiting-for-user-input 这个 phase,但它没有 userInputRequest;而
|
||||
// 策划子 Agent 的 plan.submit_gdd pending 正是 planning/pending.json 里
|
||||
// submission.pendingActionId 指向的那一个,交互面就是审批卡本身。两处都套通用面的话,
|
||||
// 用户会同时看到一句「待回答问题未能读取」和一张点了必然失败的 plan.submit_gdd
|
||||
// 确认卡,把真正该看的审批卡压在下面。
|
||||
const supervisorRunId = 'plan-root-awaiting-approval-run';
|
||||
let collaboratingRuntimes: Array<Record<string, unknown>> = [];
|
||||
const harness = createProjectSupervisorRuntimeHarness({
|
||||
expectedRunProfile: 'standard',
|
||||
initialRuntime: {
|
||||
runId: supervisorRunId,
|
||||
source: 'project-supervisor-plan',
|
||||
runProfile: 'standard',
|
||||
status: 'running',
|
||||
phase: 'waiting-for-user-input',
|
||||
currentTask: '剧情向恋爱养成游戏',
|
||||
waitingOn: '用户在审批卡选择批准、修改或退回',
|
||||
nextStep: '等待 decide_game_creator_plan_gdd',
|
||||
updatedAt: 8000,
|
||||
},
|
||||
runtimeMapLoader: async () => collaboratingRuntimes,
|
||||
});
|
||||
collaboratingRuntimes = [
|
||||
harness.runtimeState({
|
||||
agentId: 'project-planning',
|
||||
taskId: 'project-planning',
|
||||
sessionId: 'agent-session-project-planning',
|
||||
runId: 'delegated-delegation-344abab06ded0da1cf572fa3',
|
||||
source: 'agent-delegate',
|
||||
parentAgentId: 'project-supervisor',
|
||||
parentRunId: supervisorRunId,
|
||||
delegationId: 'delegation-344abab06ded0da1cf572fa3',
|
||||
runProfile: 'standard',
|
||||
status: 'idle',
|
||||
phase: 'completed',
|
||||
currentTask: '完成立项策划并给出 Fast GDD',
|
||||
pendingToolAction: {
|
||||
actionId: 'action-0123456789abcdef01234567',
|
||||
actionFingerprint: 'a'.repeat(64),
|
||||
tool: 'plan.submit_gdd',
|
||||
inputSummary: null,
|
||||
reason: '提交完整原创 MVP Fast GDD。',
|
||||
requestedAt: 8000,
|
||||
},
|
||||
updatedAt: 8000,
|
||||
}),
|
||||
];
|
||||
harness.setPlanGddState(createPlanGddStateView());
|
||||
await mountApprovalCard(harness);
|
||||
|
||||
expect(screen.queryByText('待回答问题未能读取,请稍后重试。')).toBeNull();
|
||||
expect(screen.queryByText(/plan\.submit_gdd/)).toBeNull();
|
||||
expect(screen.queryByLabelText('立项策划 Agent待确认动作')).toBeNull();
|
||||
expect(screen.getByRole('button', { name: '批准 v1' })).not.toBeNull();
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user