Fix tool call resolution

This commit is contained in:
Siddharth Ganesan
2026-02-09 10:53:45 -08:00
parent b4361b8585
commit d2c028f7cd
4 changed files with 90 additions and 18 deletions

View File

@@ -1221,13 +1221,26 @@ function isIntegrationTool(toolName: string): boolean {
}
function shouldShowRunSkipButtons(toolCall: CopilotToolCall): boolean {
if (!toolCall.name || toolCall.name === 'unknown_tool') {
return false
}
if (toolCall.state !== ClientToolCallState.pending) {
return false
}
// Never show buttons for tools the user has marked as always-allowed
if (useCopilotStore.getState().autoAllowedTools.includes(toolCall.name)) {
return false
}
const hasInterrupt = TOOL_DISPLAY_REGISTRY[toolCall.name]?.uiConfig?.interrupt === true
if (hasInterrupt && toolCall.state === 'pending') {
if (hasInterrupt) {
return true
}
const mode = useCopilotStore.getState().mode
if (mode === 'build' && isIntegrationTool(toolCall.name) && toolCall.state === 'pending') {
if (mode === 'build' && isIntegrationTool(toolCall.name)) {
return true
}

View File

@@ -10,8 +10,8 @@ import {
} from '@/lib/copilot/store-utils'
import { ClientToolCallState } from '@/lib/copilot/tools/client/tool-display-registry'
import type { CopilotStore, CopilotStreamInfo, CopilotToolCall } from '@/stores/panel/copilot/types'
import { useEnvironmentStore } from '@/stores/settings/environment/store'
import { useVariablesStore } from '@/stores/panel/variables/store'
import { useEnvironmentStore } from '@/stores/settings/environment/store'
import { useWorkflowDiffStore } from '@/stores/workflow-diff/store'
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
import type { WorkflowState } from '@/stores/workflows/workflow/types'
@@ -499,7 +499,10 @@ export const sseHandlers: Record<string, SSEHandler> = {
const { toolCallsById } = get()
if (!toolCallsById[toolCallId]) {
const initialState = ClientToolCallState.pending
const isAutoAllowed = get().autoAllowedTools.includes(toolName)
const initialState = isAutoAllowed
? ClientToolCallState.executing
: ClientToolCallState.pending
const tc: CopilotToolCall = {
id: toolCallId,
name: toolName,
@@ -524,23 +527,39 @@ export const sseHandlers: Record<string, SSEHandler> = {
const { toolCallsById } = get()
const existing = toolCallsById[id]
const toolName = name || existing?.name || 'unknown_tool'
const autoAllowedTools = get().autoAllowedTools
const isAutoAllowed =
autoAllowedTools.includes(toolName) ||
(existing?.name ? autoAllowedTools.includes(existing.name) : false)
let initialState = isAutoAllowed ? ClientToolCallState.executing : ClientToolCallState.pending
// Avoid flickering back to pending on partial/duplicate events once a tool is executing.
if (
existing?.state === ClientToolCallState.executing &&
initialState === ClientToolCallState.pending
) {
initialState = ClientToolCallState.executing
}
const next: CopilotToolCall = existing
? {
...existing,
state: ClientToolCallState.pending,
name: toolName,
state: initialState,
...(args ? { params: args } : {}),
display: resolveToolDisplay(name, ClientToolCallState.pending, id, args),
display: resolveToolDisplay(toolName, initialState, id, args || existing.params),
}
: {
id,
name: name || 'unknown_tool',
state: ClientToolCallState.pending,
name: toolName,
state: initialState,
...(args ? { params: args } : {}),
display: resolveToolDisplay(name, ClientToolCallState.pending, id, args),
display: resolveToolDisplay(toolName, initialState, id, args),
}
const updated = { ...toolCallsById, [id]: next }
set({ toolCallsById: updated })
logger.info('[toolCallsById] → pending', { id, name, params: args })
logger.info(`[toolCallsById] → ${initialState}`, { id, name: toolName, params: args })
upsertToolCallBlock(context, next)
updateStreamingMessage(set, context)
@@ -550,7 +569,7 @@ export const sseHandlers: Record<string, SSEHandler> = {
}
// OAuth: dispatch event to open the OAuth connect modal
if (name === 'oauth_request_access' && args && typeof window !== 'undefined') {
if (toolName === 'oauth_request_access' && args && typeof window !== 'undefined') {
try {
window.dispatchEvent(
new CustomEvent('open-oauth-connect', {

View File

@@ -190,12 +190,27 @@ export const subAgentSSEHandlers: Record<string, SSEHandler> = {
const existingIndex = context.subAgentToolCalls[parentToolCallId].findIndex(
(tc: CopilotToolCall) => tc.id === id
)
const existingToolCall =
existingIndex >= 0 ? context.subAgentToolCalls[parentToolCallId][existingIndex] : undefined
// Auto-allowed tools skip pending state to avoid flashing interrupt buttons
const isAutoAllowed = get().autoAllowedTools.includes(name)
let initialState = isAutoAllowed ? ClientToolCallState.executing : ClientToolCallState.pending
// Avoid flickering back to pending on partial/duplicate events once a tool is executing.
if (
existingToolCall?.state === ClientToolCallState.executing &&
initialState === ClientToolCallState.pending
) {
initialState = ClientToolCallState.executing
}
const subAgentToolCall: CopilotToolCall = {
id,
name,
state: ClientToolCallState.pending,
state: initialState,
...(args ? { params: args } : {}),
display: resolveToolDisplay(name, ClientToolCallState.pending, id, args),
display: resolveToolDisplay(name, initialState, id, args),
}
if (existingIndex >= 0) {
@@ -231,14 +246,11 @@ export const subAgentSSEHandlers: Record<string, SSEHandler> = {
// infer from presence of result data vs error (same logic as server-side
// inferToolSuccess). The Go backend uses `*bool` with omitempty so
// `success` is present when explicitly set, and absent for non-tool events.
const hasExplicitSuccess =
data?.success !== undefined || resultData.success !== undefined
const hasExplicitSuccess = data?.success !== undefined || resultData.success !== undefined
const explicitSuccess = data?.success ?? resultData.success
const hasResultData = data?.result !== undefined || resultData.result !== undefined
const hasError = !!data?.error || !!resultData.error
const success: boolean = hasExplicitSuccess
? !!explicitSuccess
: hasResultData && !hasError
const success: boolean = hasExplicitSuccess ? !!explicitSuccess : hasResultData && !hasError
if (!toolCallId) return
if (!context.subAgentToolCalls[parentToolCallId]) return

View File

@@ -1,14 +1,42 @@
import { createLogger } from '@sim/logger'
import { resolveToolDisplay } from '@/lib/copilot/store-utils'
import { ClientToolCallState } from '@/lib/copilot/tools/client/tool-display-registry'
import type { CopilotMessage, CopilotToolCall } from '@/stores/panel/copilot/types'
import { maskCredentialIdsInValue } from './credential-masking'
const logger = createLogger('CopilotMessageSerialization')
const TERMINAL_STATES = new Set<string>([
ClientToolCallState.success,
ClientToolCallState.error,
ClientToolCallState.rejected,
ClientToolCallState.aborted,
ClientToolCallState.review,
ClientToolCallState.background,
])
/**
* Clears streaming flags and normalizes non-terminal tool call states to 'aborted'.
* This ensures that tool calls loaded from DB after a refresh/abort don't render
* as in-progress with shimmer animations or interrupt buttons.
*/
export function clearStreamingFlags(toolCall: CopilotToolCall): void {
if (!toolCall) return
toolCall.subAgentStreaming = false
// Normalize non-terminal states when loading from DB.
// 'executing' → 'success': the server was running it, assume it completed.
// 'pending'/'generating' → 'aborted': never reached execution.
if (toolCall.state && !TERMINAL_STATES.has(toolCall.state)) {
const normalized =
toolCall.state === ClientToolCallState.executing
? ClientToolCallState.success
: ClientToolCallState.aborted
toolCall.state = normalized
toolCall.display = resolveToolDisplay(toolCall.name, normalized, toolCall.id, toolCall.params)
}
if (Array.isArray(toolCall.subAgentBlocks)) {
for (const block of toolCall.subAgentBlocks) {
if (block?.type === 'subagent_tool_call' && block.toolCall) {