Fix skip and mtb

This commit is contained in:
Siddharth Ganesan
2026-02-09 17:42:53 -08:00
parent 18f1d76206
commit 8a2eacf179
3 changed files with 71 additions and 49 deletions

View File

@@ -211,6 +211,24 @@ export const sseHandlers: Record<string, SSEHandler> = {
options.timeout || STREAM_TIMEOUT_MS, options.timeout || STREAM_TIMEOUT_MS,
options.abortSignal options.abortSignal
) )
if (completion?.status === 'background') {
toolCall.status = 'skipped'
toolCall.endTime = Date.now()
markToolComplete(
toolCall.id,
toolCall.name,
202,
completion.message || 'Tool execution moved to background',
{ background: true }
).catch((err) => {
logger.error('markToolComplete fire-and-forget failed (run tool background)', {
toolCallId: toolCall.id,
error: err instanceof Error ? err.message : String(err),
})
})
markToolResultSeen(toolCallId)
return
}
const success = completion?.status === 'success' const success = completion?.status === 'success'
toolCall.status = success ? 'success' : 'error' toolCall.status = success ? 'success' : 'error'
toolCall.endTime = Date.now() toolCall.endTime = Date.now()
@@ -235,48 +253,40 @@ export const sseHandlers: Record<string, SSEHandler> = {
if (decision?.status === 'rejected' || decision?.status === 'error') { if (decision?.status === 'rejected' || decision?.status === 'error') {
toolCall.status = 'rejected' toolCall.status = 'rejected'
toolCall.endTime = Date.now() toolCall.endTime = Date.now()
await markToolComplete( // Fire-and-forget: must NOT await — see deadlock note in executeToolAndReport
markToolComplete(
toolCall.id, toolCall.id,
toolCall.name, toolCall.name,
400, 400,
decision.message || 'Tool execution rejected', decision.message || 'Tool execution rejected',
{ skipped: true, reason: 'user_rejected' } { skipped: true, reason: 'user_rejected' }
) ).catch((err) => {
markToolResultSeen(toolCall.id) logger.error('markToolComplete fire-and-forget failed (rejected)', {
await options.onEvent?.({ toolCallId: toolCall.id,
type: 'tool_result', error: err instanceof Error ? err.message : String(err),
toolCallId: toolCall.id, })
data: {
id: toolCall.id,
name: toolCall.name,
success: false,
result: { skipped: true, reason: 'user_rejected' },
},
}) })
markToolResultSeen(toolCall.id)
return return
} }
if (decision?.status === 'background') { if (decision?.status === 'background') {
toolCall.status = 'skipped' toolCall.status = 'skipped'
toolCall.endTime = Date.now() toolCall.endTime = Date.now()
await markToolComplete( // Fire-and-forget: must NOT await — see deadlock note in executeToolAndReport
markToolComplete(
toolCall.id, toolCall.id,
toolCall.name, toolCall.name,
202, 202,
decision.message || 'Tool execution moved to background', decision.message || 'Tool execution moved to background',
{ background: true } { background: true }
) ).catch((err) => {
markToolResultSeen(toolCall.id) logger.error('markToolComplete fire-and-forget failed (background)', {
await options.onEvent?.({ toolCallId: toolCall.id,
type: 'tool_result', error: err instanceof Error ? err.message : String(err),
toolCallId: toolCall.id, })
data: {
id: toolCall.id,
name: toolCall.name,
success: true,
result: { background: true },
},
}) })
markToolResultSeen(toolCall.id)
return return
} }
} }
@@ -436,47 +446,39 @@ export const subAgentHandlers: Record<string, SSEHandler> = {
if (decision?.status === 'rejected' || decision?.status === 'error') { if (decision?.status === 'rejected' || decision?.status === 'error') {
toolCall.status = 'rejected' toolCall.status = 'rejected'
toolCall.endTime = Date.now() toolCall.endTime = Date.now()
await markToolComplete( // Fire-and-forget: must NOT await — see deadlock note in executeToolAndReport
markToolComplete(
toolCall.id, toolCall.id,
toolCall.name, toolCall.name,
400, 400,
decision.message || 'Tool execution rejected', decision.message || 'Tool execution rejected',
{ skipped: true, reason: 'user_rejected' } { skipped: true, reason: 'user_rejected' }
) ).catch((err) => {
markToolResultSeen(toolCall.id) logger.error('markToolComplete fire-and-forget failed (subagent rejected)', {
await options?.onEvent?.({ toolCallId: toolCall.id,
type: 'tool_result', error: err instanceof Error ? err.message : String(err),
toolCallId: toolCall.id, })
data: {
id: toolCall.id,
name: toolCall.name,
success: false,
result: { skipped: true, reason: 'user_rejected' },
},
}) })
markToolResultSeen(toolCall.id)
return return
} }
if (decision?.status === 'background') { if (decision?.status === 'background') {
toolCall.status = 'skipped' toolCall.status = 'skipped'
toolCall.endTime = Date.now() toolCall.endTime = Date.now()
await markToolComplete( // Fire-and-forget: must NOT await — see deadlock note in executeToolAndReport
markToolComplete(
toolCall.id, toolCall.id,
toolCall.name, toolCall.name,
202, 202,
decision.message || 'Tool execution moved to background', decision.message || 'Tool execution moved to background',
{ background: true } { background: true }
) ).catch((err) => {
markToolResultSeen(toolCall.id) logger.error('markToolComplete fire-and-forget failed (subagent background)', {
await options?.onEvent?.({ toolCallId: toolCall.id,
type: 'tool_result', error: err instanceof Error ? err.message : String(err),
toolCallId: toolCall.id, })
data: {
id: toolCall.id,
name: toolCall.name,
success: true,
result: { background: true },
},
}) })
markToolResultSeen(toolCall.id)
return return
} }
} }
@@ -507,6 +509,24 @@ export const subAgentHandlers: Record<string, SSEHandler> = {
markToolResultSeen(toolCallId) markToolResultSeen(toolCallId)
return return
} }
if (completion?.status === 'background') {
toolCall.status = 'skipped'
toolCall.endTime = Date.now()
markToolComplete(
toolCall.id,
toolCall.name,
202,
completion.message || 'Tool execution moved to background',
{ background: true }
).catch((err) => {
logger.error('markToolComplete fire-and-forget failed (subagent run tool background)', {
toolCallId: toolCall.id,
error: err instanceof Error ? err.message : String(err),
})
})
markToolResultSeen(toolCallId)
return
}
const success = completion?.status === 'success' const success = completion?.status === 'success'
toolCall.status = success ? 'success' : 'error' toolCall.status = success ? 'success' : 'error'
toolCall.endTime = Date.now() toolCall.endTime = Date.now()

View File

@@ -172,7 +172,8 @@ export async function waitForToolCompletion(
if ( if (
decision?.status === 'success' || decision?.status === 'success' ||
decision?.status === 'error' || decision?.status === 'error' ||
decision?.status === 'rejected' decision?.status === 'rejected' ||
decision?.status === 'background'
) { ) {
return decision return decision
} }

View File

@@ -1500,6 +1500,7 @@ export const useCopilotStore = create<CopilotStore>()(
else if (newState === 'success' || newState === 'accepted') else if (newState === 'success' || newState === 'accepted')
norm = ClientToolCallState.success norm = ClientToolCallState.success
else if (newState === 'aborted') norm = ClientToolCallState.aborted else if (newState === 'aborted') norm = ClientToolCallState.aborted
else if (newState === 'background') norm = ClientToolCallState.background
else if (typeof newState === 'number') norm = newState as unknown as ClientToolCallState else if (typeof newState === 'number') norm = newState as unknown as ClientToolCallState
map[id] = { map[id] = {
...current, ...current,