Fix run from block in copilot

This commit is contained in:
Siddharth Ganesan
2026-02-09 16:52:40 -08:00
parent 7670cdfadf
commit 1beb35c225
4 changed files with 84 additions and 46 deletions

View File

@@ -250,10 +250,10 @@ export const subAgentSSEHandlers: Record<string, SSEHandler> = {
sendAutoAcceptConfirmation(id)
}
// Client-executable run tools: execute on the client for real-time feedback.
// The server defers execution in interactive mode; we execute here and
// report back via mark-complete.
if (CLIENT_EXECUTABLE_RUN_TOOLS.has(name)) {
// Client-executable run tools: if auto-allowed, execute immediately for
// real-time feedback. For non-auto-allowed, the user must click "Allow"
// first — handleRun in tool-call.tsx triggers executeRunToolOnClient.
if (CLIENT_EXECUTABLE_RUN_TOOLS.has(name) && isAutoAllowed) {
executeRunToolOnClient(id, name, args || {})
}
},

View File

@@ -490,6 +490,23 @@ export const subAgentHandlers: Record<string, SSEHandler> = {
options.timeout || STREAM_TIMEOUT_MS,
options.abortSignal
)
if (completion?.status === 'rejected') {
toolCall.status = 'rejected'
toolCall.endTime = Date.now()
markToolComplete(
toolCall.id,
toolCall.name,
400,
completion.message || 'Tool execution rejected'
).catch((err) => {
logger.error('markToolComplete fire-and-forget failed (subagent run tool rejected)', {
toolCallId: toolCall.id,
error: err instanceof Error ? err.message : String(err),
})
})
markToolResultSeen(toolCallId)
return
}
const success = completion?.status === 'success'
toolCall.status = success ? 'success' : 'error'
toolCall.endTime = Date.now()

View File

