From 38e2aa0efa5c9eb4a4714dac313594594df33e03 Mon Sep 17 00:00:00 2001 From: Siddharth Ganesan Date: Sat, 7 Feb 2026 11:37:26 -0800 Subject: [PATCH] Fix discovery tool --- apps/sim/app/api/mcp/copilot/route.ts | 34 ++++++++---- .../components/tool-call/tool-call.tsx | 8 +-- .../orchestrator/sse-handlers/handlers.ts | 1 + .../sse-handlers/tool-execution.ts | 25 +++++++-- .../orchestrator/tool-executor/index.ts | 53 ++++++++++++------- apps/sim/lib/copilot/store-utils.ts | 4 ++ .../tools/client/tool-display-registry.ts | 13 +++++ 7 files changed, 99 insertions(+), 39 deletions(-) diff --git a/apps/sim/app/api/mcp/copilot/route.ts b/apps/sim/app/api/mcp/copilot/route.ts index 7692ef532..a49288994 100644 --- a/apps/sim/app/api/mcp/copilot/route.ts +++ b/apps/sim/app/api/mcp/copilot/route.ts @@ -18,7 +18,11 @@ import { eq, sql } from 'drizzle-orm' import { type NextRequest, NextResponse } from 'next/server' import { getHighestPrioritySubscription } from '@/lib/billing/core/subscription' import { getCopilotModel } from '@/lib/copilot/config' -import { SIM_AGENT_API_URL, SIM_AGENT_VERSION } from '@/lib/copilot/constants' +import { + ORCHESTRATION_TIMEOUT_MS, + SIM_AGENT_API_URL, + SIM_AGENT_VERSION, +} from '@/lib/copilot/constants' import { RateLimiter } from '@/lib/core/rate-limiter' import { env } from '@/lib/core/config/env' import { orchestrateCopilotStream } from '@/lib/copilot/orchestrator' @@ -35,6 +39,7 @@ const mcpRateLimiter = new RateLimiter() export const dynamic = 'force-dynamic' export const runtime = 'nodejs' +export const maxDuration = 300 interface CopilotKeyAuthResult { success: boolean @@ -358,7 +363,7 @@ class NextResponseCapture { } } -function buildMcpServer(): Server { +function buildMcpServer(abortSignal?: AbortSignal): Server { const server = new Server( { name: 'sim-copilot', @@ -449,7 +454,8 @@ function buildMcpServer(): Server { name: params.name, arguments: params.arguments, }, - authResult.userId + authResult.userId, + abortSignal ) trackMcpCopilotCall(authResult.userId) @@ -464,7 +470,7 @@ async function handleMcpRequestWithSdk( request: NextRequest, parsedBody: unknown ): Promise { - const server = buildMcpServer() + const server = buildMcpServer(request.signal) const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined, enableJsonResponse: true, @@ -481,7 +487,10 @@ async function handleMcpRequestWithSdk( try { await transport.handleRequest(requestAdapter as any, responseCapture as any, parsedBody) await responseCapture.waitForHeaders() - await responseCapture.waitForEnd() + // Must exceed the longest possible tool execution (build = 5 min). + // Using ORCHESTRATION_TIMEOUT_MS + 60 s buffer so the orchestrator can + // finish or time-out on its own before the transport is torn down. + await responseCapture.waitForEnd(ORCHESTRATION_TIMEOUT_MS + 60_000) return responseCapture.toNextResponse() } finally { await server.close().catch(() => {}) @@ -540,7 +549,8 @@ function trackMcpCopilotCall(userId: string): void { async function handleToolsCall( params: { name: string; arguments?: Record }, - userId: string + userId: string, + abortSignal?: AbortSignal ): Promise { const args = params.arguments || {} @@ -551,7 +561,7 @@ async function handleToolsCall( const subagentTool = SUBAGENT_TOOL_DEFS.find((tool) => tool.name === params.name) if (subagentTool) { - return handleSubagentToolCall(subagentTool, args, userId) + return handleSubagentToolCall(subagentTool, args, userId, abortSignal) } throw new McpError(ErrorCode.MethodNotFound, `Tool not found: ${params.name}`) @@ -606,7 +616,8 @@ async function handleDirectToolCall( */ async function handleBuildToolCall( args: Record, - userId: string + userId: string, + abortSignal?: AbortSignal ): Promise { try { const requestText = (args.request as string) || JSON.stringify(args) @@ -657,6 +668,7 @@ async function handleBuildToolCall( autoExecuteTools: true, timeout: 300000, interactive: false, + abortSignal, }) const responseData = { @@ -687,10 +699,11 @@ async function handleBuildToolCall( async function handleSubagentToolCall( toolDef: (typeof SUBAGENT_TOOL_DEFS)[number], args: Record, - userId: string + userId: string, + abortSignal?: AbortSignal ): Promise { if (toolDef.agentId === 'build') { - return handleBuildToolCall(args, userId) + return handleBuildToolCall(args, userId, abortSignal) } try { @@ -722,6 +735,7 @@ async function handleSubagentToolCall( userId, workflowId: args.workflowId as string | undefined, workspaceId: args.workspaceId as string | undefined, + abortSignal, } ) diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/tool-call/tool-call.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/tool-call/tool-call.tsx index 2d644c91e..0791b4a03 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/tool-call/tool-call.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/tool-call/tool-call.tsx @@ -1444,13 +1444,7 @@ export function ToolCall({ toolCall.name === 'mark_todo_in_progress' || toolCall.name === 'tool_search_tool_regex' || toolCall.name === 'user_memory' || - toolCall.name === 'edit_respond' || - toolCall.name === 'debug_respond' || - toolCall.name === 'plan_respond' || - toolCall.name === 'research_respond' || - toolCall.name === 'info_respond' || - toolCall.name === 'deploy_respond' || - toolCall.name === 'superagent_respond' + toolCall.name.endsWith('_respond') ) return null diff --git a/apps/sim/lib/copilot/orchestrator/sse-handlers/handlers.ts b/apps/sim/lib/copilot/orchestrator/sse-handlers/handlers.ts index 9a061029e..373fe0033 100644 --- a/apps/sim/lib/copilot/orchestrator/sse-handlers/handlers.ts +++ b/apps/sim/lib/copilot/orchestrator/sse-handlers/handlers.ts @@ -109,6 +109,7 @@ export const sseHandlers: Record = { const toolData = getEventData(event) || ({} as Record) const toolCallId = (toolData.id as string | undefined) || event.toolCallId const toolName = (toolData.name as string | undefined) || event.toolName + if (!toolCallId || !toolName) return const args = (toolData.arguments || toolData.input || asRecord(event.data).input) as diff --git a/apps/sim/lib/copilot/orchestrator/sse-handlers/tool-execution.ts b/apps/sim/lib/copilot/orchestrator/sse-handlers/tool-execution.ts index 1c707c570..80c4c6036 100644 --- a/apps/sim/lib/copilot/orchestrator/sse-handlers/tool-execution.ts +++ b/apps/sim/lib/copilot/orchestrator/sse-handlers/tool-execution.ts @@ -62,13 +62,25 @@ export async function executeToolAndReport( markToolResultSeen(toolCall.id) - await markToolComplete( + // Fire-and-forget: notify the copilot backend that the tool completed. + // IMPORTANT: We must NOT await this — the Go backend may block on the + // mark-complete handler until it can write back on the SSE stream, but + // the SSE reader (our for-await loop) is paused while we're in this + // handler. Awaiting here would deadlock: sim waits for Go's response, + // Go waits for sim to drain the SSE stream. + markToolComplete( toolCall.id, toolCall.name, result.success ? 200 : 500, result.error || (result.success ? 'Tool completed' : 'Tool failed'), result.output - ) + ).catch((err) => { + logger.error('markToolComplete fire-and-forget failed', { + toolCallId: toolCall.id, + toolName: toolCall.name, + error: err instanceof Error ? err.message : String(err), + }) + }) const resultEvent: SSEEvent = { type: 'tool_result', @@ -91,7 +103,14 @@ export async function executeToolAndReport( markToolResultSeen(toolCall.id) - await markToolComplete(toolCall.id, toolCall.name, 500, toolCall.error) + // Fire-and-forget (same reasoning as above). + markToolComplete(toolCall.id, toolCall.name, 500, toolCall.error).catch((err) => { + logger.error('markToolComplete fire-and-forget failed', { + toolCallId: toolCall.id, + toolName: toolCall.name, + error: err instanceof Error ? err.message : String(err), + }) + }) const errorEvent: SSEEvent = { type: 'tool_error', diff --git a/apps/sim/lib/copilot/orchestrator/tool-executor/index.ts b/apps/sim/lib/copilot/orchestrator/tool-executor/index.ts index 2bae9f05d..12dbbf598 100644 --- a/apps/sim/lib/copilot/orchestrator/tool-executor/index.ts +++ b/apps/sim/lib/copilot/orchestrator/tool-executor/index.ts @@ -206,6 +206,9 @@ async function executeSimWorkflowTool( return handler(params, context) } +/** Timeout for the mark-complete POST to the copilot backend (30 s). */ +const MARK_COMPLETE_TIMEOUT_MS = 30_000 + /** * Notify the copilot backend that a tool has completed. */ @@ -217,30 +220,42 @@ export async function markToolComplete( data?: unknown ): Promise { try { - const response = await fetch(`${SIM_AGENT_API_URL}/api/tools/mark-complete`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - ...(env.COPILOT_API_KEY ? { 'x-api-key': env.COPILOT_API_KEY } : {}), - }, - body: JSON.stringify({ - id: toolCallId, - name: toolName, - status, - message, - data, - }), - }) + const controller = new AbortController() + const timeoutId = setTimeout(() => controller.abort(), MARK_COMPLETE_TIMEOUT_MS) - if (!response.ok) { - logger.warn('Mark-complete call failed', { toolCallId, status: response.status }) - return false + try { + const response = await fetch(`${SIM_AGENT_API_URL}/api/tools/mark-complete`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + ...(env.COPILOT_API_KEY ? { 'x-api-key': env.COPILOT_API_KEY } : {}), + }, + body: JSON.stringify({ + id: toolCallId, + name: toolName, + status, + message, + data, + }), + signal: controller.signal, + }) + + if (!response.ok) { + logger.warn('Mark-complete call failed', { toolCallId, toolName, status: response.status }) + return false + } + + return true + } finally { + clearTimeout(timeoutId) } - - return true } catch (error) { + const isTimeout = + error instanceof DOMException && error.name === 'AbortError' logger.error('Mark-complete call failed', { toolCallId, + toolName, + timedOut: isTimeout, error: error instanceof Error ? error.message : String(error), }) return false diff --git a/apps/sim/lib/copilot/store-utils.ts b/apps/sim/lib/copilot/store-utils.ts index cdf864fee..9a124850d 100644 --- a/apps/sim/lib/copilot/store-utils.ts +++ b/apps/sim/lib/copilot/store-utils.ts @@ -13,6 +13,9 @@ type StoreSet = ( partial: Partial | ((state: CopilotStore) => Partial) ) => void +/** Respond tools are internal to copilot subagents and should never be shown in the UI */ +const HIDDEN_TOOL_SUFFIX = '_respond' + export function resolveToolDisplay( toolName: string | undefined, state: ClientToolCallState, @@ -20,6 +23,7 @@ export function resolveToolDisplay( params?: Record ): ClientToolDisplay | undefined { if (!toolName) return undefined + if (toolName.endsWith(HIDDEN_TOOL_SUFFIX)) return undefined const entry = TOOL_DISPLAY_REGISTRY[toolName] if (!entry) return humanizedFallback(toolName, state) diff --git a/apps/sim/lib/copilot/tools/client/tool-display-registry.ts b/apps/sim/lib/copilot/tools/client/tool-display-registry.ts index 45eb2f0f5..b106ea59f 100644 --- a/apps/sim/lib/copilot/tools/client/tool-display-registry.ts +++ b/apps/sim/lib/copilot/tools/client/tool-display-registry.ts @@ -862,6 +862,18 @@ const META_get_operations_examples: ToolMetadata = { }, } +const META_get_platform_actions: ToolMetadata = { + displayNames: { + [ClientToolCallState.generating]: { text: 'Viewing platform actions', icon: Loader2 }, + [ClientToolCallState.pending]: { text: 'Viewing platform actions', icon: Loader2 }, + [ClientToolCallState.executing]: { text: 'Viewing platform actions', icon: Loader2 }, + [ClientToolCallState.success]: { text: 'Viewed platform actions', icon: Navigation }, + [ClientToolCallState.error]: { text: 'Failed to view platform actions', icon: XCircle }, + [ClientToolCallState.rejected]: { text: 'Skipped platform actions', icon: MinusCircle }, + [ClientToolCallState.aborted]: { text: 'Aborted platform actions', icon: MinusCircle }, + }, +} + const META_get_page_contents: ToolMetadata = { displayNames: { [ClientToolCallState.generating]: { text: 'Getting page contents', icon: Loader2 }, @@ -2259,6 +2271,7 @@ const TOOL_METADATA_BY_ID: Record = { get_examples_rag: META_get_examples_rag, get_operations_examples: META_get_operations_examples, get_page_contents: META_get_page_contents, + get_platform_actions: META_get_platform_actions, get_trigger_blocks: META_get_trigger_blocks, get_trigger_examples: META_get_trigger_examples, get_user_workflow: META_get_user_workflow,