diff --git a/apps/sim/app/api/copilot/chat/route.ts b/apps/sim/app/api/copilot/chat/route.ts index 03e1841c0..89839a7e4 100644 --- a/apps/sim/app/api/copilot/chat/route.ts +++ b/apps/sim/app/api/copilot/chat/route.ts @@ -1,7 +1,7 @@ import { db } from '@sim/db' -import { copilotChats, permissions, workflow } from '@sim/db/schema' +import { copilotChats } from '@sim/db/schema' import { createLogger } from '@sim/logger' -import { and, asc, desc, eq, inArray, or } from 'drizzle-orm' +import { and, desc, eq } from 'drizzle-orm' import { type NextRequest, NextResponse } from 'next/server' import { z } from 'zod' import { getSession } from '@/lib/auth' @@ -21,6 +21,7 @@ import type { CopilotProviderConfig } from '@/lib/copilot/types' import { env } from '@/lib/core/config/env' import { CopilotFiles } from '@/lib/uploads' import { createFileContent } from '@/lib/uploads/utils/file-utils' +import { resolveWorkflowIdForUser } from '@/lib/workflows/utils' import { tools } from '@/tools/registry' import { getLatestVersionTools, stripVersionSuffix } from '@/tools/utils' import { orchestrateCopilotStream } from '@/lib/copilot/orchestrator' @@ -79,54 +80,6 @@ const ChatMessageSchema = z.object({ commands: z.array(z.string()).optional(), }) -async function resolveWorkflowId( - userId: string, - workflowId?: string, - workflowName?: string -): Promise<{ workflowId: string; workflowName?: string } | null> { - // If workflowId provided, use it directly - if (workflowId) { - return { workflowId } - } - - // Get user's accessible workflows - const workspaceIds = await db - .select({ entityId: permissions.entityId }) - .from(permissions) - .where(and(eq(permissions.userId, userId), eq(permissions.entityType, 'workspace'))) - - const workspaceIdList = workspaceIds.map((row) => row.entityId) - - const workflowConditions = [eq(workflow.userId, userId)] - if (workspaceIdList.length > 0) { - workflowConditions.push(inArray(workflow.workspaceId, workspaceIdList)) - } - - const workflows = await db - .select() - .from(workflow) - .where(or(...workflowConditions)) - .orderBy(asc(workflow.sortOrder), asc(workflow.createdAt), asc(workflow.id)) - - if (workflows.length === 0) { - return null - } - - // If workflowName provided, find matching workflow - if (workflowName) { - const match = workflows.find( - (w) => String(w.name || '').trim().toLowerCase() === workflowName.toLowerCase() - ) - if (match) { - return { workflowId: match.id, workflowName: match.name || undefined } - } - return null - } - - // Default to first workflow - return { workflowId: workflows[0].id, workflowName: workflows[0].name || undefined } -} - /** * POST /api/copilot/chat * Send messages to sim agent and handle chat persistence @@ -165,7 +118,11 @@ export async function POST(req: NextRequest) { } = ChatMessageSchema.parse(body) // Resolve workflowId - if not provided, use first workflow or find by name - const resolved = await resolveWorkflowId(authenticatedUserId, providedWorkflowId, workflowName) + const resolved = await resolveWorkflowIdForUser( + authenticatedUserId, + providedWorkflowId, + workflowName + ) if (!resolved) { return createBadRequestResponse( 'No workflows found. Create a workflow first or provide a valid workflowId.' diff --git a/apps/sim/app/api/mcp/copilot/route.ts b/apps/sim/app/api/mcp/copilot/route.ts index 4ecb328da..b797ccca5 100644 --- a/apps/sim/app/api/mcp/copilot/route.ts +++ b/apps/sim/app/api/mcp/copilot/route.ts @@ -15,6 +15,7 @@ import { type NextRequest, NextResponse } from 'next/server' import { checkHybridAuth } from '@/lib/auth/hybrid' import { getCopilotModel } from '@/lib/copilot/config' import { orchestrateSubagentStream } from '@/lib/copilot/orchestrator/subagent' +import { DIRECT_TOOL_DEFS, SUBAGENT_TOOL_DEFS } from '@/lib/copilot/tools/mcp/definitions' import { executeToolServerSide, prepareExecutionContext } from '@/lib/copilot/orchestrator/tool-executor' const logger = createLogger('CopilotMcpAPI') @@ -167,468 +168,6 @@ Don't guess. Don't plan. Debug first to find the actual problem. - copilot_debug: Diagnose errors (REQUIRES workflowId) - USE THIS FIRST for issues ` -/** - * Direct tools that execute immediately without LLM orchestration. - * These are fast database queries that don't need AI reasoning. - */ -const DIRECT_TOOL_DEFS: Array<{ - name: string - description: string - inputSchema: { type: 'object'; properties?: Record; required?: string[] } - toolId: string -}> = [ - { - name: 'list_workflows', - toolId: 'list_user_workflows', - description: 'List all workflows the user has access to. Returns workflow IDs, names, and workspace info.', - inputSchema: { - type: 'object', - properties: { - workspaceId: { - type: 'string', - description: 'Optional workspace ID to filter workflows.', - }, - folderId: { - type: 'string', - description: 'Optional folder ID to filter workflows.', - }, - }, - }, - }, - { - name: 'list_workspaces', - toolId: 'list_user_workspaces', - description: 'List all workspaces the user has access to. Returns workspace IDs, names, and roles.', - inputSchema: { - type: 'object', - properties: {}, - }, - }, - { - name: 'list_folders', - toolId: 'list_folders', - description: 'List all folders in a workspace.', - inputSchema: { - type: 'object', - properties: { - workspaceId: { - type: 'string', - description: 'Workspace ID to list folders from.', - }, - }, - required: ['workspaceId'], - }, - }, - { - name: 'get_workflow', - toolId: 'get_workflow_from_name', - description: 'Get a workflow by name or ID. Returns the full workflow definition.', - inputSchema: { - type: 'object', - properties: { - name: { - type: 'string', - description: 'Workflow name to search for.', - }, - workflowId: { - type: 'string', - description: 'Workflow ID to retrieve directly.', - }, - }, - }, - }, - { - name: 'create_workflow', - toolId: 'create_workflow', - description: 'Create a new workflow. Returns the new workflow ID.', - inputSchema: { - type: 'object', - properties: { - name: { - type: 'string', - description: 'Name for the new workflow.', - }, - workspaceId: { - type: 'string', - description: 'Optional workspace ID. Uses default workspace if not provided.', - }, - folderId: { - type: 'string', - description: 'Optional folder ID to place the workflow in.', - }, - description: { - type: 'string', - description: 'Optional description for the workflow.', - }, - }, - required: ['name'], - }, - }, - { - name: 'create_folder', - toolId: 'create_folder', - description: 'Create a new folder in a workspace.', - inputSchema: { - type: 'object', - properties: { - name: { - type: 'string', - description: 'Name for the new folder.', - }, - workspaceId: { - type: 'string', - description: 'Optional workspace ID. Uses default workspace if not provided.', - }, - parentId: { - type: 'string', - description: 'Optional parent folder ID for nested folders.', - }, - }, - required: ['name'], - }, - }, -] - -const SUBAGENT_TOOL_DEFS: Array<{ - name: string - description: string - inputSchema: { type: 'object'; properties?: Record; required?: string[] } - agentId: string -}> = [ - { - name: 'copilot_build', - agentId: 'build', - description: `Build a workflow end-to-end in a single step. This is the fast mode equivalent for headless/MCP usage. - -USE THIS WHEN: -- Building a new workflow from scratch -- Modifying an existing workflow -- You want to gather information and build in one pass without separate plan→edit steps - -WORKFLOW ID (REQUIRED): -- For NEW workflows: First call create_workflow to get a workflowId, then pass it here -- For EXISTING workflows: Always pass the workflowId parameter - -CAN DO: -- Gather information about blocks, credentials, patterns -- Search documentation and patterns for best practices -- Add, modify, or remove blocks -- Configure block settings and connections -- Set environment variables and workflow variables - -CANNOT DO: -- Run or test workflows (use copilot_test separately after deploying) -- Deploy workflows (use copilot_deploy separately) - -WORKFLOW: -1. Call create_workflow to get a workflowId (for new workflows) -2. Call copilot_build with the request and workflowId -3. Build agent gathers info and builds in one pass -4. Call copilot_deploy to deploy the workflow -5. Optionally call copilot_test to verify it works`, - inputSchema: { - type: 'object', - properties: { - request: { - type: 'string', - description: 'What you want to build or modify in the workflow.', - }, - workflowId: { - type: 'string', - description: - 'REQUIRED. The workflow ID. For new workflows, call create_workflow first to get this.', - }, - context: { type: 'object' }, - }, - required: ['request', 'workflowId'], - }, - }, - { - name: 'copilot_discovery', - agentId: 'discovery', - description: `Find workflows by their contents or functionality when the user doesn't know the exact name or ID. - -USE THIS WHEN: -- User describes a workflow by what it does: "the one that sends emails", "my Slack notification workflow" -- User refers to workflow contents: "the workflow with the OpenAI block" -- User needs to search/match workflows by functionality or description - -DO NOT USE (use direct tools instead): -- User knows the workflow name → use get_workflow -- User wants to list all workflows → use list_workflows -- User wants to list workspaces → use list_workspaces -- User wants to list folders → use list_folders`, - inputSchema: { - type: 'object', - properties: { - request: { type: 'string' }, - workspaceId: { type: 'string' }, - context: { type: 'object' }, - }, - required: ['request'], - }, - }, - { - name: 'copilot_plan', - agentId: 'plan', - description: `Plan workflow changes by gathering required information. - -USE THIS WHEN: -- Building a new workflow -- Modifying an existing workflow -- You need to understand what blocks and integrations are available -- The workflow requires multiple blocks or connections - -WORKFLOW ID (REQUIRED): -- For NEW workflows: First call create_workflow to get a workflowId, then pass it here -- For EXISTING workflows: Always pass the workflowId parameter - -This tool gathers information about available blocks, credentials, and the current workflow state. - -RETURNS: A plan object containing block configurations, connections, and technical details. -IMPORTANT: Pass the returned plan EXACTLY to copilot_edit - do not modify or summarize it.`, - inputSchema: { - type: 'object', - properties: { - request: { type: 'string', description: 'What you want to build or modify in the workflow.' }, - workflowId: { - type: 'string', - description: 'REQUIRED. The workflow ID. For new workflows, call create_workflow first to get this.', - }, - context: { type: 'object' }, - }, - required: ['request', 'workflowId'], - }, - }, - { - name: 'copilot_edit', - agentId: 'edit', - description: `Execute a workflow plan and apply edits. - -USE THIS WHEN: -- You have a plan from copilot_plan that needs to be executed -- Building or modifying a workflow based on the plan -- Making changes to blocks, connections, or configurations - -WORKFLOW ID (REQUIRED): -- You MUST provide the workflowId parameter -- For new workflows, get the workflowId from create_workflow first - -PLAN (REQUIRED): -- Pass the EXACT plan object from copilot_plan in the context.plan field -- Do NOT modify, summarize, or interpret the plan - pass it verbatim -- The plan contains technical details the edit agent needs exactly as-is - -IMPORTANT: After copilot_edit completes, you MUST call copilot_deploy before the workflow can be run or tested.`, - inputSchema: { - type: 'object', - properties: { - message: { type: 'string', description: 'Optional additional instructions for the edit.' }, - workflowId: { - type: 'string', - description: 'REQUIRED. The workflow ID to edit. Get this from create_workflow for new workflows.', - }, - plan: { - type: 'object', - description: 'The plan object from copilot_plan. Pass it EXACTLY as returned, do not modify.', - }, - context: { - type: 'object', - description: 'Additional context. Put the plan in context.plan if not using the plan field directly.', - }, - }, - required: ['workflowId'], - }, - }, - { - name: 'copilot_debug', - agentId: 'debug', - description: `Diagnose errors or unexpected workflow behavior. - -WORKFLOW ID (REQUIRED): Always provide the workflowId of the workflow to debug.`, - inputSchema: { - type: 'object', - properties: { - error: { type: 'string', description: 'The error message or description of the issue.' }, - workflowId: { type: 'string', description: 'REQUIRED. The workflow ID to debug.' }, - context: { type: 'object' }, - }, - required: ['error', 'workflowId'], - }, - }, - { - name: 'copilot_deploy', - agentId: 'deploy', - description: `Deploy or manage workflow deployments. - -CRITICAL: You MUST deploy a workflow after building before it can be run or tested. -Workflows without an active deployment will fail with "no active deployment" error. - -WORKFLOW ID (REQUIRED): -- Always provide the workflowId parameter -- This must match the workflow you built with copilot_edit - -USE THIS: -- After copilot_edit completes to activate the workflow -- To update deployment settings -- To redeploy after making changes - -DEPLOYMENT TYPES: -- "deploy as api" - REST API endpoint -- "deploy as chat" - Chat interface -- "deploy as mcp" - MCP server`, - inputSchema: { - type: 'object', - properties: { - request: { - type: 'string', - description: 'The deployment request, e.g. "deploy as api" or "deploy as chat"', - }, - workflowId: { - type: 'string', - description: 'REQUIRED. The workflow ID to deploy.', - }, - context: { type: 'object' }, - }, - required: ['request', 'workflowId'], - }, - }, - { - name: 'copilot_auth', - agentId: 'auth', - description: 'Handle OAuth connection flows.', - inputSchema: { - type: 'object', - properties: { - request: { type: 'string' }, - context: { type: 'object' }, - }, - required: ['request'], - }, - }, - { - name: 'copilot_knowledge', - agentId: 'knowledge', - description: 'Create and manage knowledge bases.', - inputSchema: { - type: 'object', - properties: { - request: { type: 'string' }, - context: { type: 'object' }, - }, - required: ['request'], - }, - }, - { - name: 'copilot_custom_tool', - agentId: 'custom_tool', - description: 'Create or manage custom tools.', - inputSchema: { - type: 'object', - properties: { - request: { type: 'string' }, - context: { type: 'object' }, - }, - required: ['request'], - }, - }, - { - name: 'copilot_info', - agentId: 'info', - description: 'Inspect blocks, outputs, and workflow metadata.', - inputSchema: { - type: 'object', - properties: { - request: { type: 'string' }, - workflowId: { type: 'string' }, - context: { type: 'object' }, - }, - required: ['request'], - }, - }, - { - name: 'copilot_workflow', - agentId: 'workflow', - description: 'Manage workflow environment and configuration.', - inputSchema: { - type: 'object', - properties: { - request: { type: 'string' }, - workflowId: { type: 'string' }, - context: { type: 'object' }, - }, - required: ['request'], - }, - }, - { - name: 'copilot_research', - agentId: 'research', - description: 'Research external APIs and documentation.', - inputSchema: { - type: 'object', - properties: { - request: { type: 'string' }, - context: { type: 'object' }, - }, - required: ['request'], - }, - }, - { - name: 'copilot_tour', - agentId: 'tour', - description: 'Explain platform features and usage.', - inputSchema: { - type: 'object', - properties: { - request: { type: 'string' }, - context: { type: 'object' }, - }, - required: ['request'], - }, - }, - { - name: 'copilot_test', - agentId: 'test', - description: `Run workflows and verify outputs. - -PREREQUISITE: The workflow MUST be deployed first using copilot_deploy. -Undeployed workflows will fail with "no active deployment" error. - -WORKFLOW ID (REQUIRED): -- Always provide the workflowId parameter - -USE THIS: -- After deploying to verify the workflow works correctly -- To test with sample inputs -- To validate workflow behavior before sharing with user`, - inputSchema: { - type: 'object', - properties: { - request: { type: 'string' }, - workflowId: { - type: 'string', - description: 'REQUIRED. The workflow ID to test.', - }, - context: { type: 'object' }, - }, - required: ['request', 'workflowId'], - }, - }, - { - name: 'copilot_superagent', - agentId: 'superagent', - description: 'Execute direct external actions (email, Slack, etc.).', - inputSchema: { - type: 'object', - properties: { - request: { type: 'string' }, - context: { type: 'object' }, - }, - required: ['request'], - }, - }, -] - function createResponse(id: RequestId, result: unknown): JSONRPCResponse { return { jsonrpc: '2.0', diff --git a/apps/sim/app/api/v1/copilot/chat/route.ts b/apps/sim/app/api/v1/copilot/chat/route.ts index cab197ad5..517959c44 100644 --- a/apps/sim/app/api/v1/copilot/chat/route.ts +++ b/apps/sim/app/api/v1/copilot/chat/route.ts @@ -1,7 +1,4 @@ -import { db } from '@sim/db' -import { permissions, workflow } from '@sim/db/schema' import { createLogger } from '@sim/logger' -import { and, asc, eq, inArray, or } from 'drizzle-orm' import { type NextRequest, NextResponse } from 'next/server' import { z } from 'zod' import { authenticateV1Request } from '@/app/api/v1/auth' @@ -9,6 +6,7 @@ import { getCopilotModel } from '@/lib/copilot/config' import { SIM_AGENT_VERSION } from '@/lib/copilot/constants' import { COPILOT_REQUEST_MODES } from '@/lib/copilot/models' import { orchestrateCopilotStream } from '@/lib/copilot/orchestrator' +import { resolveWorkflowIdForUser } from '@/lib/workflows/utils' const logger = createLogger('CopilotHeadlessAPI') @@ -23,54 +21,6 @@ const RequestSchema = z.object({ timeout: z.number().optional().default(300000), }) -async function resolveWorkflowId( - userId: string, - workflowId?: string, - workflowName?: string -): Promise<{ workflowId: string; workflowName?: string } | null> { - // If workflowId provided, use it directly - if (workflowId) { - return { workflowId } - } - - // Get user's accessible workflows - const workspaceIds = await db - .select({ entityId: permissions.entityId }) - .from(permissions) - .where(and(eq(permissions.userId, userId), eq(permissions.entityType, 'workspace'))) - - const workspaceIdList = workspaceIds.map((row) => row.entityId) - - const workflowConditions = [eq(workflow.userId, userId)] - if (workspaceIdList.length > 0) { - workflowConditions.push(inArray(workflow.workspaceId, workspaceIdList)) - } - - const workflows = await db - .select() - .from(workflow) - .where(or(...workflowConditions)) - .orderBy(asc(workflow.sortOrder), asc(workflow.createdAt), asc(workflow.id)) - - if (workflows.length === 0) { - return null - } - - // If workflowName provided, find matching workflow - if (workflowName) { - const match = workflows.find( - (w) => String(w.name || '').trim().toLowerCase() === workflowName.toLowerCase() - ) - if (match) { - return { workflowId: match.id, workflowName: match.name || undefined } - } - return null - } - - // Default to first workflow - return { workflowId: workflows[0].id, workflowName: workflows[0].name || undefined } -} - /** * POST /api/v1/copilot/chat * Headless copilot endpoint for server-side orchestration. @@ -93,7 +43,7 @@ export async function POST(req: NextRequest) { const selectedModel = parsed.model || defaults.model // Resolve workflow ID - const resolved = await resolveWorkflowId(auth.userId, parsed.workflowId, parsed.workflowName) + const resolved = await resolveWorkflowIdForUser(auth.userId, parsed.workflowId, parsed.workflowName) if (!resolved) { return NextResponse.json( { success: false, error: 'No workflows found. Create a workflow first or provide a valid workflowId.' }, 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 5edc29271..9d575cfd5 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 @@ -27,7 +27,6 @@ import { getBlock } from '@/blocks/registry' import type { CopilotToolCall } from '@/stores/panel' import { useCopilotStore } from '@/stores/panel' import { CLASS_TOOL_METADATA } from '@/stores/panel/copilot/store' -import { COPILOT_SERVER_ORCHESTRATED } from '@/lib/copilot/orchestrator/config' import type { SubAgentContentBlock } from '@/stores/panel/copilot/types' import { useWorkflowStore } from '@/stores/workflows/workflow/store' @@ -1261,7 +1260,7 @@ function shouldShowRunSkipButtons(toolCall: CopilotToolCall): boolean { const toolCallLogger = createLogger('CopilotToolCall') -async function sendToolDecision(toolCallId: string, status: 'accepted' | 'rejected') { +async function sendToolDecision(toolCallId: string, status: 'accepted' | 'rejected' | 'background') { try { await fetch('/api/copilot/confirm', { method: 'POST', @@ -1283,105 +1282,15 @@ async function handleRun( onStateChange?: any, editedParams?: any ) { - if (COPILOT_SERVER_ORCHESTRATED) { - setToolCallState(toolCall, 'executing') - onStateChange?.('executing') - await sendToolDecision(toolCall.id, 'accepted') - return - } - const instance = getClientTool(toolCall.id) - - if (!instance && isIntegrationTool(toolCall.name)) { - onStateChange?.('executing') - try { - await useCopilotStore.getState().executeIntegrationTool(toolCall.id) - } catch (e) { - setToolCallState(toolCall, 'error', { error: e instanceof Error ? e.message : String(e) }) - onStateChange?.('error') - try { - await fetch('/api/copilot/tools/mark-complete', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - id: toolCall.id, - name: toolCall.name, - status: 500, - message: e instanceof Error ? e.message : 'Tool execution failed', - data: { error: e instanceof Error ? e.message : String(e) }, - }), - }) - } catch { - console.error('[handleRun] Failed to notify backend of tool error:', toolCall.id) - } - } - return - } - - if (!instance) return - try { - const mergedParams = - editedParams || - (toolCall as any).params || - (toolCall as any).parameters || - (toolCall as any).input || - {} - await instance.handleAccept?.(mergedParams) - onStateChange?.('executing') - } catch (e) { - setToolCallState(toolCall, 'error', { error: e instanceof Error ? e.message : String(e) }) - } + setToolCallState(toolCall, 'executing', editedParams ? { params: editedParams } : undefined) + onStateChange?.('executing') + await sendToolDecision(toolCall.id, 'accepted') } async function handleSkip(toolCall: CopilotToolCall, setToolCallState: any, onStateChange?: any) { - if (COPILOT_SERVER_ORCHESTRATED) { - setToolCallState(toolCall, 'rejected') - onStateChange?.('rejected') - await sendToolDecision(toolCall.id, 'rejected') - return - } - const instance = getClientTool(toolCall.id) - - if (!instance && isIntegrationTool(toolCall.name)) { - setToolCallState(toolCall, 'rejected') - onStateChange?.('rejected') - - let notified = false - for (let attempt = 0; attempt < 3 && !notified; attempt++) { - try { - const res = await fetch('/api/copilot/tools/mark-complete', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - id: toolCall.id, - name: toolCall.name, - status: 400, - message: 'Tool execution skipped by user', - data: { skipped: true, reason: 'user_skipped' }, - }), - }) - if (res.ok) { - notified = true - } - } catch (e) { - if (attempt < 2) { - await new Promise((resolve) => setTimeout(resolve, 500)) - } - } - } - - if (!notified) { - console.error('[handleSkip] Failed to notify backend after 3 attempts:', toolCall.id) - } - return - } - - if (instance) { - try { - await instance.handleReject?.() - } catch {} - } setToolCallState(toolCall, 'rejected') onStateChange?.('rejected') + await sendToolDecision(toolCall.id, 'rejected') } function getDisplayName(toolCall: CopilotToolCall): string { @@ -1541,7 +1450,7 @@ export function ToolCall({ // Check if this integration tool is auto-allowed // Subscribe to autoAllowedTools so we re-render when it changes const autoAllowedTools = useCopilotStore((s) => s.autoAllowedTools) - const { removeAutoAllowedTool } = useCopilotStore() + const { removeAutoAllowedTool, setToolCallState } = useCopilotStore() const isAutoAllowed = isIntegrationTool(toolCall.name) && autoAllowedTools.includes(toolCall.name) // Update edited params when toolCall params change (deep comparison to avoid resetting user edits on ref change) @@ -2243,16 +2152,9 @@ export function ToolCall({