@@ -146,10 +146,12 @@ export async function waitForToolDecision(
}
/**
* Wait for a tool completion signal (success/error) from the client.
* Unlike waitForToolDecision which returns on any status, this ignores
* intermediate statuses like 'accepted'/'rejected'/'background' and only
* returns when the client reports final completion via success/error.
* Wait for a tool completion signal (success/error/rejected) from the client.
* Unlike waitForToolDecision which returns on any status, this ignores the
* initial 'accepted' status and only returns on terminal statuses:
* - success: client finished executing successfully
* - error: client execution failed
* - rejected: user clicked Skip (subagent run tools where user hasn't auto-allowed)
*
* Used for client-executable run tools: the client executes the workflow
* and posts success/error to /api/copilot/confirm when done. The server
@@ -166,8 +168,12 @@ export async function waitForToolCompletion(
while (Date.now() - start < timeoutMs) {
if (abortSignal?.aborted) return null
const decision = await getToolConfirmation(toolCallId)
// Only return on completion statuses, not accept/reject decisions
if (decision?.status === 'success' || decision?.status === 'error') {
// Return on completion/terminal statuses, not intermediate 'accepted'
if (
decision?.status === 'success' ||
decision?.status === 'error' ||
decision?.status === 'rejected'
) {
return decision
}
await new Promise((resolve) => setTimeout(resolve, interval))

View File

@@ -50,6 +50,18 @@ import {
import { getLatestBlock } from '@/blocks/registry'
import { getCustomTool } from '@/hooks/queries/custom-tools'
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
import { useWorkflowStore } from '@/stores/workflows/workflow/store'
/** Resolve a block ID to its human-readable name from the workflow store. */
function resolveBlockName(blockId: string | undefined): string | undefined {
if (!blockId) return undefined
try {
const blocks = useWorkflowStore.getState().blocks
return blocks[blockId]?.name || undefined
} catch {
return undefined
}
}
export enum ClientToolCallState {
generating = 'generating',
@@ -1742,12 +1754,12 @@ const META_generate_api_key: ToolMetadata = {
const META_run_block: ToolMetadata = {
displayNames: {
[ClientToolCallState.generating]: { text: 'Preparing to run block', icon: Loader2 },
[ClientToolCallState.pending]: { text: 'Run this block?', icon: Play },
[ClientToolCallState.pending]: { text: 'Run block?', icon: Play },
[ClientToolCallState.executing]: { text: 'Running block', icon: Loader2 },
[ClientToolCallState.success]: { text: 'Executed block', icon: Play },
[ClientToolCallState.success]: { text: 'Ran block', icon: Play },
[ClientToolCallState.error]: { text: 'Failed to run block', icon: XCircle },
[ClientToolCallState.rejected]: { text: 'Skipped block execution', icon: MinusCircle },
[ClientToolCallState.aborted]: { text: 'Aborted block execution', icon: MinusCircle },
[ClientToolCallState.rejected]: { text: 'Skipped running block', icon: MinusCircle },
[ClientToolCallState.aborted]: { text: 'Aborted running block', icon: MinusCircle },
[ClientToolCallState.background]: { text: 'Running block in background', icon: Play },
},
interrupt: {
@@ -1775,23 +1787,24 @@ const META_run_block: ToolMetadata = {
getDynamicText: (params, state) => {
const blockId = params?.blockId || params?.block_id
if (blockId && typeof blockId === 'string') {
const name = resolveBlockName(blockId) || blockId
switch (state) {
case ClientToolCallState.success:
return `Executed block ${blockId}`
return `Ran ${name}`
case ClientToolCallState.executing:
return `Running block ${blockId}`
return `Running ${name}`
case ClientToolCallState.generating:
return `Preparing to run block ${blockId}`
return `Preparing to run ${name}`
case ClientToolCallState.pending:
return `Run block ${blockId}?`
return `Run ${name}?`
case ClientToolCallState.error:
return `Failed to run block ${blockId}`
return `Failed to run ${name}`
case ClientToolCallState.rejected:
return `Skipped running block ${blockId}`
return `Skipped running ${name}`
case ClientToolCallState.aborted:
return `Aborted running block ${blockId}`
return `Aborted running ${name}`
case ClientToolCallState.background:
return `Running block ${blockId} in background`
return `Running ${name} in background`
}
}
return undefined
@@ -1801,12 +1814,12 @@ const META_run_block: ToolMetadata = {
const META_run_from_block: ToolMetadata = {
displayNames: {
[ClientToolCallState.generating]: { text: 'Preparing to run from block', icon: Loader2 },
[ClientToolCallState.pending]: { text: 'Run from this block?', icon: Play },
[ClientToolCallState.pending]: { text: 'Run from block?', icon: Play },
[ClientToolCallState.executing]: { text: 'Running from block', icon: Loader2 },
[ClientToolCallState.success]: { text: 'Executed from block', icon: Play },
[ClientToolCallState.success]: { text: 'Ran from block', icon: Play },
[ClientToolCallState.error]: { text: 'Failed to run from block', icon: XCircle },
[ClientToolCallState.rejected]: { text: 'Skipped run from block', icon: MinusCircle },
[ClientToolCallState.aborted]: { text: 'Aborted run from block', icon: MinusCircle },
[ClientToolCallState.rejected]: { text: 'Skipped running from block', icon: MinusCircle },
[ClientToolCallState.aborted]: { text: 'Aborted running from block', icon: MinusCircle },
[ClientToolCallState.background]: { text: 'Running from block in background', icon: Play },
},
interrupt: {
@@ -1834,23 +1847,24 @@ const META_run_from_block: ToolMetadata = {
getDynamicText: (params, state) => {
const blockId = params?.startBlockId || params?.start_block_id
if (blockId && typeof blockId === 'string') {
const name = resolveBlockName(blockId) || blockId
switch (state) {
case ClientToolCallState.success:
return `Executed from block ${blockId}`
return `Ran from ${name}`
case ClientToolCallState.executing:
return `Running from block ${blockId}`
return `Running from ${name}`
case ClientToolCallState.generating:
return `Preparing to run from block ${blockId}`
return `Preparing to run from ${name}`
case ClientToolCallState.pending:
return `Run from block ${blockId}?`
return `Run from ${name}?`
case ClientToolCallState.error:
return `Failed to run from block ${blockId}`
return `Failed to run from ${name}`
case ClientToolCallState.rejected:
return `Skipped running from block ${blockId}`
return `Skipped running from ${name}`
case ClientToolCallState.aborted:
return `Aborted running from block ${blockId}`
return `Aborted running from ${name}`
case ClientToolCallState.background:
return `Running from block ${blockId} in background`
return `Running from ${name} in background`
}
}
return undefined
@@ -1860,12 +1874,12 @@ const META_run_from_block: ToolMetadata = {
const META_run_workflow_until_block: ToolMetadata = {
displayNames: {
[ClientToolCallState.generating]: { text: 'Preparing to run until block', icon: Loader2 },
[ClientToolCallState.pending]: { text: 'Run until this block?', icon: Play },
[ClientToolCallState.pending]: { text: 'Run until block?', icon: Play },
[ClientToolCallState.executing]: { text: 'Running until block', icon: Loader2 },
[ClientToolCallState.success]: { text: 'Executed until block', icon: Play },
[ClientToolCallState.success]: { text: 'Ran until block', icon: Play },
[ClientToolCallState.error]: { text: 'Failed to run until block', icon: XCircle },
[ClientToolCallState.rejected]: { text: 'Skipped run until block', icon: MinusCircle },
[ClientToolCallState.aborted]: { text: 'Aborted run until block', icon: MinusCircle },
[ClientToolCallState.rejected]: { text: 'Skipped running until block', icon: MinusCircle },
[ClientToolCallState.aborted]: { text: 'Aborted running until block', icon: MinusCircle },
[ClientToolCallState.background]: { text: 'Running until block in background', icon: Play },
},
interrupt: {
@@ -1893,23 +1907,24 @@ const META_run_workflow_until_block: ToolMetadata = {
getDynamicText: (params, state) => {
const blockId = params?.stopAfterBlockId || params?.stop_after_block_id
if (blockId && typeof blockId === 'string') {
const name = resolveBlockName(blockId) || blockId
switch (state) {
case ClientToolCallState.success:
return `Executed until block ${blockId}`
return `Ran until ${name}`
case ClientToolCallState.executing:
return `Running until block ${blockId}`
return `Running until ${name}`
case ClientToolCallState.generating:
return `Preparing to run until block ${blockId}`
return `Preparing to run until ${name}`
case ClientToolCallState.pending:
return `Run until block ${blockId}?`
return `Run until ${name}?`
case ClientToolCallState.error:
return `Failed to run until block ${blockId}`
return `Failed to run until ${name}`
case ClientToolCallState.rejected:
return `Skipped running until block ${blockId}`
return `Skipped running until ${name}`
case ClientToolCallState.aborted:
return `Aborted running until block ${blockId}`
return `Aborted running until ${name}`
case ClientToolCallState.background:
return `Running until block ${blockId} in background`
return `Running until ${name} in background`
}
}
return undefined