mirror of
https://github.com/simstudioai/sim.git
synced 2026-02-13 16:05:09 -05:00
SSE interface
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { COPILOT_CONFIRM_API_PATH, STREAM_STORAGE_KEY } from '@/lib/copilot/constants'
|
||||
import { STREAM_STORAGE_KEY } from '@/lib/copilot/constants'
|
||||
import { asRecord } from '@/lib/copilot/orchestrator/sse-utils'
|
||||
import type { SSEEvent } from '@/lib/copilot/orchestrator/types'
|
||||
import {
|
||||
@@ -35,6 +35,80 @@ function isWorkflowEditToolCall(toolName?: string, params?: Record<string, unkno
|
||||
return typeof params?.proposalId === 'string' && params.proposalId.length > 0
|
||||
}
|
||||
|
||||
function isClientRunCapability(toolCall: CopilotToolCall): boolean {
|
||||
if (toolCall.execution?.target === 'sim_client_capability') {
|
||||
return toolCall.execution.capabilityId === 'workflow.run' || !toolCall.execution.capabilityId
|
||||
}
|
||||
return CLIENT_EXECUTABLE_RUN_TOOLS.has(toolCall.name)
|
||||
}
|
||||
|
||||
function mapServerStateToClientState(state: unknown): ClientToolCallState {
|
||||
switch (String(state || '')) {
|
||||
case 'generating':
|
||||
return ClientToolCallState.generating
|
||||
case 'pending':
|
||||
case 'awaiting_approval':
|
||||
return ClientToolCallState.pending
|
||||
case 'executing':
|
||||
return ClientToolCallState.executing
|
||||
case 'success':
|
||||
return ClientToolCallState.success
|
||||
case 'rejected':
|
||||
case 'skipped':
|
||||
return ClientToolCallState.rejected
|
||||
case 'aborted':
|
||||
return ClientToolCallState.aborted
|
||||
case 'error':
|
||||
case 'failed':
|
||||
return ClientToolCallState.error
|
||||
default:
|
||||
return ClientToolCallState.pending
|
||||
}
|
||||
}
|
||||
|
||||
function extractToolUiMetadata(data: Record<string, unknown>): CopilotToolCall['ui'] | undefined {
|
||||
const ui = asRecord(data.ui)
|
||||
if (!ui || Object.keys(ui).length === 0) return undefined
|
||||
const autoAllowedFromUi = ui.autoAllowed === true
|
||||
const autoAllowedFromData = data.autoAllowed === true
|
||||
return {
|
||||
title: typeof ui.title === 'string' ? ui.title : undefined,
|
||||
phaseLabel: typeof ui.phaseLabel === 'string' ? ui.phaseLabel : undefined,
|
||||
icon: typeof ui.icon === 'string' ? ui.icon : undefined,
|
||||
showInterrupt: ui.showInterrupt === true,
|
||||
showRemember: ui.showRemember === true,
|
||||
autoAllowed: autoAllowedFromUi || autoAllowedFromData,
|
||||
actions: Array.isArray(ui.actions)
|
||||
? ui.actions
|
||||
.map((action) => {
|
||||
const a = asRecord(action)
|
||||
const id = typeof a.id === 'string' ? a.id : undefined
|
||||
const label = typeof a.label === 'string' ? a.label : undefined
|
||||
const kind: 'accept' | 'reject' = a.kind === 'reject' ? 'reject' : 'accept'
|
||||
if (!id || !label) return null
|
||||
return {
|
||||
id,
|
||||
label,
|
||||
kind,
|
||||
remember: a.remember === true,
|
||||
}
|
||||
})
|
||||
.filter((a): a is NonNullable<typeof a> => !!a)
|
||||
: undefined,
|
||||
}
|
||||
}
|
||||
|
||||
function extractToolExecutionMetadata(
|
||||
data: Record<string, unknown>
|
||||
): CopilotToolCall['execution'] | undefined {
|
||||
const execution = asRecord(data.execution)
|
||||
if (!execution || Object.keys(execution).length === 0) return undefined
|
||||
return {
|
||||
target: typeof execution.target === 'string' ? execution.target : undefined,
|
||||
capabilityId: typeof execution.capabilityId === 'string' ? execution.capabilityId : undefined,
|
||||
}
|
||||
}
|
||||
|
||||
function isWorkflowChangeApplyCall(toolName?: string, params?: Record<string, unknown>): boolean {
|
||||
if (toolName !== 'workflow_change') return false
|
||||
const mode = typeof params?.mode === 'string' ? params.mode.toLowerCase() : ''
|
||||
@@ -67,23 +141,6 @@ function extractOperationListFromResultPayload(
|
||||
return undefined
|
||||
}
|
||||
|
||||
/**
|
||||
* Send an auto-accept confirmation to the server for auto-allowed tools.
|
||||
* The server-side orchestrator polls Redis for this decision.
|
||||
*/
|
||||
export function sendAutoAcceptConfirmation(toolCallId: string): void {
|
||||
fetch(COPILOT_CONFIRM_API_PATH, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ toolCallId, status: 'accepted' }),
|
||||
}).catch((error) => {
|
||||
logger.warn('Failed to send auto-accept confirmation', {
|
||||
toolCallId,
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
function writeActiveStreamToStorage(info: CopilotStreamInfo | null): void {
|
||||
if (typeof window === 'undefined') return
|
||||
try {
|
||||
@@ -285,12 +342,25 @@ export const sseHandlers: Record<string, SSEHandler> = {
|
||||
try {
|
||||
const eventData = asRecord(data?.data)
|
||||
const toolCallId: string | undefined =
|
||||
data?.toolCallId || (eventData.id as string | undefined)
|
||||
data?.toolCallId ||
|
||||
(eventData.id as string | undefined) ||
|
||||
(eventData.callId as string | undefined)
|
||||
const success: boolean | undefined = data?.success
|
||||
const failedDependency: boolean = data?.failedDependency === true
|
||||
const resultObj = asRecord(data?.result)
|
||||
const skipped: boolean = resultObj.skipped === true
|
||||
if (!toolCallId) return
|
||||
const uiMetadata = extractToolUiMetadata(eventData)
|
||||
const executionMetadata = extractToolExecutionMetadata(eventData)
|
||||
const serverState = (eventData.state as string | undefined) || undefined
|
||||
const targetState = serverState
|
||||
? mapServerStateToClientState(serverState)
|
||||
: success
|
||||
? ClientToolCallState.success
|
||||
: failedDependency || skipped
|
||||
? ClientToolCallState.rejected
|
||||
: ClientToolCallState.error
|
||||
const resultPayload = asRecord(data?.result || eventData.result || eventData.data || data?.data)
|
||||
const { toolCallsById } = get()
|
||||
const current = toolCallsById[toolCallId]
|
||||
let paramsForCurrentToolCall: Record<string, unknown> | undefined = current?.params
|
||||
@@ -302,15 +372,6 @@ export const sseHandlers: Record<string, SSEHandler> = {
|
||||
) {
|
||||
return
|
||||
}
|
||||
const targetState = success
|
||||
? ClientToolCallState.success
|
||||
: failedDependency || skipped
|
||||
? ClientToolCallState.rejected
|
||||
: ClientToolCallState.error
|
||||
const resultPayload = asRecord(
|
||||
data?.result || eventData.result || eventData.data || data?.data
|
||||
)
|
||||
|
||||
if (
|
||||
targetState === ClientToolCallState.success &&
|
||||
isWorkflowChangeApplyCall(current.name, paramsForCurrentToolCall)
|
||||
@@ -327,6 +388,8 @@ export const sseHandlers: Record<string, SSEHandler> = {
|
||||
const updatedMap = { ...toolCallsById }
|
||||
updatedMap[toolCallId] = {
|
||||
...current,
|
||||
ui: uiMetadata || current.ui,
|
||||
execution: executionMetadata || current.execution,
|
||||
params: paramsForCurrentToolCall,
|
||||
state: targetState,
|
||||
display: resolveToolDisplay(
|
||||
@@ -542,6 +605,8 @@ export const sseHandlers: Record<string, SSEHandler> = {
|
||||
toolCall: {
|
||||
...b.toolCall,
|
||||
params: paramsForBlock,
|
||||
ui: uiMetadata || b.toolCall?.ui,
|
||||
execution: executionMetadata || b.toolCall?.execution,
|
||||
state: targetState,
|
||||
display: resolveToolDisplay(
|
||||
b.toolCall?.name,
|
||||
@@ -565,7 +630,9 @@ export const sseHandlers: Record<string, SSEHandler> = {
|
||||
try {
|
||||
const errorData = asRecord(data?.data)
|
||||
const toolCallId: string | undefined =
|
||||
data?.toolCallId || (errorData.id as string | undefined)
|
||||
data?.toolCallId ||
|
||||
(errorData.id as string | undefined) ||
|
||||
(errorData.callId as string | undefined)
|
||||
const failedDependency: boolean = data?.failedDependency === true
|
||||
if (!toolCallId) return
|
||||
const { toolCallsById } = get()
|
||||
@@ -578,12 +645,18 @@ export const sseHandlers: Record<string, SSEHandler> = {
|
||||
) {
|
||||
return
|
||||
}
|
||||
const targetState = failedDependency
|
||||
? ClientToolCallState.rejected
|
||||
: ClientToolCallState.error
|
||||
const targetState = errorData.state
|
||||
? mapServerStateToClientState(errorData.state)
|
||||
: failedDependency
|
||||
? ClientToolCallState.rejected
|
||||
: ClientToolCallState.error
|
||||
const uiMetadata = extractToolUiMetadata(errorData)
|
||||
const executionMetadata = extractToolExecutionMetadata(errorData)
|
||||
const updatedMap = { ...toolCallsById }
|
||||
updatedMap[toolCallId] = {
|
||||
...current,
|
||||
ui: uiMetadata || current.ui,
|
||||
execution: executionMetadata || current.execution,
|
||||
state: targetState,
|
||||
display: resolveToolDisplay(current.name, targetState, current.id, current.params),
|
||||
}
|
||||
@@ -598,13 +671,19 @@ export const sseHandlers: Record<string, SSEHandler> = {
|
||||
isBackgroundState(b.toolCall?.state)
|
||||
)
|
||||
break
|
||||
const targetState = failedDependency
|
||||
? ClientToolCallState.rejected
|
||||
: ClientToolCallState.error
|
||||
const targetState = errorData.state
|
||||
? mapServerStateToClientState(errorData.state)
|
||||
: failedDependency
|
||||
? ClientToolCallState.rejected
|
||||
: ClientToolCallState.error
|
||||
const uiMetadata = extractToolUiMetadata(errorData)
|
||||
const executionMetadata = extractToolExecutionMetadata(errorData)
|
||||
context.contentBlocks[i] = {
|
||||
...b,
|
||||
toolCall: {
|
||||
...b.toolCall,
|
||||
ui: uiMetadata || b.toolCall?.ui,
|
||||
execution: executionMetadata || b.toolCall?.execution,
|
||||
state: targetState,
|
||||
display: resolveToolDisplay(
|
||||
b.toolCall?.name,
|
||||
@@ -625,19 +704,26 @@ export const sseHandlers: Record<string, SSEHandler> = {
|
||||
}
|
||||
},
|
||||
tool_generating: (data, context, get, set) => {
|
||||
const { toolCallId, toolName } = data
|
||||
const eventData = asRecord(data?.data)
|
||||
const toolCallId =
|
||||
data?.toolCallId ||
|
||||
(eventData.id as string | undefined) ||
|
||||
(eventData.callId as string | undefined)
|
||||
const toolName =
|
||||
data?.toolName ||
|
||||
(eventData.name as string | undefined) ||
|
||||
(eventData.toolName as string | undefined)
|
||||
if (!toolCallId || !toolName) return
|
||||
const { toolCallsById } = get()
|
||||
|
||||
if (!toolCallsById[toolCallId]) {
|
||||
const isAutoAllowed = get().isToolAutoAllowed(toolName)
|
||||
const initialState = isAutoAllowed
|
||||
? ClientToolCallState.executing
|
||||
: ClientToolCallState.pending
|
||||
const initialState = ClientToolCallState.generating
|
||||
const tc: CopilotToolCall = {
|
||||
id: toolCallId,
|
||||
name: toolName,
|
||||
state: initialState,
|
||||
ui: extractToolUiMetadata(eventData),
|
||||
execution: extractToolExecutionMetadata(eventData),
|
||||
display: resolveToolDisplay(toolName, initialState, toolCallId),
|
||||
}
|
||||
const updated = { ...toolCallsById, [toolCallId]: tc }
|
||||
@@ -650,17 +736,27 @@ export const sseHandlers: Record<string, SSEHandler> = {
|
||||
},
|
||||
tool_call: (data, context, get, set) => {
|
||||
const toolData = asRecord(data?.data)
|
||||
const id: string | undefined = (toolData.id as string | undefined) || data?.toolCallId
|
||||
const name: string | undefined = (toolData.name as string | undefined) || data?.toolName
|
||||
const id: string | undefined =
|
||||
(toolData.id as string | undefined) ||
|
||||
(toolData.callId as string | undefined) ||
|
||||
data?.toolCallId
|
||||
const name: string | undefined =
|
||||
(toolData.name as string | undefined) ||
|
||||
(toolData.toolName as string | undefined) ||
|
||||
data?.toolName
|
||||
if (!id) return
|
||||
const args = toolData.arguments as Record<string, unknown> | undefined
|
||||
const isPartial = toolData.partial === true
|
||||
const uiMetadata = extractToolUiMetadata(toolData)
|
||||
const executionMetadata = extractToolExecutionMetadata(toolData)
|
||||
const serverState = toolData.state
|
||||
const { toolCallsById } = get()
|
||||
|
||||
const existing = toolCallsById[id]
|
||||
const toolName = name || existing?.name || 'unknown_tool'
|
||||
const isAutoAllowed = get().isToolAutoAllowed(toolName)
|
||||
let initialState = isAutoAllowed ? ClientToolCallState.executing : ClientToolCallState.pending
|
||||
let initialState = serverState
|
||||
? mapServerStateToClientState(serverState)
|
||||
: ClientToolCallState.pending
|
||||
|
||||
// Avoid flickering back to pending on partial/duplicate events once a tool is executing.
|
||||
if (
|
||||
@@ -675,6 +771,8 @@ export const sseHandlers: Record<string, SSEHandler> = {
|
||||
...existing,
|
||||
name: toolName,
|
||||
state: initialState,
|
||||
ui: uiMetadata || existing.ui,
|
||||
execution: executionMetadata || existing.execution,
|
||||
...(args ? { params: args } : {}),
|
||||
display: resolveToolDisplay(toolName, initialState, id, args || existing.params),
|
||||
}
|
||||
@@ -682,6 +780,8 @@ export const sseHandlers: Record<string, SSEHandler> = {
|
||||
id,
|
||||
name: toolName,
|
||||
state: initialState,
|
||||
ui: uiMetadata,
|
||||
execution: executionMetadata,
|
||||
...(args ? { params: args } : {}),
|
||||
display: resolveToolDisplay(toolName, initialState, id, args),
|
||||
}
|
||||
@@ -696,20 +796,12 @@ export const sseHandlers: Record<string, SSEHandler> = {
|
||||
return
|
||||
}
|
||||
|
||||
// Auto-allowed tools: send confirmation to the server so it can proceed
|
||||
// without waiting for the user to click "Allow".
|
||||
if (isAutoAllowed) {
|
||||
sendAutoAcceptConfirmation(id)
|
||||
}
|
||||
const shouldInterrupt = next.ui?.showInterrupt === true
|
||||
|
||||
// Client-executable run tools: execute on the client for real-time feedback
|
||||
// (block pulsing, console logs, stop button). The server defers execution
|
||||
// for these tools in interactive mode; the client reports back via mark-complete.
|
||||
if (
|
||||
CLIENT_EXECUTABLE_RUN_TOOLS.has(toolName) &&
|
||||
initialState === ClientToolCallState.executing
|
||||
) {
|
||||
executeRunToolOnClient(id, toolName, args || existing?.params || {})
|
||||
// Client-run capability: execution is delegated to the browser.
|
||||
// We run immediately only when no interrupt is required.
|
||||
if (isClientRunCapability(next) && !shouldInterrupt) {
|
||||
executeRunToolOnClient(id, toolName, args || next.params || {})
|
||||
}
|
||||
|
||||
// OAuth: dispatch event to open the OAuth connect modal
|
||||
|
||||
@@ -13,7 +13,6 @@ import { useWorkflowDiffStore } from '@/stores/workflow-diff/store'
|
||||
import type { WorkflowState } from '@/stores/workflows/workflow/types'
|
||||
import {
|
||||
type SSEHandler,
|
||||
sendAutoAcceptConfirmation,
|
||||
sseHandlers,
|
||||
updateStreamingMessage,
|
||||
} from './handlers'
|
||||
@@ -26,6 +25,80 @@ type StoreSet = (
|
||||
partial: Partial<CopilotStore> | ((state: CopilotStore) => Partial<CopilotStore>)
|
||||
) => void
|
||||
|
||||
function mapServerStateToClientState(state: unknown): ClientToolCallState {
|
||||
switch (String(state || '')) {
|
||||
case 'generating':
|
||||
return ClientToolCallState.generating
|
||||
case 'pending':
|
||||
case 'awaiting_approval':
|
||||
return ClientToolCallState.pending
|
||||
case 'executing':
|
||||
return ClientToolCallState.executing
|
||||
case 'success':
|
||||
return ClientToolCallState.success
|
||||
case 'rejected':
|
||||
case 'skipped':
|
||||
return ClientToolCallState.rejected
|
||||
case 'aborted':
|
||||
return ClientToolCallState.aborted
|
||||
case 'error':
|
||||
case 'failed':
|
||||
return ClientToolCallState.error
|
||||
default:
|
||||
return ClientToolCallState.pending
|
||||
}
|
||||
}
|
||||
|
||||
function extractToolUiMetadata(data: Record<string, unknown>): CopilotToolCall['ui'] | undefined {
|
||||
const ui = asRecord(data.ui)
|
||||
if (!ui || Object.keys(ui).length === 0) return undefined
|
||||
const autoAllowedFromUi = ui.autoAllowed === true
|
||||
const autoAllowedFromData = data.autoAllowed === true
|
||||
return {
|
||||
title: typeof ui.title === 'string' ? ui.title : undefined,
|
||||
phaseLabel: typeof ui.phaseLabel === 'string' ? ui.phaseLabel : undefined,
|
||||
icon: typeof ui.icon === 'string' ? ui.icon : undefined,
|
||||
showInterrupt: ui.showInterrupt === true,
|
||||
showRemember: ui.showRemember === true,
|
||||
autoAllowed: autoAllowedFromUi || autoAllowedFromData,
|
||||
actions: Array.isArray(ui.actions)
|
||||
? ui.actions
|
||||
.map((action) => {
|
||||
const a = asRecord(action)
|
||||
const id = typeof a.id === 'string' ? a.id : undefined
|
||||
const label = typeof a.label === 'string' ? a.label : undefined
|
||||
const kind: 'accept' | 'reject' = a.kind === 'reject' ? 'reject' : 'accept'
|
||||
if (!id || !label) return null
|
||||
return {
|
||||
id,
|
||||
label,
|
||||
kind,
|
||||
remember: a.remember === true,
|
||||
}
|
||||
})
|
||||
.filter((a): a is NonNullable<typeof a> => !!a)
|
||||
: undefined,
|
||||
}
|
||||
}
|
||||
|
||||
function extractToolExecutionMetadata(
|
||||
data: Record<string, unknown>
|
||||
): CopilotToolCall['execution'] | undefined {
|
||||
const execution = asRecord(data.execution)
|
||||
if (!execution || Object.keys(execution).length === 0) return undefined
|
||||
return {
|
||||
target: typeof execution.target === 'string' ? execution.target : undefined,
|
||||
capabilityId: typeof execution.capabilityId === 'string' ? execution.capabilityId : undefined,
|
||||
}
|
||||
}
|
||||
|
||||
function isClientRunCapability(toolCall: CopilotToolCall): boolean {
|
||||
if (toolCall.execution?.target === 'sim_client_capability') {
|
||||
return toolCall.execution.capabilityId === 'workflow.run' || !toolCall.execution.capabilityId
|
||||
}
|
||||
return CLIENT_EXECUTABLE_RUN_TOOLS.has(toolCall.name)
|
||||
}
|
||||
|
||||
function isWorkflowChangeApplyCall(toolCall: CopilotToolCall): boolean {
|
||||
if (toolCall.name !== 'workflow_change') return false
|
||||
const params = (toolCall.params || {}) as Record<string, unknown>
|
||||
@@ -199,6 +272,8 @@ export const subAgentSSEHandlers: Record<string, SSEHandler> = {
|
||||
const name: string | undefined = (toolData.name as string | undefined) || data?.toolName
|
||||
if (!id || !name) return
|
||||
const isPartial = toolData.partial === true
|
||||
const uiMetadata = extractToolUiMetadata(toolData)
|
||||
const executionMetadata = extractToolExecutionMetadata(toolData)
|
||||
|
||||
let args: Record<string, unknown> | undefined = (toolData.arguments || toolData.input) as
|
||||
| Record<string, unknown>
|
||||
@@ -234,9 +309,10 @@ export const subAgentSSEHandlers: Record<string, SSEHandler> = {
|
||||
const existingToolCall =
|
||||
existingIndex >= 0 ? context.subAgentToolCalls[parentToolCallId][existingIndex] : undefined
|
||||
|
||||
// Auto-allowed tools skip pending state to avoid flashing interrupt buttons
|
||||
const isAutoAllowed = get().isToolAutoAllowed(name)
|
||||
let initialState = isAutoAllowed ? ClientToolCallState.executing : ClientToolCallState.pending
|
||||
const serverState = toolData.state
|
||||
let initialState = serverState
|
||||
? mapServerStateToClientState(serverState)
|
||||
: ClientToolCallState.pending
|
||||
|
||||
// Avoid flickering back to pending on partial/duplicate events once a tool is executing.
|
||||
if (
|
||||
@@ -250,6 +326,8 @@ export const subAgentSSEHandlers: Record<string, SSEHandler> = {
|
||||
id,
|
||||
name,
|
||||
state: initialState,
|
||||
ui: uiMetadata,
|
||||
execution: executionMetadata,
|
||||
...(args ? { params: args } : {}),
|
||||
display: resolveToolDisplay(name, initialState, id, args),
|
||||
}
|
||||
@@ -276,16 +354,11 @@ export const subAgentSSEHandlers: Record<string, SSEHandler> = {
|
||||
return
|
||||
}
|
||||
|
||||
// Auto-allowed tools: send confirmation to the server so it can proceed
|
||||
// without waiting for the user to click "Allow".
|
||||
if (isAutoAllowed) {
|
||||
sendAutoAcceptConfirmation(id)
|
||||
}
|
||||
const shouldInterrupt = subAgentToolCall.ui?.showInterrupt === true
|
||||
|
||||
// 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) {
|
||||
// Client-run capability: execution is delegated to the browser.
|
||||
// Execute immediately only for non-interrupting calls.
|
||||
if (isClientRunCapability(subAgentToolCall) && !shouldInterrupt) {
|
||||
executeRunToolOnClient(id, name, args || {})
|
||||
}
|
||||
},
|
||||
@@ -310,7 +383,14 @@ export const subAgentSSEHandlers: Record<string, SSEHandler> = {
|
||||
if (!context.subAgentToolCalls[parentToolCallId]) return
|
||||
if (!context.subAgentBlocks[parentToolCallId]) return
|
||||
|
||||
const targetState = success ? ClientToolCallState.success : ClientToolCallState.error
|
||||
const serverState = resultData.state
|
||||
const targetState = serverState
|
||||
? mapServerStateToClientState(serverState)
|
||||
: success
|
||||
? ClientToolCallState.success
|
||||
: ClientToolCallState.error
|
||||
const uiMetadata = extractToolUiMetadata(resultData)
|
||||
const executionMetadata = extractToolExecutionMetadata(resultData)
|
||||
const existingIndex = context.subAgentToolCalls[parentToolCallId].findIndex(
|
||||
(tc: CopilotToolCall) => tc.id === toolCallId
|
||||
)
|
||||
@@ -338,6 +418,8 @@ export const subAgentSSEHandlers: Record<string, SSEHandler> = {
|
||||
const updatedSubAgentToolCall = {
|
||||
...existing,
|
||||
params: nextParams,
|
||||
ui: uiMetadata || existing.ui,
|
||||
execution: executionMetadata || existing.execution,
|
||||
state: targetState,
|
||||
display: resolveToolDisplay(existing.name, targetState, toolCallId, nextParams),
|
||||
}
|
||||
|
||||
@@ -101,9 +101,6 @@ export const COPILOT_CHECKPOINTS_API_PATH = '/api/copilot/checkpoints'
|
||||
/** POST — revert to a checkpoint. */
|
||||
export const COPILOT_CHECKPOINTS_REVERT_API_PATH = '/api/copilot/checkpoints/revert'
|
||||
|
||||
/** GET/POST/DELETE — manage auto-allowed tools. */
|
||||
export const COPILOT_AUTO_ALLOWED_TOOLS_API_PATH = '/api/copilot/auto-allowed-tools'
|
||||
|
||||
/** GET — fetch dynamically available copilot models. */
|
||||
export const COPILOT_MODELS_API_PATH = '/api/copilot/models'
|
||||
|
||||
|
||||
@@ -1,67 +0,0 @@
|
||||
export const INTERRUPT_TOOL_NAMES = [
|
||||
'set_global_workflow_variables',
|
||||
'run_workflow',
|
||||
'run_workflow_until_block',
|
||||
'run_from_block',
|
||||
'run_block',
|
||||
'manage_mcp_tool',
|
||||
'manage_custom_tool',
|
||||
'deploy_mcp',
|
||||
'deploy_chat',
|
||||
'deploy_api',
|
||||
'create_workspace_mcp_server',
|
||||
'set_environment_variables',
|
||||
'make_api_request',
|
||||
'oauth_request_access',
|
||||
'navigate_ui',
|
||||
'knowledge_base',
|
||||
'generate_api_key',
|
||||
] as const
|
||||
|
||||
export const INTERRUPT_TOOL_SET = new Set<string>(INTERRUPT_TOOL_NAMES)
|
||||
|
||||
export const SUBAGENT_TOOL_NAMES = [
|
||||
'debug',
|
||||
'edit',
|
||||
'build',
|
||||
'plan',
|
||||
'test',
|
||||
'deploy',
|
||||
'auth',
|
||||
'research',
|
||||
'knowledge',
|
||||
'custom_tool',
|
||||
'tour',
|
||||
'info',
|
||||
'workflow',
|
||||
'evaluate',
|
||||
'superagent',
|
||||
'discovery',
|
||||
] as const
|
||||
|
||||
export const SUBAGENT_TOOL_SET = new Set<string>(SUBAGENT_TOOL_NAMES)
|
||||
|
||||
/**
|
||||
* Respond tools are internal to the copilot's subagent system.
|
||||
* They're used by subagents to signal completion and should NOT be executed by the sim side.
|
||||
* The copilot backend handles these internally.
|
||||
*/
|
||||
export const RESPOND_TOOL_NAMES = [
|
||||
'plan_respond',
|
||||
'edit_respond',
|
||||
'build_respond',
|
||||
'debug_respond',
|
||||
'info_respond',
|
||||
'research_respond',
|
||||
'deploy_respond',
|
||||
'superagent_respond',
|
||||
'discovery_respond',
|
||||
'tour_respond',
|
||||
'auth_respond',
|
||||
'workflow_respond',
|
||||
'knowledge_respond',
|
||||
'custom_tool_respond',
|
||||
'test_respond',
|
||||
] as const
|
||||
|
||||
export const RESPOND_TOOL_SET = new Set<string>(RESPOND_TOOL_NAMES)
|
||||
@@ -1,17 +1,12 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { STREAM_TIMEOUT_MS } from '@/lib/copilot/constants'
|
||||
import { RESPOND_TOOL_SET, SUBAGENT_TOOL_SET } from '@/lib/copilot/orchestrator/config'
|
||||
import {
|
||||
asRecord,
|
||||
getEventData,
|
||||
markToolResultSeen,
|
||||
wasToolResultSeen,
|
||||
} from '@/lib/copilot/orchestrator/sse-utils'
|
||||
import {
|
||||
isIntegrationTool,
|
||||
isToolAvailableOnSimSide,
|
||||
markToolComplete,
|
||||
} from '@/lib/copilot/orchestrator/tool-executor'
|
||||
import { markToolComplete } from '@/lib/copilot/orchestrator/tool-executor'
|
||||
import type {
|
||||
ContentBlock,
|
||||
ExecutionContext,
|
||||
@@ -22,7 +17,6 @@ import type {
|
||||
} from '@/lib/copilot/orchestrator/types'
|
||||
import {
|
||||
executeToolAndReport,
|
||||
isInterruptToolName,
|
||||
waitForToolCompletion,
|
||||
waitForToolDecision,
|
||||
} from './tool-execution'
|
||||
@@ -41,6 +35,113 @@ const CLIENT_EXECUTABLE_RUN_TOOLS = new Set([
|
||||
'run_block',
|
||||
])
|
||||
|
||||
function mapServerStateToToolStatus(state: unknown): ToolCallState['status'] {
|
||||
switch (String(state || '')) {
|
||||
case 'generating':
|
||||
case 'pending':
|
||||
case 'awaiting_approval':
|
||||
return 'pending'
|
||||
case 'executing':
|
||||
return 'executing'
|
||||
case 'success':
|
||||
return 'success'
|
||||
case 'rejected':
|
||||
case 'skipped':
|
||||
return 'rejected'
|
||||
case 'aborted':
|
||||
return 'skipped'
|
||||
case 'error':
|
||||
case 'failed':
|
||||
return 'error'
|
||||
default:
|
||||
return 'pending'
|
||||
}
|
||||
}
|
||||
|
||||
function getExecutionTarget(
|
||||
toolData: Record<string, unknown>,
|
||||
toolName: string
|
||||
): { target: string; capabilityId?: string } {
|
||||
const execution = asRecord(toolData.execution)
|
||||
if (typeof execution.target === 'string' && execution.target.length > 0) {
|
||||
return {
|
||||
target: execution.target,
|
||||
capabilityId:
|
||||
typeof execution.capabilityId === 'string' ? execution.capabilityId : undefined,
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback only when metadata is missing.
|
||||
if (CLIENT_EXECUTABLE_RUN_TOOLS.has(toolName)) {
|
||||
return { target: 'sim_client_capability', capabilityId: 'workflow.run' }
|
||||
}
|
||||
return { target: 'sim_server' }
|
||||
}
|
||||
|
||||
function needsApproval(toolData: Record<string, unknown>): boolean {
|
||||
const ui = asRecord(toolData.ui)
|
||||
return ui.showInterrupt === true
|
||||
}
|
||||
|
||||
async function waitForClientCapabilityAndReport(
|
||||
toolCall: ToolCallState,
|
||||
options: OrchestratorOptions,
|
||||
logScope: string
|
||||
): Promise<void> {
|
||||
toolCall.status = 'executing'
|
||||
const completion = await waitForToolCompletion(
|
||||
toolCall.id,
|
||||
options.timeout || STREAM_TIMEOUT_MS,
|
||||
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 (${logScope} background)`, {
|
||||
toolCallId: toolCall.id,
|
||||
error: err instanceof Error ? err.message : String(err),
|
||||
})
|
||||
})
|
||||
markToolResultSeen(toolCall.id)
|
||||
return
|
||||
}
|
||||
|
||||
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 (${logScope} rejected)`, {
|
||||
toolCallId: toolCall.id,
|
||||
error: err instanceof Error ? err.message : String(err),
|
||||
})
|
||||
})
|
||||
markToolResultSeen(toolCall.id)
|
||||
return
|
||||
}
|
||||
|
||||
const success = completion?.status === 'success'
|
||||
toolCall.status = success ? 'success' : 'error'
|
||||
toolCall.endTime = Date.now()
|
||||
const msg = completion?.message || (success ? 'Tool completed' : 'Tool failed or timed out')
|
||||
markToolComplete(toolCall.id, toolCall.name, success ? 200 : 500, msg).catch((err) => {
|
||||
logger.error(`markToolComplete fire-and-forget failed (${logScope})`, {
|
||||
toolCallId: toolCall.id,
|
||||
toolName: toolCall.name,
|
||||
error: err instanceof Error ? err.message : String(err),
|
||||
})
|
||||
})
|
||||
markToolResultSeen(toolCall.id)
|
||||
}
|
||||
|
||||
// Normalization + dedupe helpers live in sse-utils to keep server/client in sync.
|
||||
|
||||
function inferToolSuccess(data: Record<string, unknown> | undefined): {
|
||||
@@ -85,7 +186,11 @@ export const sseHandlers: Record<string, SSEHandler> = {
|
||||
|
||||
const { success, hasResultData, hasError } = inferToolSuccess(data)
|
||||
|
||||
current.status = success ? 'success' : 'error'
|
||||
current.status = data?.state
|
||||
? mapServerStateToToolStatus(data.state)
|
||||
: success
|
||||
? 'success'
|
||||
: 'error'
|
||||
current.endTime = Date.now()
|
||||
if (hasResultData) {
|
||||
current.result = {
|
||||
@@ -104,7 +209,7 @@ export const sseHandlers: Record<string, SSEHandler> = {
|
||||
if (!toolCallId) return
|
||||
const current = context.toolCalls.get(toolCallId)
|
||||
if (!current) return
|
||||
current.status = 'error'
|
||||
current.status = data?.state ? mapServerStateToToolStatus(data.state) : 'error'
|
||||
current.error = (data?.error as string | undefined) || 'Tool execution failed'
|
||||
current.endTime = Date.now()
|
||||
},
|
||||
@@ -121,7 +226,7 @@ export const sseHandlers: Record<string, SSEHandler> = {
|
||||
context.toolCalls.set(toolCallId, {
|
||||
id: toolCallId,
|
||||
name: toolName,
|
||||
status: 'pending',
|
||||
status: data?.state ? mapServerStateToToolStatus(data.state) : 'pending',
|
||||
startTime: Date.now(),
|
||||
})
|
||||
}
|
||||
@@ -156,7 +261,7 @@ export const sseHandlers: Record<string, SSEHandler> = {
|
||||
context.toolCalls.set(toolCallId, {
|
||||
id: toolCallId,
|
||||
name: toolName,
|
||||
status: 'pending',
|
||||
status: toolData.state ? mapServerStateToToolStatus(toolData.state) : 'pending',
|
||||
params: args,
|
||||
startTime: Date.now(),
|
||||
})
|
||||
@@ -170,83 +275,29 @@ export const sseHandlers: Record<string, SSEHandler> = {
|
||||
const toolCall = context.toolCalls.get(toolCallId)
|
||||
if (!toolCall) return
|
||||
|
||||
// Subagent tools are executed by the copilot backend, not sim side.
|
||||
if (SUBAGENT_TOOL_SET.has(toolName)) {
|
||||
return
|
||||
}
|
||||
|
||||
// Respond tools are internal to copilot's subagent system - skip execution.
|
||||
// The copilot backend handles these internally to signal subagent completion.
|
||||
if (RESPOND_TOOL_SET.has(toolName)) {
|
||||
toolCall.status = 'success'
|
||||
toolCall.endTime = Date.now()
|
||||
toolCall.result = {
|
||||
success: true,
|
||||
output: 'Internal respond tool - handled by copilot backend',
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
const isInterruptTool = isInterruptToolName(toolName)
|
||||
const execution = getExecutionTarget(toolData, toolName)
|
||||
const isInteractive = options.interactive === true
|
||||
// Integration tools (user-installed) also require approval in interactive mode
|
||||
const needsApproval = isInterruptTool || isIntegrationTool(toolName)
|
||||
const requiresApproval = isInteractive && needsApproval(toolData)
|
||||
if (toolData.state) {
|
||||
toolCall.status = mapServerStateToToolStatus(toolData.state)
|
||||
}
|
||||
|
||||
if (needsApproval && isInteractive) {
|
||||
if (requiresApproval) {
|
||||
const decision = await waitForToolDecision(
|
||||
toolCallId,
|
||||
options.timeout || STREAM_TIMEOUT_MS,
|
||||
options.abortSignal
|
||||
)
|
||||
if (decision?.status === 'accepted' || decision?.status === 'success') {
|
||||
// Client-executable run tools: defer execution to the browser client.
|
||||
// The client calls executeWorkflowWithFullLogging for real-time feedback
|
||||
// (block pulsing, logs, stop button) and reports completion via
|
||||
// /api/copilot/confirm with status success/error. We poll Redis for
|
||||
// that completion signal, then fire-and-forget markToolComplete to Go.
|
||||
if (CLIENT_EXECUTABLE_RUN_TOOLS.has(toolName)) {
|
||||
toolCall.status = 'executing'
|
||||
const completion = await waitForToolCompletion(
|
||||
toolCallId,
|
||||
options.timeout || STREAM_TIMEOUT_MS,
|
||||
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'
|
||||
toolCall.status = success ? 'success' : 'error'
|
||||
toolCall.endTime = Date.now()
|
||||
const msg =
|
||||
completion?.message || (success ? 'Tool completed' : 'Tool failed or timed out')
|
||||
// Fire-and-forget: tell Go backend the tool is done
|
||||
// (must NOT await — see deadlock note in executeToolAndReport)
|
||||
markToolComplete(toolCall.id, toolCall.name, success ? 200 : 500, msg).catch((err) => {
|
||||
logger.error('markToolComplete fire-and-forget failed (run tool)', {
|
||||
toolCallId: toolCall.id,
|
||||
toolName: toolCall.name,
|
||||
error: err instanceof Error ? err.message : String(err),
|
||||
})
|
||||
})
|
||||
markToolResultSeen(toolCallId)
|
||||
if (execution.target === 'sim_client_capability' && isInteractive) {
|
||||
await waitForClientCapabilityAndReport(toolCall, options, 'run tool')
|
||||
return
|
||||
}
|
||||
await executeToolAndReport(toolCallId, context, execContext, options)
|
||||
if (execution.target === 'sim_server' || execution.target === 'sim_client_capability') {
|
||||
if (options.autoExecuteTools !== false) {
|
||||
await executeToolAndReport(toolCallId, context, execContext, options)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
@@ -308,7 +359,15 @@ export const sseHandlers: Record<string, SSEHandler> = {
|
||||
return
|
||||
}
|
||||
|
||||
if (options.autoExecuteTools !== false) {
|
||||
if (execution.target === 'sim_client_capability' && isInteractive) {
|
||||
await waitForClientCapabilityAndReport(toolCall, options, 'run tool')
|
||||
return
|
||||
}
|
||||
|
||||
if (
|
||||
(execution.target === 'sim_server' || execution.target === 'sim_client_capability') &&
|
||||
options.autoExecuteTools !== false
|
||||
) {
|
||||
await executeToolAndReport(toolCallId, context, execContext, options)
|
||||
}
|
||||
},
|
||||
@@ -410,7 +469,7 @@ export const subAgentHandlers: Record<string, SSEHandler> = {
|
||||
const toolCall: ToolCallState = {
|
||||
id: toolCallId,
|
||||
name: toolName,
|
||||
status: 'pending',
|
||||
status: toolData.state ? mapServerStateToToolStatus(toolData.state) : 'pending',
|
||||
params: args,
|
||||
startTime: Date.now(),
|
||||
}
|
||||
@@ -428,37 +487,26 @@ export const subAgentHandlers: Record<string, SSEHandler> = {
|
||||
|
||||
if (isPartial) return
|
||||
|
||||
// Respond tools are internal to copilot's subagent system - skip execution.
|
||||
if (RESPOND_TOOL_SET.has(toolName)) {
|
||||
toolCall.status = 'success'
|
||||
toolCall.endTime = Date.now()
|
||||
toolCall.result = {
|
||||
success: true,
|
||||
output: 'Internal respond tool - handled by copilot backend',
|
||||
}
|
||||
return
|
||||
}
|
||||
const execution = getExecutionTarget(toolData, toolName)
|
||||
const isInteractive = options.interactive === true
|
||||
const requiresApproval = isInteractive && needsApproval(toolData)
|
||||
|
||||
// Tools that only exist on the Go backend (e.g. search_patterns,
|
||||
// search_errors, remember_debug) should NOT be re-executed on the Sim side.
|
||||
// The Go backend already executed them and will send its own tool_result
|
||||
// SSE event with the real outcome. Trying to execute them here would fail
|
||||
// with "Tool not found" and incorrectly mark the tool as failed.
|
||||
if (!isToolAvailableOnSimSide(toolName)) {
|
||||
return
|
||||
}
|
||||
|
||||
// Interrupt tools and integration tools (user-installed) require approval
|
||||
// in interactive mode, same as top-level handler.
|
||||
const needsSubagentApproval = isInterruptToolName(toolName) || isIntegrationTool(toolName)
|
||||
if (options.interactive === true && needsSubagentApproval) {
|
||||
if (requiresApproval) {
|
||||
const decision = await waitForToolDecision(
|
||||
toolCallId,
|
||||
options.timeout || STREAM_TIMEOUT_MS,
|
||||
options.abortSignal
|
||||
)
|
||||
if (decision?.status === 'accepted' || decision?.status === 'success') {
|
||||
await executeToolAndReport(toolCallId, context, execContext, options)
|
||||
if (execution.target === 'sim_client_capability' && isInteractive) {
|
||||
await waitForClientCapabilityAndReport(toolCall, options, 'subagent run tool')
|
||||
return
|
||||
}
|
||||
if (execution.target === 'sim_server' || execution.target === 'sim_client_capability') {
|
||||
if (options.autoExecuteTools !== false) {
|
||||
await executeToolAndReport(toolCallId, context, execContext, options)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
if (decision?.status === 'rejected' || decision?.status === 'error') {
|
||||
@@ -517,66 +565,15 @@ export const subAgentHandlers: Record<string, SSEHandler> = {
|
||||
return
|
||||
}
|
||||
|
||||
// Client-executable run tools in interactive mode: defer to client.
|
||||
// Same pattern as main handler: wait for client completion, then tell Go.
|
||||
if (options.interactive === true && CLIENT_EXECUTABLE_RUN_TOOLS.has(toolName)) {
|
||||
toolCall.status = 'executing'
|
||||
const completion = await waitForToolCompletion(
|
||||
toolCallId,
|
||||
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
|
||||
}
|
||||
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'
|
||||
toolCall.status = success ? 'success' : 'error'
|
||||
toolCall.endTime = Date.now()
|
||||
const msg = completion?.message || (success ? 'Tool completed' : 'Tool failed or timed out')
|
||||
markToolComplete(toolCall.id, toolCall.name, success ? 200 : 500, msg).catch((err) => {
|
||||
logger.error('markToolComplete fire-and-forget failed (subagent run tool)', {
|
||||
toolCallId: toolCall.id,
|
||||
toolName: toolCall.name,
|
||||
error: err instanceof Error ? err.message : String(err),
|
||||
})
|
||||
})
|
||||
markToolResultSeen(toolCallId)
|
||||
if (execution.target === 'sim_client_capability' && isInteractive) {
|
||||
await waitForClientCapabilityAndReport(toolCall, options, 'subagent run tool')
|
||||
return
|
||||
}
|
||||
|
||||
if (options.autoExecuteTools !== false) {
|
||||
if (
|
||||
(execution.target === 'sim_server' || execution.target === 'sim_client_capability') &&
|
||||
options.autoExecuteTools !== false
|
||||
) {
|
||||
await executeToolAndReport(toolCallId, context, execContext, options)
|
||||
}
|
||||
},
|
||||
@@ -596,7 +593,7 @@ export const subAgentHandlers: Record<string, SSEHandler> = {
|
||||
|
||||
const { success, hasResultData, hasError } = inferToolSuccess(data)
|
||||
|
||||
const status = success ? 'success' : 'error'
|
||||
const status = data?.state ? mapServerStateToToolStatus(data.state) : success ? 'success' : 'error'
|
||||
const endTime = Date.now()
|
||||
const result = hasResultData ? { success, output: data?.result || data?.data } : undefined
|
||||
|
||||
|
||||
@@ -4,7 +4,6 @@ import {
|
||||
TOOL_DECISION_MAX_POLL_MS,
|
||||
TOOL_DECISION_POLL_BACKOFF,
|
||||
} from '@/lib/copilot/constants'
|
||||
import { INTERRUPT_TOOL_SET } from '@/lib/copilot/orchestrator/config'
|
||||
import { getToolConfirmation } from '@/lib/copilot/orchestrator/persistence'
|
||||
import {
|
||||
asRecord,
|
||||
@@ -21,10 +20,6 @@ import type {
|
||||
|
||||
const logger = createLogger('CopilotSseToolExecution')
|
||||
|
||||
export function isInterruptToolName(toolName: string): boolean {
|
||||
return INTERRUPT_TOOL_SET.has(toolName)
|
||||
}
|
||||
|
||||
export async function executeToolAndReport(
|
||||
toolCallId: string,
|
||||
context: StreamingContext,
|
||||
@@ -34,9 +29,11 @@ export async function executeToolAndReport(
|
||||
const toolCall = context.toolCalls.get(toolCallId)
|
||||
if (!toolCall) return
|
||||
|
||||
if (toolCall.status === 'executing') return
|
||||
const lockable = toolCall as typeof toolCall & { __simExecuting?: boolean }
|
||||
if (lockable.__simExecuting) return
|
||||
if (wasToolResultSeen(toolCall.id)) return
|
||||
|
||||
lockable.__simExecuting = true
|
||||
toolCall.status = 'executing'
|
||||
try {
|
||||
const result = await executeToolServerSide(toolCall, execContext)
|
||||
@@ -122,6 +119,8 @@ export async function executeToolAndReport(
|
||||
},
|
||||
}
|
||||
await options?.onEvent?.(errorEvent)
|
||||
} finally {
|
||||
delete lockable.__simExecuting
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user