From 6942bef56786da70b810ba15f6be5fd5a04b2820 Mon Sep 17 00:00:00 2001 From: lofyer Date: Tue, 11 Aug 2026 20:58:10 +0800 Subject: [PATCH] fix: preserve recovered opencode results --- src/main/agent/opencode-runtime.test.ts | 66 +++++++++++++++++++++++++ src/main/agent/opencode-runtime.ts | 51 ++++++++++++++++--- 2 files changed, 110 insertions(+), 7 deletions(-) diff --git a/src/main/agent/opencode-runtime.test.ts b/src/main/agent/opencode-runtime.test.ts index 8a52baf..a898830 100644 --- a/src/main/agent/opencode-runtime.test.ts +++ b/src/main/agent/opencode-runtime.test.ts @@ -1928,6 +1928,72 @@ describe('OpenCodeRuntime embedded permission mediation', () => { await runtime.dispose() }) + it('keeps a completed response when an earlier tool attempt failed', async () => { + const { client, session } = runClient([ + { + id: 'event-tool-error', + type: 'message.part.updated', + properties: { + sessionID: 'session-1', + part: { + id: 'part-1', + callID: 'call-1', + type: 'tool', + tool: 'read', + state: { + status: 'error', + error: 'Cannot read binary file' + } + } + } + }, + completedToolEvent('call-2', 'write'), + { + id: 'event-text', + type: 'message.part.delta', + properties: { + sessionID: 'session-1', + messageID: 'message-1', + partID: 'part-text', + field: 'text', + delta: 'PPT 已生成并保存。' + } + }, + { + id: 'event-idle', + type: 'session.idle', + properties: { sessionID: 'session-1' } + } + ]) + const runtime = embeddedRuntime(client) + const events = await collectRun(runtime, 'execute') + + expect( + events.filter( + (event) => + event.type === 'tool' && event.callId === 'call-1' + ) + ).toEqual([ + expect.objectContaining({ + state: 'failed', + error: 'Cannot read binary file' + }), + expect.objectContaining({ + state: 'recoverable', + error: 'Cannot read binary file' + }) + ]) + expect(events).toContainEqual( + expect.objectContaining({ + type: 'text', + delta: 'PPT 已生成并保存。' + }) + ) + expect(events.at(-1)).toMatchObject({ type: 'done' }) + expect(session.abort).not.toHaveBeenCalled() + await runtime.dispose() + }) + it('surfaces a rejected async prompt instead of reporting success', async () => { const { client, session } = runClient([ { diff --git a/src/main/agent/opencode-runtime.ts b/src/main/agent/opencode-runtime.ts index 192469a..1440b77 100644 --- a/src/main/agent/opencode-runtime.ts +++ b/src/main/agent/opencode-runtime.ts @@ -1190,6 +1190,7 @@ export class OpenCodeRuntime implements AgentRuntime { >() const reasoningPartIds = new Set() const reportedQuestionIds = new Set() + let hasResponseTextAfterFailure = false try { const promptText = session.created && request.history?.length @@ -1262,6 +1263,15 @@ export class OpenCodeRuntime implements AgentRuntime { 'thinking' ].includes(event.properties.field) if (reasoning || event.properties.field === 'text') { + if ( + !reasoning && + /\S/u.test(event.properties.delta) && + [...toolStates.values()].some( + (tool) => tool.state === 'failed' + ) + ) { + hasResponseTextAfterFailure = true + } yield { requestId: request.requestId, type: reasoning ? 'reasoning' : 'text', @@ -1291,6 +1301,9 @@ export class OpenCodeRuntime implements AgentRuntime { } const state = part.state.status === 'error' ? 'failed' : part.state.status + if (state === 'failed') { + hasResponseTextAfterFailure = false + } const error = part.state.status === 'error' ? safeToolErrorDetail(part.state.error) @@ -1498,17 +1511,41 @@ export class OpenCodeRuntime implements AgentRuntime { ) ) } - const unsuccessfulTool = [...toolStates.entries()].find( - ([, tool]) => tool.state !== 'completed' + const incompleteTool = [...toolStates.entries()].find( + ([, tool]) => + tool.state === 'pending' || tool.state === 'running' ) - if (unsuccessfulTool) { - const [callId, tool] = unsuccessfulTool + if (incompleteTool) { + const [callId] = incompleteTool throw new Error( - tool.state === 'failed' - ? `OpenCode 工具执行失败(${callId.slice(0, 128)})${tool.error ? `:${tool.error}` : ''}` - : `OpenCode 工具未完成(${callId.slice(0, 128)})` + `OpenCode 工具未完成(${callId.slice(0, 128)})` ) } + const failedTools = [...toolStates.entries()].filter( + ([, tool]) => tool.state === 'failed' + ) + if ( + failedTools.length > 0 && + !hasResponseTextAfterFailure + ) { + const [callId, tool] = failedTools[0]! + throw new Error( + `OpenCode 工具执行失败(${callId.slice(0, 128)})${tool.error ? `:${tool.error}` : ''}` + ) + } + for (const [callId, tool] of failedTools) { + yield { + requestId: request.requestId, + type: 'tool', + callId, + name: tool.name, + state: 'recoverable', + summary: `OpenCode 已在后续响应中处理工具失败:${tool.name}`, + ...(tool.input ? { input: tool.input } : {}), + ...(tool.output ? { output: tool.output } : {}), + ...(tool.error ? { error: tool.error } : {}) + } + } yield { requestId: request.requestId, type: 'done',