mirror of
https://github.com/simstudioai/sim.git
synced 2026-02-07 21:25:38 -05:00
Fix discovery tool
This commit is contained in:
@@ -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<NextResponse> {
|
||||
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<string, unknown> },
|
||||
userId: string
|
||||
userId: string,
|
||||
abortSignal?: AbortSignal
|
||||
): Promise<CallToolResult> {
|
||||
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<string, unknown>,
|
||||
userId: string
|
||||
userId: string,
|
||||
abortSignal?: AbortSignal
|
||||
): Promise<CallToolResult> {
|
||||
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<string, unknown>,
|
||||
userId: string
|
||||
userId: string,
|
||||
abortSignal?: AbortSignal
|
||||
): Promise<CallToolResult> {
|
||||
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,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -109,6 +109,7 @@ export const sseHandlers: Record<string, SSEHandler> = {
|
||||
const toolData = getEventData(event) || ({} as Record<string, unknown>)
|
||||
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
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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<boolean> {
|
||||
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
|
||||
|
||||
@@ -13,6 +13,9 @@ type StoreSet = (
|
||||
partial: Partial<CopilotStore> | ((state: CopilotStore) => Partial<CopilotStore>)
|
||||
) => 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<string, any>
|
||||
): 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)
|
||||
|
||||
|
||||
@@ -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<string, ToolMetadata> = {
|
||||
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,
|
||||
|
||||
Reference in New Issue
Block a user