diff --git a/lib/ai/zcode-agent/task.ts b/lib/ai/zcode-agent/task.ts index 88354b938..9fa0f35de 100644 --- a/lib/ai/zcode-agent/task.ts +++ b/lib/ai/zcode-agent/task.ts @@ -37,6 +37,7 @@ export async function runZcodeCanvasTask(ports: Ports): Promise { 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 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"); } } diff --git a/scripts/zcode-agent/task-fixture.ts b/scripts/zcode-agent/task-fixture.ts index 9d03addcc..565eac094 100644 --- a/scripts/zcode-agent/task-fixture.ts +++ b/scripts/zcode-agent/task-fixture.ts @@ -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++;