fix: preserve recovered opencode results
This commit is contained in:
@@ -1928,6 +1928,72 @@ describe('OpenCodeRuntime embedded permission mediation', () => {
|
|||||||
await runtime.dispose()
|
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 () => {
|
it('surfaces a rejected async prompt instead of reporting success', async () => {
|
||||||
const { client, session } = runClient([
|
const { client, session } = runClient([
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1190,6 +1190,7 @@ export class OpenCodeRuntime implements AgentRuntime {
|
|||||||
>()
|
>()
|
||||||
const reasoningPartIds = new Set<string>()
|
const reasoningPartIds = new Set<string>()
|
||||||
const reportedQuestionIds = new Set<string>()
|
const reportedQuestionIds = new Set<string>()
|
||||||
|
let hasResponseTextAfterFailure = false
|
||||||
try {
|
try {
|
||||||
const promptText =
|
const promptText =
|
||||||
session.created && request.history?.length
|
session.created && request.history?.length
|
||||||
@@ -1262,6 +1263,15 @@ export class OpenCodeRuntime implements AgentRuntime {
|
|||||||
'thinking'
|
'thinking'
|
||||||
].includes(event.properties.field)
|
].includes(event.properties.field)
|
||||||
if (reasoning || event.properties.field === 'text') {
|
if (reasoning || event.properties.field === 'text') {
|
||||||
|
if (
|
||||||
|
!reasoning &&
|
||||||
|
/\S/u.test(event.properties.delta) &&
|
||||||
|
[...toolStates.values()].some(
|
||||||
|
(tool) => tool.state === 'failed'
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
hasResponseTextAfterFailure = true
|
||||||
|
}
|
||||||
yield {
|
yield {
|
||||||
requestId: request.requestId,
|
requestId: request.requestId,
|
||||||
type: reasoning ? 'reasoning' : 'text',
|
type: reasoning ? 'reasoning' : 'text',
|
||||||
@@ -1291,6 +1301,9 @@ export class OpenCodeRuntime implements AgentRuntime {
|
|||||||
}
|
}
|
||||||
const state =
|
const state =
|
||||||
part.state.status === 'error' ? 'failed' : part.state.status
|
part.state.status === 'error' ? 'failed' : part.state.status
|
||||||
|
if (state === 'failed') {
|
||||||
|
hasResponseTextAfterFailure = false
|
||||||
|
}
|
||||||
const error =
|
const error =
|
||||||
part.state.status === 'error'
|
part.state.status === 'error'
|
||||||
? safeToolErrorDetail(part.state.error)
|
? safeToolErrorDetail(part.state.error)
|
||||||
@@ -1498,17 +1511,41 @@ export class OpenCodeRuntime implements AgentRuntime {
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
const unsuccessfulTool = [...toolStates.entries()].find(
|
const incompleteTool = [...toolStates.entries()].find(
|
||||||
([, tool]) => tool.state !== 'completed'
|
([, tool]) =>
|
||||||
|
tool.state === 'pending' || tool.state === 'running'
|
||||||
)
|
)
|
||||||
if (unsuccessfulTool) {
|
if (incompleteTool) {
|
||||||
const [callId, tool] = unsuccessfulTool
|
const [callId] = incompleteTool
|
||||||
throw new Error(
|
throw new Error(
|
||||||
tool.state === 'failed'
|
`OpenCode 工具未完成(${callId.slice(0, 128)})`
|
||||||
? `OpenCode 工具执行失败(${callId.slice(0, 128)})${tool.error ? `:${tool.error}` : ''}`
|
|
||||||
: `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 {
|
yield {
|
||||||
requestId: request.requestId,
|
requestId: request.requestId,
|
||||||
type: 'done',
|
type: 'done',
|
||||||
|
|||||||
Reference in New Issue
Block a user