fix(agent): verify delivered images when the planner reply disconnects

This commit is contained in:
134 2026-10-21 16:12:58 +08:00
parent 82bc90680a
commit 76bf04c221
2 changed files with 32 additions and 2 deletions

View File

@ -37,6 +37,7 @@ export async function runZcodeCanvasTask(ports: Ports): Promise<CanvasAgentRunSt
}
let modelError = "";
let toolFailure = "";
let nativeFailed = false;
let verifiedCompletion = "";
async function finishVerifiedCutout() {
snapshot.nativeRuntime!.state = "stopped";
@ -187,7 +188,7 @@ export async function runZcodeCanvasTask(ports: Ports): Promise<CanvasAgentRunSt
throw error;
}
},
});
}).catch(error => { nativeFailed = true; throw error; });
if (verifiedCompletion && !ports.signal.aborted && !(await ports.isCancelled())) return finishVerifiedCutout();
await exclusive(async () => {
snapshot.nativeRuntime!.sessionId = result.sessionId;
@ -245,6 +246,27 @@ export async function runZcodeCanvasTask(ports: Ports): Promise<CanvasAgentRunSt
if (snapshot.nativeRuntime) snapshot.nativeRuntime.state = "stopped";
const cancelled = await ports.isCancelled().catch(() => false);
if (verifiedCompletion && !cancelled && !ports.signal.aborted) return finishVerifiedCutout();
// Losing the planner's final reply must not discard an already delivered
// image. Only inspect saved results here; never retry a paid tool.
if (nativeFailed && !cancelled && !ports.signal.aborted && !toolFailure && snapshot.resultNodeIds.length &&
!snapshot.calls.some(call => call.status === "started" || call.uncertain ||
(call.status === "failed" && !call.rejected && !call.confirmedNoResult && !ports.tools[call.name]?.readOnly) ||
requiresCanvasInteraction(snapshot, call))) {
try {
const inputs = JSON.stringify(snapshot.inputs);
await save("图片已返回画布,正在确认结果");
const reviewSnapshot = { ...snapshot, calls: snapshot.calls.filter(call => !call.rejected && !call.confirmedNoResult &&
!(call.status === "failed" && ports.tools[call.name]?.readOnly)) };
const verification = await ports.verify(reviewSnapshot);
snapshot.verification = verification;
snapshot.inputTokens = reviewSnapshot.inputTokens; snapshot.outputTokens = reviewSnapshot.outputTokens;
const control = await ports.consumeInputs(snapshot);
if (control.cancelled || ports.signal.aborted) return await save("已停止后续步骤,完成的结果保留在画布。", "cancelled");
if (inputs !== JSON.stringify(snapshot.inputs)) return await save("已收到新的补充要求,图片已保留,请继续处理。", "needs_review");
return await save(verification.passed ? "图片已完成并通过检查,结果已保留在画布。"
: `图片已返回画布,结果待核对:${verification.issues.join(";")}`, verification.passed ? "completed" : "needs_review");
} catch { /* Keep the original interruption if independent inspection also fails. */ }
}
return await save(cancelled ? "已停止后续步骤,完成的结果保留在画布。" : toolFailure || modelError || publicAgentError(error), cancelled ? "cancelled" : "needs_review");
}
}

View File

@ -24,6 +24,14 @@ function fixture(objective = '修改图片') {
const result = { type: 'result', sessionId: 'sess_fixture', response: '已完成' };
async function main() {
let checks = 0;
for (const passed of [true,false]) {
const h=fixture();let paid=0,reviews=0;
h.ports.tools.edit.execute=async()=>{paid++;return {updatedNodeIds:['a'],createdNodeIds:['real-result']};};
h.ports.native=async options=>{await options.execute('planCanvasTask',{},'plan',options.signal!);await options.execute('edit',{nodeIds:['a']},'generated',options.signal!);throw new Error('planner connection lost after result');};
h.ports.verify=async()=>{reviews++;return {passed,issues:passed?[]:['主体不符合要求']};};
assert.equal(await runZcodeCanvasTask(h.ports),passed?'completed':'needs_review');assert.equal(paid,1);assert.equal(reviews,1);
assert.match(h.snapshot.message,passed?/已完成并通过检查/:/结果待核对.*主体/);checks++;
}
{
const h = fixture();let paid = 0;
h.ports.tools.edit.execute = async () => { paid++; throw new AgentImageGenerationError('生图通道余额不足,本次未生成图片。你希望切换模型还是稍后重试?', true); };
@ -38,7 +46,7 @@ let checks = 0;
assert.equal(call.confirmedNoResult,true); assert.equal(call.uncertain,false); assert.match(h.snapshot.message,/余额不足.*切换模型/);
// A later user-confirmed retry may proceed, while keeping the old failure in
// history and independently checking the newly returned result.
h.ports.tools.edit.execute=async()=>{paid++;return {createdNodeIds:['real-result']};};
h.ports.tools.edit.execute=async()=>{paid++;return {updatedNodeIds:['a'],createdNodeIds:['real-result']};};
h.ports.native=async options=>{await options.execute('edit',{nodeIds:['a']},'user-retry',options.signal!);return result;};
h.ports.verify=async snapshot=>{assert(!snapshot.calls.some(c=>c.confirmedNoResult));return {passed:true,issues:[]};};
assert.equal(await runZcodeCanvasTask(h.ports),'completed'); assert.equal(paid,2);checks++;