From 0729e37a6e39411ebd2ff9027f074b02e666fd7b Mon Sep 17 00:00:00 2001 From: Siddharth Ganesan Date: Thu, 5 Feb 2026 12:01:29 -0800 Subject: [PATCH] Initial temp state, in the middle of a refactor --- .../api/copilot/api-keys/generate/route.ts | 5 +- apps/sim/app/api/copilot/api-keys/route.ts | 6 +- apps/sim/app/api/copilot/confirm/route.ts | 46 +--- apps/sim/app/api/copilot/credentials/route.ts | 26 ++ .../execute-copilot-server-tool/route.ts | 6 + .../sim/app/api/copilot/execute-tool/route.ts | 247 ----------------- apps/sim/app/api/copilot/stats/route.ts | 4 +- .../api/copilot/tools/mark-complete/route.ts | 123 --------- apps/sim/app/api/mcp/copilot/route.ts | 92 ++++++- apps/sim/lib/copilot/constants.ts | 5 + apps/sim/lib/copilot/orchestrator/config.ts | 21 ++ apps/sim/lib/copilot/orchestrator/index.ts | 9 +- .../lib/copilot/orchestrator/persistence.ts | 112 -------- .../lib/copilot/orchestrator/sse-handlers.ts | 67 ++--- .../sim/lib/copilot/orchestrator/sse-utils.ts | 10 +- apps/sim/lib/copilot/orchestrator/subagent.ts | 10 +- .../deploy.ts} | 218 +-------------- .../tool-executor/deployment-tools/index.ts | 2 + .../tool-executor/deployment-tools/manage.ts | 211 ++++++++++++++ .../index.ts} | 9 +- .../tool-executor/workflow-tools/index.ts | 2 + .../tool-executor/workflow-tools/mutations.ts | 251 +++++++++++++++++ .../queries.ts} | 258 +----------------- apps/sim/lib/copilot/orchestrator/types.ts | 24 +- .../sim/lib/copilot/tools/client/base-tool.ts | 64 +---- .../tools/client/blocks/get-block-config.ts | 49 +--- .../tools/client/blocks/get-block-options.ts | 55 +--- .../client/blocks/get-blocks-and-tools.ts | 33 +-- .../client/blocks/get-blocks-metadata.ts | 42 +-- .../tools/client/blocks/get-trigger-blocks.ts | 37 +-- .../tools/client/knowledge/knowledge-base.ts | 51 +--- .../tools/client/other/make-api-request.ts | 32 +-- .../client/other/search-documentation.ts | 35 +-- .../tools/client/user/get-credentials.ts | 40 +-- .../client/user/set-environment-variables.ts | 45 +-- .../client/workflow/get-workflow-console.ts | 60 +--- apps/sim/stores/panel/copilot/store.ts | 6 +- 37 files changed, 759 insertions(+), 1554 deletions(-) create mode 100644 apps/sim/app/api/copilot/credentials/route.ts delete mode 100644 apps/sim/app/api/copilot/execute-tool/route.ts delete mode 100644 apps/sim/app/api/copilot/tools/mark-complete/route.ts rename apps/sim/lib/copilot/orchestrator/tool-executor/{deployment-tools.ts => deployment-tools/deploy.ts} (55%) create mode 100644 apps/sim/lib/copilot/orchestrator/tool-executor/deployment-tools/index.ts create mode 100644 apps/sim/lib/copilot/orchestrator/tool-executor/deployment-tools/manage.ts rename apps/sim/lib/copilot/orchestrator/{tool-executor.ts => tool-executor/index.ts} (94%) create mode 100644 apps/sim/lib/copilot/orchestrator/tool-executor/workflow-tools/index.ts create mode 100644 apps/sim/lib/copilot/orchestrator/tool-executor/workflow-tools/mutations.ts rename apps/sim/lib/copilot/orchestrator/tool-executor/{workflow-tools.ts => workflow-tools/queries.ts} (69%) diff --git a/apps/sim/app/api/copilot/api-keys/generate/route.ts b/apps/sim/app/api/copilot/api-keys/generate/route.ts index db890bdca..27971cede 100644 --- a/apps/sim/app/api/copilot/api-keys/generate/route.ts +++ b/apps/sim/app/api/copilot/api-keys/generate/route.ts @@ -1,7 +1,7 @@ import { type NextRequest, NextResponse } from 'next/server' import { z } from 'zod' import { getSession } from '@/lib/auth' -import { SIM_AGENT_API_URL_DEFAULT } from '@/lib/copilot/constants' +import { SIM_AGENT_API_URL } from '@/lib/copilot/constants' import { env } from '@/lib/core/config/env' const GenerateApiKeySchema = z.object({ @@ -17,9 +17,6 @@ export async function POST(req: NextRequest) { const userId = session.user.id - // Move environment variable access inside the function - const SIM_AGENT_API_URL = env.SIM_AGENT_API_URL || SIM_AGENT_API_URL_DEFAULT - const body = await req.json().catch(() => ({})) const validationResult = GenerateApiKeySchema.safeParse(body) diff --git a/apps/sim/app/api/copilot/api-keys/route.ts b/apps/sim/app/api/copilot/api-keys/route.ts index f3e25ac82..02d0d5be2 100644 --- a/apps/sim/app/api/copilot/api-keys/route.ts +++ b/apps/sim/app/api/copilot/api-keys/route.ts @@ -1,6 +1,6 @@ import { type NextRequest, NextResponse } from 'next/server' import { getSession } from '@/lib/auth' -import { SIM_AGENT_API_URL_DEFAULT } from '@/lib/copilot/constants' +import { SIM_AGENT_API_URL } from '@/lib/copilot/constants' import { env } from '@/lib/core/config/env' export async function GET(request: NextRequest) { @@ -12,8 +12,6 @@ export async function GET(request: NextRequest) { const userId = session.user.id - const SIM_AGENT_API_URL = env.SIM_AGENT_API_URL || SIM_AGENT_API_URL_DEFAULT - const res = await fetch(`${SIM_AGENT_API_URL}/api/validate-key/get-api-keys`, { method: 'POST', headers: { @@ -68,8 +66,6 @@ export async function DELETE(request: NextRequest) { return NextResponse.json({ error: 'id is required' }, { status: 400 }) } - const SIM_AGENT_API_URL = env.SIM_AGENT_API_URL || SIM_AGENT_API_URL_DEFAULT - const res = await fetch(`${SIM_AGENT_API_URL}/api/validate-key/delete`, { method: 'POST', headers: { diff --git a/apps/sim/app/api/copilot/confirm/route.ts b/apps/sim/app/api/copilot/confirm/route.ts index 9fd5476c9..01b6672a3 100644 --- a/apps/sim/app/api/copilot/confirm/route.ts +++ b/apps/sim/app/api/copilot/confirm/route.ts @@ -23,7 +23,8 @@ const ConfirmationSchema = z.object({ }) /** - * Update tool call status in Redis + * Write the user's tool decision to Redis. The server-side orchestrator's + * waitForToolDecision() polls Redis for this value. */ async function updateToolCallStatus( toolCallId: string, @@ -32,57 +33,24 @@ async function updateToolCallStatus( ): Promise { const redis = getRedisClient() if (!redis) { - logger.warn('updateToolCallStatus: Redis client not available') + logger.warn('Redis client not available for tool confirmation') return false } try { const key = `tool_call:${toolCallId}` - const timeout = 600000 // 10 minutes timeout for user confirmation - const pollInterval = 100 // Poll every 100ms - const startTime = Date.now() - - logger.info('Polling for tool call in Redis', { toolCallId, key, timeout }) - - // Poll until the key exists or timeout - while (Date.now() - startTime < timeout) { - const exists = await redis.exists(key) - if (exists) { - break - } - - // Wait before next poll - await new Promise((resolve) => setTimeout(resolve, pollInterval)) - } - - // Final check if key exists after polling - const exists = await redis.exists(key) - if (!exists) { - logger.warn('Tool call not found in Redis after polling timeout', { - toolCallId, - key, - timeout, - pollDuration: Date.now() - startTime, - }) - return false - } - - // Store both status and message as JSON - const toolCallData = { + const payload = { status, message: message || null, timestamp: new Date().toISOString(), } - - await redis.set(key, JSON.stringify(toolCallData), 'EX', 86400) // Keep 24 hour expiry - + await redis.set(key, JSON.stringify(payload), 'EX', 86400) return true } catch (error) { - logger.error('Failed to update tool call status in Redis', { + logger.error('Failed to update tool call status', { toolCallId, status, - message, - error: error instanceof Error ? error.message : 'Unknown error', + error: error instanceof Error ? error.message : String(error), }) return false } diff --git a/apps/sim/app/api/copilot/credentials/route.ts b/apps/sim/app/api/copilot/credentials/route.ts new file mode 100644 index 000000000..acc99958f --- /dev/null +++ b/apps/sim/app/api/copilot/credentials/route.ts @@ -0,0 +1,26 @@ +import { type NextRequest, NextResponse } from 'next/server' +import { authenticateCopilotRequestSessionOnly } from '@/lib/copilot/request-helpers' +import { routeExecution } from '@/lib/copilot/tools/server/router' + +/** + * GET /api/copilot/credentials + * Returns connected OAuth credentials for the authenticated user. + * Used by the copilot store for credential masking. + */ +export async function GET(_req: NextRequest) { + const { userId, isAuthenticated } = await authenticateCopilotRequestSessionOnly() + if (!isAuthenticated || !userId) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) + } + + try { + const result = await routeExecution('get_credentials', {}, { userId }) + return NextResponse.json({ success: true, result }) + } catch (error) { + return NextResponse.json( + { success: false, error: error instanceof Error ? error.message : 'Failed to load credentials' }, + { status: 500 } + ) + } +} + diff --git a/apps/sim/app/api/copilot/execute-copilot-server-tool/route.ts b/apps/sim/app/api/copilot/execute-copilot-server-tool/route.ts index 5627ae897..3d6ab2e3a 100644 --- a/apps/sim/app/api/copilot/execute-copilot-server-tool/route.ts +++ b/apps/sim/app/api/copilot/execute-copilot-server-tool/route.ts @@ -17,6 +17,12 @@ const ExecuteSchema = z.object({ payload: z.unknown().optional(), }) +/** + * @deprecated Transitional route used by the legacy client-side tool execution path + * (Zustand store → client tool classes → this route). Will be removed once the + * interactive browser path is fully migrated to server-side orchestration. + * New server-side code should use lib/copilot/orchestrator/tool-executor directly. + */ export async function POST(req: NextRequest) { const tracker = createRequestTracker() try { diff --git a/apps/sim/app/api/copilot/execute-tool/route.ts b/apps/sim/app/api/copilot/execute-tool/route.ts deleted file mode 100644 index d134d28eb..000000000 --- a/apps/sim/app/api/copilot/execute-tool/route.ts +++ /dev/null @@ -1,247 +0,0 @@ -import { db } from '@sim/db' -import { account, workflow } from '@sim/db/schema' -import { createLogger } from '@sim/logger' -import { and, eq } from 'drizzle-orm' -import { type NextRequest, NextResponse } from 'next/server' -import { z } from 'zod' -import { getSession } from '@/lib/auth' -import { - createBadRequestResponse, - createInternalServerErrorResponse, - createRequestTracker, - createUnauthorizedResponse, -} from '@/lib/copilot/request-helpers' -import { generateRequestId } from '@/lib/core/utils/request' -import { getEffectiveDecryptedEnv } from '@/lib/environment/utils' -import { refreshTokenIfNeeded } from '@/app/api/auth/oauth/utils' -import { resolveEnvVarReferences } from '@/executor/utils/reference-validation' -import { executeTool } from '@/tools' -import { getTool, resolveToolId } from '@/tools/utils' - -const logger = createLogger('CopilotExecuteToolAPI') - -const ExecuteToolSchema = z.object({ - toolCallId: z.string(), - toolName: z.string(), - arguments: z.record(z.any()).optional().default({}), - workflowId: z.string().optional(), -}) - -export async function POST(req: NextRequest) { - const tracker = createRequestTracker() - - try { - const session = await getSession() - if (!session?.user?.id) { - return createUnauthorizedResponse() - } - - const userId = session.user.id - const body = await req.json() - - try { - const preview = JSON.stringify(body).slice(0, 300) - logger.debug(`[${tracker.requestId}] Incoming execute-tool request`, { preview }) - } catch {} - - const { toolCallId, toolName, arguments: toolArgs, workflowId } = ExecuteToolSchema.parse(body) - - const resolvedToolName = resolveToolId(toolName) - - logger.info(`[${tracker.requestId}] Executing tool`, { - toolCallId, - toolName, - resolvedToolName, - workflowId, - hasArgs: Object.keys(toolArgs).length > 0, - }) - - const toolConfig = getTool(resolvedToolName) - if (!toolConfig) { - // Find similar tool names to help debug - const { tools: allTools } = await import('@/tools/registry') - const allToolNames = Object.keys(allTools) - const prefix = toolName.split('_').slice(0, 2).join('_') - const similarTools = allToolNames - .filter((name) => name.startsWith(`${prefix.split('_')[0]}_`)) - .slice(0, 10) - - logger.warn(`[${tracker.requestId}] Tool not found in registry`, { - toolName, - prefix, - similarTools, - totalToolsInRegistry: allToolNames.length, - }) - return NextResponse.json( - { - success: false, - error: `Tool not found: ${toolName}. Similar tools: ${similarTools.join(', ')}`, - toolCallId, - }, - { status: 404 } - ) - } - - // Get the workspaceId from the workflow (env vars are stored at workspace level) - let workspaceId: string | undefined - if (workflowId) { - const workflowResult = await db - .select({ workspaceId: workflow.workspaceId }) - .from(workflow) - .where(eq(workflow.id, workflowId)) - .limit(1) - workspaceId = workflowResult[0]?.workspaceId ?? undefined - } - - // Get decrypted environment variables early so we can resolve all {{VAR}} references - const decryptedEnvVars = await getEffectiveDecryptedEnv(userId, workspaceId) - - logger.info(`[${tracker.requestId}] Fetched environment variables`, { - workflowId, - workspaceId, - envVarCount: Object.keys(decryptedEnvVars).length, - envVarKeys: Object.keys(decryptedEnvVars), - }) - - // Build execution params starting with LLM-provided arguments - // Resolve all {{ENV_VAR}} references in the arguments (deep for nested objects) - const executionParams: Record = resolveEnvVarReferences( - toolArgs, - decryptedEnvVars, - { deep: true } - ) as Record - - logger.info(`[${tracker.requestId}] Resolved env var references in arguments`, { - toolName, - originalArgKeys: Object.keys(toolArgs), - resolvedArgKeys: Object.keys(executionParams), - }) - - // Resolve OAuth access token if required - if (toolConfig.oauth?.required && toolConfig.oauth.provider) { - const provider = toolConfig.oauth.provider - logger.info(`[${tracker.requestId}] Resolving OAuth token`, { provider }) - - try { - // Find the account for this provider and user - const accounts = await db - .select() - .from(account) - .where(and(eq(account.providerId, provider), eq(account.userId, userId))) - .limit(1) - - if (accounts.length > 0) { - const acc = accounts[0] - const requestId = generateRequestId() - const { accessToken } = await refreshTokenIfNeeded(requestId, acc as any, acc.id) - - if (accessToken) { - executionParams.accessToken = accessToken - logger.info(`[${tracker.requestId}] OAuth token resolved`, { provider }) - } else { - logger.warn(`[${tracker.requestId}] No access token available`, { provider }) - return NextResponse.json( - { - success: false, - error: `OAuth token not available for ${provider}. Please reconnect your account.`, - toolCallId, - }, - { status: 400 } - ) - } - } else { - logger.warn(`[${tracker.requestId}] No account found for provider`, { provider }) - return NextResponse.json( - { - success: false, - error: `No ${provider} account connected. Please connect your account first.`, - toolCallId, - }, - { status: 400 } - ) - } - } catch (error) { - logger.error(`[${tracker.requestId}] Failed to resolve OAuth token`, { - provider, - error: error instanceof Error ? error.message : String(error), - }) - return NextResponse.json( - { - success: false, - error: `Failed to get OAuth token for ${provider}`, - toolCallId, - }, - { status: 500 } - ) - } - } - - // Check if tool requires an API key that wasn't resolved via {{ENV_VAR}} reference - const needsApiKey = toolConfig.params?.apiKey?.required - - if (needsApiKey && !executionParams.apiKey) { - logger.warn(`[${tracker.requestId}] No API key found for tool`, { toolName }) - return NextResponse.json( - { - success: false, - error: `API key not provided for ${toolName}. Use {{YOUR_API_KEY_ENV_VAR}} to reference your environment variable.`, - toolCallId, - }, - { status: 400 } - ) - } - - // Add execution context - executionParams._context = { - workflowId, - userId, - } - - // Special handling for function_execute - inject environment variables - if (toolName === 'function_execute') { - executionParams.envVars = decryptedEnvVars - executionParams.workflowVariables = {} // No workflow variables in copilot context - executionParams.blockData = {} // No block data in copilot context - executionParams.blockNameMapping = {} // No block mapping in copilot context - executionParams.language = executionParams.language || 'javascript' - executionParams.timeout = executionParams.timeout || 30000 - - logger.info(`[${tracker.requestId}] Injected env vars for function_execute`, { - envVarCount: Object.keys(decryptedEnvVars).length, - }) - } - - // Execute the tool - logger.info(`[${tracker.requestId}] Executing tool with resolved credentials`, { - toolName, - hasAccessToken: !!executionParams.accessToken, - hasApiKey: !!executionParams.apiKey, - }) - - const result = await executeTool(resolvedToolName, executionParams) - - logger.info(`[${tracker.requestId}] Tool execution complete`, { - toolName, - success: result.success, - hasOutput: !!result.output, - }) - - return NextResponse.json({ - success: true, - toolCallId, - result: { - success: result.success, - output: result.output, - error: result.error, - }, - }) - } catch (error) { - if (error instanceof z.ZodError) { - logger.debug(`[${tracker.requestId}] Zod validation error`, { issues: error.issues }) - return createBadRequestResponse('Invalid request body for execute-tool') - } - logger.error(`[${tracker.requestId}] Failed to execute tool:`, error) - const errorMessage = error instanceof Error ? error.message : 'Failed to execute tool' - return createInternalServerErrorResponse(errorMessage) - } -} diff --git a/apps/sim/app/api/copilot/stats/route.ts b/apps/sim/app/api/copilot/stats/route.ts index ea52c1c58..493f6e4ec 100644 --- a/apps/sim/app/api/copilot/stats/route.ts +++ b/apps/sim/app/api/copilot/stats/route.ts @@ -1,6 +1,6 @@ import { type NextRequest, NextResponse } from 'next/server' import { z } from 'zod' -import { SIM_AGENT_API_URL_DEFAULT } from '@/lib/copilot/constants' +import { SIM_AGENT_API_URL } from '@/lib/copilot/constants' import { authenticateCopilotRequestSessionOnly, createBadRequestResponse, @@ -10,8 +10,6 @@ import { } from '@/lib/copilot/request-helpers' import { env } from '@/lib/core/config/env' -const SIM_AGENT_API_URL = env.SIM_AGENT_API_URL || SIM_AGENT_API_URL_DEFAULT - const BodySchema = z.object({ messageId: z.string(), diffCreated: z.boolean(), diff --git a/apps/sim/app/api/copilot/tools/mark-complete/route.ts b/apps/sim/app/api/copilot/tools/mark-complete/route.ts deleted file mode 100644 index 1ada484e5..000000000 --- a/apps/sim/app/api/copilot/tools/mark-complete/route.ts +++ /dev/null @@ -1,123 +0,0 @@ -import { createLogger } from '@sim/logger' -import { type NextRequest, NextResponse } from 'next/server' -import { z } from 'zod' -import { SIM_AGENT_API_URL_DEFAULT } from '@/lib/copilot/constants' -import { - authenticateCopilotRequestSessionOnly, - createBadRequestResponse, - createInternalServerErrorResponse, - createRequestTracker, - createUnauthorizedResponse, -} from '@/lib/copilot/request-helpers' -import { env } from '@/lib/core/config/env' - -const logger = createLogger('CopilotMarkToolCompleteAPI') - -const SIM_AGENT_API_URL = env.SIM_AGENT_API_URL || SIM_AGENT_API_URL_DEFAULT - -const MarkCompleteSchema = z.object({ - id: z.string(), - name: z.string(), - status: z.number().int(), - message: z.any().optional(), - data: z.any().optional(), -}) - -/** - * POST /api/copilot/tools/mark-complete - * Proxy to Sim Agent: POST /api/tools/mark-complete - */ -export async function POST(req: NextRequest) { - const tracker = createRequestTracker() - - try { - const { userId, isAuthenticated } = await authenticateCopilotRequestSessionOnly() - if (!isAuthenticated || !userId) { - return createUnauthorizedResponse() - } - - const body = await req.json() - - // Log raw body shape for diagnostics (avoid dumping huge payloads) - try { - const bodyPreview = JSON.stringify(body).slice(0, 300) - logger.debug(`[${tracker.requestId}] Incoming mark-complete raw body preview`, { - preview: `${bodyPreview}${bodyPreview.length === 300 ? '...' : ''}`, - }) - } catch {} - - const parsed = MarkCompleteSchema.parse(body) - - const messagePreview = (() => { - try { - const s = - typeof parsed.message === 'string' ? parsed.message : JSON.stringify(parsed.message) - return s ? `${s.slice(0, 200)}${s.length > 200 ? '...' : ''}` : undefined - } catch { - return undefined - } - })() - - logger.info(`[${tracker.requestId}] Forwarding tool mark-complete`, { - userId, - toolCallId: parsed.id, - toolName: parsed.name, - status: parsed.status, - hasMessage: parsed.message !== undefined, - hasData: parsed.data !== undefined, - messagePreview, - agentUrl: `${SIM_AGENT_API_URL}/api/tools/mark-complete`, - }) - - const agentRes = 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(parsed), - }) - - // Attempt to parse agent response JSON - let agentJson: any = null - let agentText: string | null = null - try { - agentJson = await agentRes.json() - } catch (_) { - try { - agentText = await agentRes.text() - } catch {} - } - - logger.info(`[${tracker.requestId}] Agent responded to mark-complete`, { - status: agentRes.status, - ok: agentRes.ok, - responseJsonPreview: agentJson ? JSON.stringify(agentJson).slice(0, 300) : undefined, - responseTextPreview: agentText ? agentText.slice(0, 300) : undefined, - }) - - if (agentRes.ok) { - return NextResponse.json({ success: true }) - } - - const errorMessage = - agentJson?.error || agentText || `Agent responded with status ${agentRes.status}` - const status = agentRes.status >= 500 ? 500 : 400 - - logger.warn(`[${tracker.requestId}] Mark-complete failed`, { - status, - error: errorMessage, - }) - - return NextResponse.json({ success: false, error: errorMessage }, { status }) - } catch (error) { - if (error instanceof z.ZodError) { - logger.warn(`[${tracker.requestId}] Invalid mark-complete request body`, { - issues: error.issues, - }) - return createBadRequestResponse('Invalid request body for mark-complete') - } - logger.error(`[${tracker.requestId}] Failed to proxy mark-complete:`, error) - return createInternalServerErrorResponse('Failed to mark tool as complete') - } -} diff --git a/apps/sim/app/api/mcp/copilot/route.ts b/apps/sim/app/api/mcp/copilot/route.ts index ee297b5b9..6f36be94d 100644 --- a/apps/sim/app/api/mcp/copilot/route.ts +++ b/apps/sim/app/api/mcp/copilot/route.ts @@ -14,12 +14,15 @@ import { createLogger } from '@sim/logger' import { type NextRequest, NextResponse } from 'next/server' import { checkHybridAuth } from '@/lib/auth/hybrid' import { getCopilotModel } from '@/lib/copilot/config' +import { SIM_AGENT_VERSION } from '@/lib/copilot/constants' +import { orchestrateCopilotStream } from '@/lib/copilot/orchestrator' import { orchestrateSubagentStream } from '@/lib/copilot/orchestrator/subagent' import { executeToolServerSide, prepareExecutionContext, } from '@/lib/copilot/orchestrator/tool-executor' import { DIRECT_TOOL_DEFS, SUBAGENT_TOOL_DEFS } from '@/lib/copilot/tools/mcp/definitions' +import { resolveWorkflowIdForUser } from '@/lib/workflows/utils' const logger = createLogger('CopilotMcpAPI') @@ -336,12 +339,95 @@ async function handleDirectToolCall( } } +/** + * Build mode uses the main chat orchestrator with the 'fast' command instead of + * the subagent endpoint. In Go, 'build' is not a registered subagent — it's a mode + * (ModeFast) on the main chat processor that bypasses subagent orchestration and + * executes all tools directly. + */ +async function handleBuildToolCall( + id: RequestId, + args: Record, + userId: string +): Promise { + try { + const requestText = (args.request as string) || JSON.stringify(args) + const { model } = getCopilotModel('chat') + const workflowId = args.workflowId as string | undefined + + const resolved = workflowId + ? { workflowId } + : await resolveWorkflowIdForUser(userId) + + if (!resolved?.workflowId) { + const response: CallToolResult = { + content: [{ type: 'text', text: JSON.stringify({ success: false, error: 'workflowId is required for build. Call create_workflow first.' }, null, 2) }], + isError: true, + } + return NextResponse.json(createResponse(id, response)) + } + + const chatId = crypto.randomUUID() + const context = (args.context as Record) || {} + + const requestPayload = { + message: requestText, + workflowId: resolved.workflowId, + userId, + stream: true, + streamToolCalls: true, + model, + mode: 'agent', + commands: ['fast'], + messageId: crypto.randomUUID(), + version: SIM_AGENT_VERSION, + headless: true, + chatId, + context, + } + + const result = await orchestrateCopilotStream(requestPayload, { + userId, + workflowId: resolved.workflowId, + chatId, + autoExecuteTools: true, + timeout: 300000, + interactive: false, + }) + + const responseData = { + success: result.success, + content: result.content, + toolCalls: result.toolCalls, + error: result.error, + } + + const response: CallToolResult = { + content: [{ type: 'text', text: JSON.stringify(responseData, null, 2) }], + isError: !result.success, + } + + return NextResponse.json(createResponse(id, response)) + } catch (error) { + logger.error('Build tool call failed', { error }) + return NextResponse.json( + createError(id, ErrorCode.InternalError, `Build failed: ${error}`), + { status: 500 } + ) + } +} + async function handleSubagentToolCall( id: RequestId, toolDef: (typeof SUBAGENT_TOOL_DEFS)[number], args: Record, userId: string ): Promise { + // Build mode uses the main chat endpoint, not the subagent endpoint + if (toolDef.agentId === 'build') { + return handleBuildToolCall(id, args, userId) + } + const requestText = (args.request as string) || (args.message as string) || @@ -363,8 +449,6 @@ async function handleSubagentToolCall( workspaceId: args.workspaceId, context, model, - // Signal to the copilot backend that this is a headless request - // so it can enforce workflowId requirements on tools headless: true, }, { @@ -374,9 +458,6 @@ async function handleSubagentToolCall( } ) - // When a respond tool (plan_respond, edit_respond, etc.) was used, - // return only the structured result - not the full result with all internal tool calls. - // This provides clean output for MCP consumers. let responseData: unknown if (result.structuredResult) { responseData = { @@ -392,7 +473,6 @@ async function handleSubagentToolCall( errors: result.errors, } } else { - // Fallback: return content if no structured result responseData = { success: result.success, content: result.content, diff --git a/apps/sim/lib/copilot/constants.ts b/apps/sim/lib/copilot/constants.ts index e4b1f3a5d..7a45127eb 100644 --- a/apps/sim/lib/copilot/constants.ts +++ b/apps/sim/lib/copilot/constants.ts @@ -1,2 +1,7 @@ +import { env } from '@/lib/core/config/env' + export const SIM_AGENT_API_URL_DEFAULT = 'https://copilot.sim.ai' export const SIM_AGENT_VERSION = '1.0.3' + +/** Resolved copilot backend URL — reads from env with fallback to default. */ +export const SIM_AGENT_API_URL = env.SIM_AGENT_API_URL || SIM_AGENT_API_URL_DEFAULT diff --git a/apps/sim/lib/copilot/orchestrator/config.ts b/apps/sim/lib/copilot/orchestrator/config.ts index 9e2dc7221..6658ca6b9 100644 --- a/apps/sim/lib/copilot/orchestrator/config.ts +++ b/apps/sim/lib/copilot/orchestrator/config.ts @@ -19,6 +19,7 @@ export const INTERRUPT_TOOL_SET = new Set(INTERRUPT_TOOL_NAMES) export const SUBAGENT_TOOL_NAMES = [ 'debug', 'edit', + 'build', 'plan', 'test', 'deploy', @@ -31,6 +32,26 @@ export const SUBAGENT_TOOL_NAMES = [ 'workflow', 'evaluate', 'superagent', + 'discovery', ] as const export const SUBAGENT_TOOL_SET = new Set(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', +] as const + +export const RESPOND_TOOL_SET = new Set(RESPOND_TOOL_NAMES) diff --git a/apps/sim/lib/copilot/orchestrator/index.ts b/apps/sim/lib/copilot/orchestrator/index.ts index 0fe0abe62..1f3a54ee9 100644 --- a/apps/sim/lib/copilot/orchestrator/index.ts +++ b/apps/sim/lib/copilot/orchestrator/index.ts @@ -1,6 +1,7 @@ import { createLogger } from '@sim/logger' -import { SIM_AGENT_API_URL_DEFAULT } from '@/lib/copilot/constants' +import { SIM_AGENT_API_URL } from '@/lib/copilot/constants' import { handleSubagentRouting, sseHandlers, subAgentHandlers } from '@/lib/copilot/orchestrator/sse-handlers' +import { env } from '@/lib/core/config/env' import { normalizeSseEvent, shouldSkipToolCallEvent, @@ -15,10 +16,7 @@ import type { StreamingContext, ToolCallSummary, } from '@/lib/copilot/orchestrator/types' -import { env } from '@/lib/core/config/env' - const logger = createLogger('CopilotOrchestrator') -const SIM_AGENT_API_URL = env.SIM_AGENT_API_URL || SIM_AGENT_API_URL_DEFAULT export interface OrchestrateStreamOptions extends OrchestratorOptions { userId: string @@ -103,7 +101,8 @@ export async function orchestrateCopilotStream( } if (normalizedEvent.type === 'subagent_start') { - const toolCallId = normalizedEvent.data?.tool_call_id + const eventData = normalizedEvent.data as Record | undefined + const toolCallId = eventData?.tool_call_id as string | undefined if (toolCallId) { context.subAgentParentToolCallId = toolCallId context.subAgentContent[toolCallId] = '' diff --git a/apps/sim/lib/copilot/orchestrator/persistence.ts b/apps/sim/lib/copilot/orchestrator/persistence.ts index 418b652a5..f42d16e37 100644 --- a/apps/sim/lib/copilot/orchestrator/persistence.ts +++ b/apps/sim/lib/copilot/orchestrator/persistence.ts @@ -1,120 +1,8 @@ -import { db } from '@sim/db' -import { copilotChats } from '@sim/db/schema' import { createLogger } from '@sim/logger' -import { and, eq } from 'drizzle-orm' import { getRedisClient } from '@/lib/core/config/redis' const logger = createLogger('CopilotOrchestratorPersistence') -/** - * Create a new copilot chat record. - */ -export async function createChat(params: { - userId: string - workflowId: string - model: string -}): Promise<{ id: string }> { - const [chat] = await db - .insert(copilotChats) - .values({ - userId: params.userId, - workflowId: params.workflowId, - model: params.model, - messages: [], - }) - .returning({ id: copilotChats.id }) - - return { id: chat.id } -} - -/** - * Load an existing chat for a user. - */ -export async function loadChat(chatId: string, userId: string) { - const [chat] = await db - .select() - .from(copilotChats) - .where(and(eq(copilotChats.id, chatId), eq(copilotChats.userId, userId))) - .limit(1) - - return chat || null -} - -/** - * Save chat messages and metadata. - */ -export async function saveMessages( - chatId: string, - messages: any[], - options?: { - title?: string - conversationId?: string - planArtifact?: string | null - config?: { mode?: string; model?: string } - } -): Promise { - await db - .update(copilotChats) - .set({ - messages, - updatedAt: new Date(), - ...(options?.title ? { title: options.title } : {}), - ...(options?.conversationId ? { conversationId: options.conversationId } : {}), - ...(options?.planArtifact !== undefined ? { planArtifact: options.planArtifact } : {}), - ...(options?.config ? { config: options.config } : {}), - }) - .where(eq(copilotChats.id, chatId)) -} - -/** - * Update the conversationId for a chat without overwriting messages. - */ -export async function updateChatConversationId( - chatId: string, - conversationId: string -): Promise { - await db - .update(copilotChats) - .set({ - conversationId, - updatedAt: new Date(), - }) - .where(eq(copilotChats.id, chatId)) -} - -/** - * Set a tool call confirmation status in Redis. - */ -export async function setToolConfirmation( - toolCallId: string, - status: 'accepted' | 'rejected' | 'background' | 'pending', - message?: string -): Promise { - const redis = getRedisClient() - if (!redis) { - logger.warn('Redis client not available for tool confirmation') - return false - } - - const key = `tool_call:${toolCallId}` - const payload = { - status, - message: message || null, - timestamp: new Date().toISOString(), - } - - try { - await redis.set(key, JSON.stringify(payload), 'EX', 86400) - return true - } catch (error) { - logger.error('Failed to set tool confirmation', { - toolCallId, - error: error instanceof Error ? error.message : String(error), - }) - return false - } -} - /** * Get a tool call confirmation status from Redis. */ diff --git a/apps/sim/lib/copilot/orchestrator/sse-handlers.ts b/apps/sim/lib/copilot/orchestrator/sse-handlers.ts index 0f5f3df1a..597de39b4 100644 --- a/apps/sim/lib/copilot/orchestrator/sse-handlers.ts +++ b/apps/sim/lib/copilot/orchestrator/sse-handlers.ts @@ -1,7 +1,12 @@ import { createLogger } from '@sim/logger' -import { INTERRUPT_TOOL_SET, SUBAGENT_TOOL_SET } from '@/lib/copilot/orchestrator/config' +import { + INTERRUPT_TOOL_SET, + RESPOND_TOOL_SET, + SUBAGENT_TOOL_SET, +} from '@/lib/copilot/orchestrator/config' import { getToolConfirmation } from '@/lib/copilot/orchestrator/persistence' import { + asRecord, getEventData, markToolResultSeen, wasToolResultSeen, @@ -20,22 +25,6 @@ const logger = createLogger('CopilotSseHandlers') // Normalization + dedupe helpers live in sse-utils to keep server/client in sync. -/** - * 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. - */ -const RESPOND_TOOL_SET = new Set([ - 'plan_respond', - 'edit_respond', - 'debug_respond', - 'info_respond', - 'research_respond', - 'deploy_respond', - 'superagent_respond', - 'discovery_respond', -]) - export type SSEHandler = ( event: SSEEvent, context: StreamingContext, @@ -72,15 +61,16 @@ async function executeToolAndReport( // If create_workflow was successful, update the execution context with the new workflowId // This ensures subsequent tools in the same stream have access to the workflowId + const output = asRecord(result.output) if ( toolCall.name === 'create_workflow' && result.success && - result.output?.workflowId && + output.workflowId && !execContext.workflowId ) { - execContext.workflowId = result.output.workflowId - if (result.output.workspaceId) { - execContext.workspaceId = result.output.workspaceId + execContext.workflowId = output.workflowId as string + if (output.workspaceId) { + execContext.workspaceId = output.workspaceId as string } } @@ -145,7 +135,7 @@ async function waitForToolDecision( export const sseHandlers: Record = { chat_id: (event, context) => { - context.chatId = event.data?.chatId + context.chatId = asRecord(event.data).chatId }, title_updated: () => {}, tool_result: (event, context) => { @@ -206,7 +196,7 @@ export const sseHandlers: Record = { const toolName = toolData.name || event.toolName if (!toolCallId || !toolName) return - const args = toolData.arguments || toolData.input || event.data?.input + const args = toolData.arguments || toolData.input || asRecord(event.data).input const isPartial = toolData.partial === true const existing = context.toolCalls.get(toolCallId) @@ -323,7 +313,7 @@ export const sseHandlers: Record = { } }, reasoning: (event, context) => { - const phase = event.data?.phase || event.data?.data?.phase + const phase = asRecord(event.data).phase || asRecord(asRecord(event.data).data).phase if (phase === 'start') { context.isInThinkingBlock = true context.currentThinkingBlock = { @@ -341,34 +331,35 @@ export const sseHandlers: Record = { context.currentThinkingBlock = null return } - const chunk = - typeof event.data === 'string' ? event.data : event.data?.data || event.data?.content + const d = asRecord(event.data) + const chunk = typeof event.data === 'string' ? event.data : d.data || d.content if (!chunk || !context.currentThinkingBlock) return context.currentThinkingBlock.content = `${context.currentThinkingBlock.content || ''}${chunk}` }, content: (event, context) => { - const chunk = - typeof event.data === 'string' ? event.data : event.data?.content || event.data?.data + const d = asRecord(event.data) + const chunk = typeof event.data === 'string' ? event.data : d.content || d.data if (!chunk) return context.accumulatedContent += chunk - addContentBlock(context, { type: 'text', content: chunk }) + addContentBlock(context, { type: 'text', content: chunk as string }) }, done: (event, context) => { - if (event.data?.responseId) { - context.conversationId = event.data.responseId + const d = asRecord(event.data) + if (d.responseId) { + context.conversationId = d.responseId as string } context.streamComplete = true }, start: (event, context) => { - if (event.data?.responseId) { - context.conversationId = event.data.responseId + const d = asRecord(event.data) + if (d.responseId) { + context.conversationId = d.responseId as string } }, error: (event, context) => { + const d = asRecord(event.data) const message = - event.data?.message || - event.data?.error || - (typeof event.data === 'string' ? event.data : null) + d.message || d.error || (typeof event.data === 'string' ? event.data : null) if (message) { context.errors.push(message) } @@ -380,7 +371,7 @@ export const subAgentHandlers: Record = { content: (event, context) => { const parentToolCallId = context.subAgentParentToolCallId if (!parentToolCallId || !event.data) return - const chunk = typeof event.data === 'string' ? event.data : event.data?.content || '' + const chunk = typeof event.data === 'string' ? event.data : asRecord(event.data).content || '' if (!chunk) return context.subAgentContent[parentToolCallId] = (context.subAgentContent[parentToolCallId] || '') + chunk @@ -394,7 +385,7 @@ export const subAgentHandlers: Record = { const toolName = toolData.name || event.toolName if (!toolCallId || !toolName) return const isPartial = toolData.partial === true - const args = toolData.arguments || toolData.input || event.data?.input + const args = toolData.arguments || toolData.input || asRecord(event.data).input const existing = context.toolCalls.get(toolCallId) // Ignore late/duplicate tool_call events once we already have a result diff --git a/apps/sim/lib/copilot/orchestrator/sse-utils.ts b/apps/sim/lib/copilot/orchestrator/sse-utils.ts index 792a42aba..0dd805dec 100644 --- a/apps/sim/lib/copilot/orchestrator/sse-utils.ts +++ b/apps/sim/lib/copilot/orchestrator/sse-utils.ts @@ -2,6 +2,10 @@ import type { SSEEvent } from '@/lib/copilot/orchestrator/types' type EventDataObject = Record | undefined +/** Safely cast event.data to a record for property access. */ +export const asRecord = (data: unknown): Record => + (data && typeof data === 'object' && !Array.isArray(data) ? data : {}) as Record + const DEFAULT_TOOL_EVENT_TTL_MS = 5 * 60 * 1000 /** @@ -45,7 +49,7 @@ export const getEventData = (event: SSEEvent): EventDataObject => { return nested || topLevel } -export function getToolCallIdFromEvent(event: SSEEvent): string | undefined { +function getToolCallIdFromEvent(event: SSEEvent): string | undefined { const data = getEventData(event) return event.toolCallId || data?.id || data?.toolCallId } @@ -70,14 +74,14 @@ export function normalizeSseEvent(event: SSEEvent): SSEEvent { } } -export function markToolCallSeen(toolCallId: string, ttlMs: number = DEFAULT_TOOL_EVENT_TTL_MS): void { +function markToolCallSeen(toolCallId: string, ttlMs: number = DEFAULT_TOOL_EVENT_TTL_MS): void { seenToolCalls.add(toolCallId) setTimeout(() => { seenToolCalls.delete(toolCallId) }, ttlMs) } -export function wasToolCallSeen(toolCallId: string): boolean { +function wasToolCallSeen(toolCallId: string): boolean { return seenToolCalls.has(toolCallId) } diff --git a/apps/sim/lib/copilot/orchestrator/subagent.ts b/apps/sim/lib/copilot/orchestrator/subagent.ts index 17079e4d5..80e71d672 100644 --- a/apps/sim/lib/copilot/orchestrator/subagent.ts +++ b/apps/sim/lib/copilot/orchestrator/subagent.ts @@ -1,6 +1,7 @@ import { createLogger } from '@sim/logger' -import { SIM_AGENT_API_URL_DEFAULT } from '@/lib/copilot/constants' +import { SIM_AGENT_API_URL } from '@/lib/copilot/constants' import { handleSubagentRouting, sseHandlers, subAgentHandlers } from '@/lib/copilot/orchestrator/sse-handlers' +import { env } from '@/lib/core/config/env' import { normalizeSseEvent, shouldSkipToolCallEvent, @@ -15,11 +16,9 @@ import type { StreamingContext, ToolCallSummary, } from '@/lib/copilot/orchestrator/types' -import { env } from '@/lib/core/config/env' import { getEffectiveDecryptedEnv } from '@/lib/environment/utils' const logger = createLogger('CopilotSubagentOrchestrator') -const SIM_AGENT_API_URL = env.SIM_AGENT_API_URL || SIM_AGENT_API_URL_DEFAULT export interface SubagentOrchestratorOptions extends Omit { userId: string @@ -77,7 +76,7 @@ export async function orchestrateSubagentStream( 'Content-Type': 'application/json', ...(env.COPILOT_API_KEY ? { 'x-api-key': env.COPILOT_API_KEY } : {}), }, - body: JSON.stringify({ ...requestPayload, stream: true, userId }), + body: JSON.stringify({ ...requestPayload, stream: true }), signal: abortSignal, }) @@ -129,7 +128,8 @@ export async function orchestrateSubagentStream( // Handle subagent_start/subagent_end events to track nested subagent calls if (normalizedEvent.type === 'subagent_start') { - const toolCallId = normalizedEvent.data?.tool_call_id + const eventData = normalizedEvent.data as Record | undefined + const toolCallId = eventData?.tool_call_id as string | undefined if (toolCallId) { context.subAgentParentToolCallId = toolCallId context.subAgentContent[toolCallId] = '' diff --git a/apps/sim/lib/copilot/orchestrator/tool-executor/deployment-tools.ts b/apps/sim/lib/copilot/orchestrator/tool-executor/deployment-tools/deploy.ts similarity index 55% rename from apps/sim/lib/copilot/orchestrator/tool-executor/deployment-tools.ts rename to apps/sim/lib/copilot/orchestrator/tool-executor/deployment-tools/deploy.ts index fdc962382..aad2ed214 100644 --- a/apps/sim/lib/copilot/orchestrator/tool-executor/deployment-tools.ts +++ b/apps/sim/lib/copilot/orchestrator/tool-executor/deployment-tools/deploy.ts @@ -1,13 +1,12 @@ import crypto from 'crypto' import { db } from '@sim/db' -import { chat, workflow, workflowMcpServer, workflowMcpTool } from '@sim/db/schema' -import { and, eq, inArray } from 'drizzle-orm' +import { chat, workflow, workflowMcpTool } from '@sim/db/schema' +import { and, eq } from 'drizzle-orm' import type { ExecutionContext, ToolCallResult } from '@/lib/copilot/orchestrator/types' import { sanitizeToolName } from '@/lib/mcp/workflow-tool-schema' import { deployWorkflow, undeployWorkflow } from '@/lib/workflows/persistence/utils' -import { hasValidStartBlock } from '@/lib/workflows/triggers/trigger-utils.server' import { checkChatAccess, checkWorkflowAccessForChatCreation } from '@/app/api/chat/utils' -import { ensureWorkflowAccess } from './access' +import { ensureWorkflowAccess } from '../access' export async function executeDeployApi( params: Record, @@ -115,6 +114,11 @@ export async function executeDeployChat( return { success: false, error: deployResult.error || 'Failed to deploy workflow' } } + const existingCustomizations = + (existingDeployment?.customizations as + | { primaryColor?: string; welcomeMessage?: string } + | undefined) || {} + const payload = { workflowId, identifier, @@ -122,12 +126,10 @@ export async function executeDeployChat( description: String(params.description || existingDeployment?.description || ''), customizations: { primaryColor: - params.customizations?.primaryColor || - existingDeployment?.customizations?.primaryColor || + params.customizations?.primaryColor || existingCustomizations.primaryColor || 'var(--brand-primary-hover-hex)', welcomeMessage: - params.customizations?.welcomeMessage || - existingDeployment?.customizations?.welcomeMessage || + params.customizations?.welcomeMessage || existingCustomizations.welcomeMessage || 'Hi there! How can I help you today?', }, authType: params.authType || existingDeployment?.authType || 'public', @@ -277,203 +279,3 @@ export async function executeRedeploy(context: ExecutionContext): Promise, - context: ExecutionContext -): Promise { - try { - const workflowId = params.workflowId || context.workflowId - if (!workflowId) { - return { success: false, error: 'workflowId is required' } - } - const { workflow: workflowRecord } = await ensureWorkflowAccess(workflowId, context.userId) - const workspaceId = workflowRecord.workspaceId - - const [apiDeploy, chatDeploy] = await Promise.all([ - db.select().from(workflow).where(eq(workflow.id, workflowId)).limit(1), - db.select().from(chat).where(eq(chat.workflowId, workflowId)).limit(1), - ]) - - const isApiDeployed = apiDeploy[0]?.isDeployed || false - const apiDetails = { - isDeployed: isApiDeployed, - deployedAt: apiDeploy[0]?.deployedAt || null, - endpoint: isApiDeployed ? `/api/workflows/${workflowId}/execute` : null, - apiKey: workflowRecord.workspaceId ? 'Workspace API keys' : 'Personal API keys', - needsRedeployment: false, - } - - const isChatDeployed = !!chatDeploy[0] - const chatDetails = { - isDeployed: isChatDeployed, - chatId: chatDeploy[0]?.id || null, - identifier: chatDeploy[0]?.identifier || null, - chatUrl: isChatDeployed ? `/chat/${chatDeploy[0]?.identifier}` : null, - title: chatDeploy[0]?.title || null, - description: chatDeploy[0]?.description || null, - authType: chatDeploy[0]?.authType || null, - allowedEmails: chatDeploy[0]?.allowedEmails || null, - outputConfigs: chatDeploy[0]?.outputConfigs || null, - welcomeMessage: chatDeploy[0]?.customizations?.welcomeMessage || null, - primaryColor: chatDeploy[0]?.customizations?.primaryColor || null, - hasPassword: Boolean(chatDeploy[0]?.password), - } - - const mcpDetails = { isDeployed: false, servers: [] as any[] } - if (workspaceId) { - const servers = await db - .select({ - serverId: workflowMcpServer.id, - serverName: workflowMcpServer.name, - toolName: workflowMcpTool.toolName, - toolDescription: workflowMcpTool.toolDescription, - parameterSchema: workflowMcpTool.parameterSchema, - toolId: workflowMcpTool.id, - }) - .from(workflowMcpTool) - .innerJoin(workflowMcpServer, eq(workflowMcpTool.serverId, workflowMcpServer.id)) - .where(eq(workflowMcpTool.workflowId, workflowId)) - - if (servers.length > 0) { - mcpDetails.isDeployed = true - mcpDetails.servers = servers - } - } - - const isDeployed = apiDetails.isDeployed || chatDetails.isDeployed || mcpDetails.isDeployed - return { - success: true, - output: { isDeployed, api: apiDetails, chat: chatDetails, mcp: mcpDetails }, - } - } catch (error) { - return { success: false, error: error instanceof Error ? error.message : String(error) } - } -} - -export async function executeListWorkspaceMcpServers( - params: Record, - context: ExecutionContext -): Promise { - try { - const workflowId = params.workflowId || context.workflowId - if (!workflowId) { - return { success: false, error: 'workflowId is required' } - } - const { workflow: workflowRecord } = await ensureWorkflowAccess(workflowId, context.userId) - const workspaceId = workflowRecord.workspaceId - if (!workspaceId) { - return { success: false, error: 'workspaceId is required' } - } - - const servers = await db - .select({ - id: workflowMcpServer.id, - name: workflowMcpServer.name, - description: workflowMcpServer.description, - }) - .from(workflowMcpServer) - .where(eq(workflowMcpServer.workspaceId, workspaceId)) - - const serverIds = servers.map((server) => server.id) - const tools = - serverIds.length > 0 - ? await db - .select({ - serverId: workflowMcpTool.serverId, - toolName: workflowMcpTool.toolName, - }) - .from(workflowMcpTool) - .where(inArray(workflowMcpTool.serverId, serverIds)) - : [] - - const toolNamesByServer: Record = {} - for (const tool of tools) { - if (!toolNamesByServer[tool.serverId]) { - toolNamesByServer[tool.serverId] = [] - } - toolNamesByServer[tool.serverId].push(tool.toolName) - } - - const serversWithToolNames = servers.map((server) => ({ - ...server, - toolCount: toolNamesByServer[server.id]?.length || 0, - toolNames: toolNamesByServer[server.id] || [], - })) - - return { success: true, output: { servers: serversWithToolNames, count: servers.length } } - } catch (error) { - return { success: false, error: error instanceof Error ? error.message : String(error) } - } -} - -export async function executeCreateWorkspaceMcpServer( - params: Record, - context: ExecutionContext -): Promise { - try { - const workflowId = params.workflowId || context.workflowId - if (!workflowId) { - return { success: false, error: 'workflowId is required' } - } - const { workflow: workflowRecord } = await ensureWorkflowAccess(workflowId, context.userId) - const workspaceId = workflowRecord.workspaceId - if (!workspaceId) { - return { success: false, error: 'workspaceId is required' } - } - - const name = params.name?.trim() - if (!name) { - return { success: false, error: 'name is required' } - } - - const serverId = crypto.randomUUID() - const [server] = await db - .insert(workflowMcpServer) - .values({ - id: serverId, - workspaceId, - createdBy: context.userId, - name, - description: params.description?.trim() || null, - isPublic: params.isPublic ?? false, - createdAt: new Date(), - updatedAt: new Date(), - }) - .returning() - - const workflowIds: string[] = params.workflowIds || [] - const addedTools: Array<{ workflowId: string; toolName: string }> = [] - - if (workflowIds.length > 0) { - const workflows = await db.select().from(workflow).where(inArray(workflow.id, workflowIds)) - - for (const wf of workflows) { - if (wf.workspaceId !== workspaceId || !wf.isDeployed) { - continue - } - const hasStartBlock = await hasValidStartBlock(wf.id) - if (!hasStartBlock) { - continue - } - const toolName = sanitizeToolName(wf.name || `workflow_${wf.id}`) - await db.insert(workflowMcpTool).values({ - id: crypto.randomUUID(), - serverId, - workflowId: wf.id, - toolName, - toolDescription: wf.description || `Execute ${wf.name} workflow`, - parameterSchema: {}, - createdAt: new Date(), - updatedAt: new Date(), - }) - addedTools.push({ workflowId: wf.id, toolName }) - } - } - - return { success: true, output: { server, addedTools } } - } catch (error) { - return { success: false, error: error instanceof Error ? error.message : String(error) } - } -} - diff --git a/apps/sim/lib/copilot/orchestrator/tool-executor/deployment-tools/index.ts b/apps/sim/lib/copilot/orchestrator/tool-executor/deployment-tools/index.ts new file mode 100644 index 000000000..9e490922b --- /dev/null +++ b/apps/sim/lib/copilot/orchestrator/tool-executor/deployment-tools/index.ts @@ -0,0 +1,2 @@ +export * from './deploy' +export * from './manage' diff --git a/apps/sim/lib/copilot/orchestrator/tool-executor/deployment-tools/manage.ts b/apps/sim/lib/copilot/orchestrator/tool-executor/deployment-tools/manage.ts new file mode 100644 index 000000000..4e6db4af3 --- /dev/null +++ b/apps/sim/lib/copilot/orchestrator/tool-executor/deployment-tools/manage.ts @@ -0,0 +1,211 @@ +import crypto from 'crypto' +import { db } from '@sim/db' +import { chat, workflow, workflowMcpServer, workflowMcpTool } from '@sim/db/schema' +import { eq, inArray } from 'drizzle-orm' +import type { ExecutionContext, ToolCallResult } from '@/lib/copilot/orchestrator/types' +import { sanitizeToolName } from '@/lib/mcp/workflow-tool-schema' +import { hasValidStartBlock } from '@/lib/workflows/triggers/trigger-utils.server' +import { ensureWorkflowAccess } from '../access' + +export async function executeCheckDeploymentStatus( + params: Record, + context: ExecutionContext +): Promise { + try { + const workflowId = params.workflowId || context.workflowId + if (!workflowId) { + return { success: false, error: 'workflowId is required' } + } + const { workflow: workflowRecord } = await ensureWorkflowAccess(workflowId, context.userId) + const workspaceId = workflowRecord.workspaceId + + const [apiDeploy, chatDeploy] = await Promise.all([ + db.select().from(workflow).where(eq(workflow.id, workflowId)).limit(1), + db.select().from(chat).where(eq(chat.workflowId, workflowId)).limit(1), + ]) + + const isApiDeployed = apiDeploy[0]?.isDeployed || false + const apiDetails = { + isDeployed: isApiDeployed, + deployedAt: apiDeploy[0]?.deployedAt || null, + endpoint: isApiDeployed ? `/api/workflows/${workflowId}/execute` : null, + apiKey: workflowRecord.workspaceId ? 'Workspace API keys' : 'Personal API keys', + needsRedeployment: false, + } + + const isChatDeployed = !!chatDeploy[0] + const chatCustomizations = + (chatDeploy[0]?.customizations as + | { welcomeMessage?: string; primaryColor?: string } + | undefined) || {} + const chatDetails = { + isDeployed: isChatDeployed, + chatId: chatDeploy[0]?.id || null, + identifier: chatDeploy[0]?.identifier || null, + chatUrl: isChatDeployed ? `/chat/${chatDeploy[0]?.identifier}` : null, + title: chatDeploy[0]?.title || null, + description: chatDeploy[0]?.description || null, + authType: chatDeploy[0]?.authType || null, + allowedEmails: chatDeploy[0]?.allowedEmails || null, + outputConfigs: chatDeploy[0]?.outputConfigs || null, + welcomeMessage: chatCustomizations.welcomeMessage || null, + primaryColor: chatCustomizations.primaryColor || null, + hasPassword: Boolean(chatDeploy[0]?.password), + } + + const mcpDetails = { isDeployed: false, servers: [] as any[] } + if (workspaceId) { + const servers = await db + .select({ + serverId: workflowMcpServer.id, + serverName: workflowMcpServer.name, + toolName: workflowMcpTool.toolName, + toolDescription: workflowMcpTool.toolDescription, + parameterSchema: workflowMcpTool.parameterSchema, + toolId: workflowMcpTool.id, + }) + .from(workflowMcpTool) + .innerJoin(workflowMcpServer, eq(workflowMcpTool.serverId, workflowMcpServer.id)) + .where(eq(workflowMcpTool.workflowId, workflowId)) + + if (servers.length > 0) { + mcpDetails.isDeployed = true + mcpDetails.servers = servers + } + } + + const isDeployed = apiDetails.isDeployed || chatDetails.isDeployed || mcpDetails.isDeployed + return { + success: true, + output: { isDeployed, api: apiDetails, chat: chatDetails, mcp: mcpDetails }, + } + } catch (error) { + return { success: false, error: error instanceof Error ? error.message : String(error) } + } +} + +export async function executeListWorkspaceMcpServers( + params: Record, + context: ExecutionContext +): Promise { + try { + const workflowId = params.workflowId || context.workflowId + if (!workflowId) { + return { success: false, error: 'workflowId is required' } + } + const { workflow: workflowRecord } = await ensureWorkflowAccess(workflowId, context.userId) + const workspaceId = workflowRecord.workspaceId + if (!workspaceId) { + return { success: false, error: 'workspaceId is required' } + } + + const servers = await db + .select({ + id: workflowMcpServer.id, + name: workflowMcpServer.name, + description: workflowMcpServer.description, + }) + .from(workflowMcpServer) + .where(eq(workflowMcpServer.workspaceId, workspaceId)) + + const serverIds = servers.map((server) => server.id) + const tools = + serverIds.length > 0 + ? await db + .select({ + serverId: workflowMcpTool.serverId, + toolName: workflowMcpTool.toolName, + }) + .from(workflowMcpTool) + .where(inArray(workflowMcpTool.serverId, serverIds)) + : [] + + const toolNamesByServer: Record = {} + for (const tool of tools) { + if (!toolNamesByServer[tool.serverId]) { + toolNamesByServer[tool.serverId] = [] + } + toolNamesByServer[tool.serverId].push(tool.toolName) + } + + const serversWithToolNames = servers.map((server) => ({ + ...server, + toolCount: toolNamesByServer[server.id]?.length || 0, + toolNames: toolNamesByServer[server.id] || [], + })) + + return { success: true, output: { servers: serversWithToolNames, count: servers.length } } + } catch (error) { + return { success: false, error: error instanceof Error ? error.message : String(error) } + } +} + +export async function executeCreateWorkspaceMcpServer( + params: Record, + context: ExecutionContext +): Promise { + try { + const workflowId = params.workflowId || context.workflowId + if (!workflowId) { + return { success: false, error: 'workflowId is required' } + } + const { workflow: workflowRecord } = await ensureWorkflowAccess(workflowId, context.userId) + const workspaceId = workflowRecord.workspaceId + if (!workspaceId) { + return { success: false, error: 'workspaceId is required' } + } + + const name = params.name?.trim() + if (!name) { + return { success: false, error: 'name is required' } + } + + const serverId = crypto.randomUUID() + const [server] = await db + .insert(workflowMcpServer) + .values({ + id: serverId, + workspaceId, + createdBy: context.userId, + name, + description: params.description?.trim() || null, + isPublic: params.isPublic ?? false, + createdAt: new Date(), + updatedAt: new Date(), + }) + .returning() + + const workflowIds: string[] = params.workflowIds || [] + const addedTools: Array<{ workflowId: string; toolName: string }> = [] + + if (workflowIds.length > 0) { + const workflows = await db.select().from(workflow).where(inArray(workflow.id, workflowIds)) + + for (const wf of workflows) { + if (wf.workspaceId !== workspaceId || !wf.isDeployed) { + continue + } + const hasStartBlock = await hasValidStartBlock(wf.id) + if (!hasStartBlock) { + continue + } + const toolName = sanitizeToolName(wf.name || `workflow_${wf.id}`) + await db.insert(workflowMcpTool).values({ + id: crypto.randomUUID(), + serverId, + workflowId: wf.id, + toolName, + toolDescription: wf.description || `Execute ${wf.name} workflow`, + parameterSchema: {}, + createdAt: new Date(), + updatedAt: new Date(), + }) + addedTools.push({ workflowId: wf.id, toolName }) + } + } + + return { success: true, output: { server, addedTools } } + } catch (error) { + return { success: false, error: error instanceof Error ? error.message : String(error) } + } +} diff --git a/apps/sim/lib/copilot/orchestrator/tool-executor.ts b/apps/sim/lib/copilot/orchestrator/tool-executor/index.ts similarity index 94% rename from apps/sim/lib/copilot/orchestrator/tool-executor.ts rename to apps/sim/lib/copilot/orchestrator/tool-executor/index.ts index 1c04181cd..fc839f612 100644 --- a/apps/sim/lib/copilot/orchestrator/tool-executor.ts +++ b/apps/sim/lib/copilot/orchestrator/tool-executor/index.ts @@ -2,7 +2,7 @@ import { db } from '@sim/db' import { workflow } from '@sim/db/schema' import { createLogger } from '@sim/logger' import { eq } from 'drizzle-orm' -import { SIM_AGENT_API_URL_DEFAULT } from '@/lib/copilot/constants' +import { SIM_AGENT_API_URL } from '@/lib/copilot/constants' import type { ExecutionContext, ToolCallResult, @@ -11,7 +11,7 @@ import type { import { routeExecution } from '@/lib/copilot/tools/server/router' import { env } from '@/lib/core/config/env' import { getEffectiveDecryptedEnv } from '@/lib/environment/utils' -import { executeIntegrationToolDirect } from '@/lib/copilot/orchestrator/tool-executor/integration-tools' +import { executeIntegrationToolDirect } from './integration-tools' import { executeGetBlockOutputs, executeGetBlockUpstreamReferences, @@ -25,7 +25,7 @@ import { executeCreateFolder, executeRunWorkflow, executeSetGlobalWorkflowVariables, -} from '@/lib/copilot/orchestrator/tool-executor/workflow-tools' +} from './workflow-tools' import { executeCheckDeploymentStatus, executeCreateWorkspaceMcpServer, @@ -34,11 +34,10 @@ import { executeDeployMcp, executeListWorkspaceMcpServers, executeRedeploy, -} from '@/lib/copilot/orchestrator/tool-executor/deployment-tools' +} from './deployment-tools' import { getTool, resolveToolId } from '@/tools/utils' const logger = createLogger('CopilotToolExecutor') -const SIM_AGENT_API_URL = env.SIM_AGENT_API_URL || SIM_AGENT_API_URL_DEFAULT const SERVER_TOOLS = new Set([ 'get_blocks_and_tools', diff --git a/apps/sim/lib/copilot/orchestrator/tool-executor/workflow-tools/index.ts b/apps/sim/lib/copilot/orchestrator/tool-executor/workflow-tools/index.ts new file mode 100644 index 000000000..938d84e7b --- /dev/null +++ b/apps/sim/lib/copilot/orchestrator/tool-executor/workflow-tools/index.ts @@ -0,0 +1,2 @@ +export * from './queries' +export * from './mutations' diff --git a/apps/sim/lib/copilot/orchestrator/tool-executor/workflow-tools/mutations.ts b/apps/sim/lib/copilot/orchestrator/tool-executor/workflow-tools/mutations.ts new file mode 100644 index 000000000..ed4b51cc0 --- /dev/null +++ b/apps/sim/lib/copilot/orchestrator/tool-executor/workflow-tools/mutations.ts @@ -0,0 +1,251 @@ +import crypto from 'crypto' +import { db } from '@sim/db' +import { workflow, workflowFolder } from '@sim/db/schema' +import { and, eq, isNull, max } from 'drizzle-orm' +import type { ExecutionContext, ToolCallResult } from '@/lib/copilot/orchestrator/types' +import { generateRequestId } from '@/lib/core/utils/request' +import { buildDefaultWorkflowArtifacts } from '@/lib/workflows/defaults' +import { executeWorkflow } from '@/lib/workflows/executor/execute-workflow' +import { saveWorkflowToNormalizedTables } from '@/lib/workflows/persistence/utils' +import { ensureWorkflowAccess, ensureWorkspaceAccess, getDefaultWorkspaceId } from '../access' + +export async function executeCreateWorkflow( + params: Record, + context: ExecutionContext +): Promise { + try { + const name = typeof params?.name === 'string' ? params.name.trim() : '' + if (!name) { + return { success: false, error: 'name is required' } + } + + const workspaceId = params?.workspaceId || (await getDefaultWorkspaceId(context.userId)) + const folderId = params?.folderId || null + const description = typeof params?.description === 'string' ? params.description : null + + await ensureWorkspaceAccess(workspaceId, context.userId, true) + + const workflowId = crypto.randomUUID() + const now = new Date() + + const folderCondition = folderId ? eq(workflow.folderId, folderId) : isNull(workflow.folderId) + const [maxResult] = await db + .select({ maxOrder: max(workflow.sortOrder) }) + .from(workflow) + .where(and(eq(workflow.workspaceId, workspaceId), folderCondition)) + const sortOrder = (maxResult?.maxOrder ?? 0) + 1 + + await db.insert(workflow).values({ + id: workflowId, + userId: context.userId, + workspaceId, + folderId, + sortOrder, + name, + description, + color: '#3972F6', + lastSynced: now, + createdAt: now, + updatedAt: now, + isDeployed: false, + runCount: 0, + variables: {}, + }) + + const { workflowState } = buildDefaultWorkflowArtifacts() + const saveResult = await saveWorkflowToNormalizedTables(workflowId, workflowState) + if (!saveResult.success) { + throw new Error(saveResult.error || 'Failed to save workflow state') + } + + return { + success: true, + output: { + workflowId, + workflowName: name, + workspaceId, + folderId, + }, + } + } catch (error) { + return { success: false, error: error instanceof Error ? error.message : String(error) } + } +} + +export async function executeCreateFolder( + params: Record, + context: ExecutionContext +): Promise { + try { + const name = typeof params?.name === 'string' ? params.name.trim() : '' + if (!name) { + return { success: false, error: 'name is required' } + } + + const workspaceId = params?.workspaceId || (await getDefaultWorkspaceId(context.userId)) + const parentId = params?.parentId || null + + await ensureWorkspaceAccess(workspaceId, context.userId, true) + + const [maxResult] = await db + .select({ maxOrder: max(workflowFolder.sortOrder) }) + .from(workflowFolder) + .where( + and( + eq(workflowFolder.workspaceId, workspaceId), + parentId ? eq(workflowFolder.parentId, parentId) : isNull(workflowFolder.parentId) + ) + ) + const sortOrder = (maxResult?.maxOrder ?? 0) + 1 + + const folderId = crypto.randomUUID() + await db.insert(workflowFolder).values({ + id: folderId, + userId: context.userId, + workspaceId, + parentId, + name, + sortOrder, + createdAt: new Date(), + updatedAt: new Date(), + }) + + return { success: true, output: { folderId, name, workspaceId, parentId } } + } catch (error) { + return { success: false, error: error instanceof Error ? error.message : String(error) } + } +} + +export async function executeRunWorkflow( + params: Record, + context: ExecutionContext +): Promise { + try { + const workflowId = params.workflowId || context.workflowId + if (!workflowId) { + return { success: false, error: 'workflowId is required' } + } + + const { workflow: workflowRecord } = await ensureWorkflowAccess(workflowId, context.userId) + + const result = await executeWorkflow( + { + id: workflowRecord.id, + userId: workflowRecord.userId, + workspaceId: workflowRecord.workspaceId, + variables: workflowRecord.variables || {}, + }, + generateRequestId(), + params.workflow_input || params.input || undefined, + context.userId + ) + + return { + success: result.success, + output: { + executionId: result.metadata?.executionId, + success: result.success, + output: result.output, + logs: result.logs, + }, + error: result.success ? undefined : result.error || 'Workflow execution failed', + } + } catch (error) { + return { success: false, error: error instanceof Error ? error.message : String(error) } + } +} + +export async function executeSetGlobalWorkflowVariables( + params: Record, + context: ExecutionContext +): Promise { + try { + const workflowId = params.workflowId || context.workflowId + if (!workflowId) { + return { success: false, error: 'workflowId is required' } + } + const operations = Array.isArray(params.operations) ? params.operations : [] + const { workflow: workflowRecord } = await ensureWorkflowAccess(workflowId, context.userId) + + const currentVarsRecord = (workflowRecord.variables as Record) || {} + const byName: Record = {} + Object.values(currentVarsRecord).forEach((v: any) => { + if (v && typeof v === 'object' && v.id && v.name) byName[String(v.name)] = v + }) + + for (const op of operations) { + const key = String(op?.name || '') + if (!key) continue + const nextType = op?.type || byName[key]?.type || 'plain' + const coerceValue = (value: any, type: string) => { + if (value === undefined) return value + if (type === 'number') { + const n = Number(value) + return Number.isNaN(n) ? value : n + } + if (type === 'boolean') { + const v = String(value).trim().toLowerCase() + if (v === 'true') return true + if (v === 'false') return false + return value + } + if (type === 'array' || type === 'object') { + try { + const parsed = JSON.parse(String(value)) + if (type === 'array' && Array.isArray(parsed)) return parsed + if (type === 'object' && parsed && typeof parsed === 'object' && !Array.isArray(parsed)) + return parsed + } catch {} + return value + } + return value + } + + if (op.operation === 'delete') { + delete byName[key] + continue + } + const typedValue = coerceValue(op.value, nextType) + if (op.operation === 'add') { + byName[key] = { + id: crypto.randomUUID(), + workflowId, + name: key, + type: nextType, + value: typedValue, + } + continue + } + if (op.operation === 'edit') { + if (!byName[key]) { + byName[key] = { + id: crypto.randomUUID(), + workflowId, + name: key, + type: nextType, + value: typedValue, + } + } else { + byName[key] = { + ...byName[key], + type: nextType, + value: typedValue, + } + } + } + } + + const nextVarsRecord = Object.fromEntries( + Object.values(byName).map((v: any) => [String(v.id), v]) + ) + + await db + .update(workflow) + .set({ variables: nextVarsRecord, updatedAt: new Date() }) + .where(eq(workflow.id, workflowId)) + + return { success: true, output: { updated: Object.values(byName).length } } + } catch (error) { + return { success: false, error: error instanceof Error ? error.message : String(error) } + } +} diff --git a/apps/sim/lib/copilot/orchestrator/tool-executor/workflow-tools.ts b/apps/sim/lib/copilot/orchestrator/tool-executor/workflow-tools/queries.ts similarity index 69% rename from apps/sim/lib/copilot/orchestrator/tool-executor/workflow-tools.ts rename to apps/sim/lib/copilot/orchestrator/tool-executor/workflow-tools/queries.ts index 0adc1a768..7bbb8bd38 100644 --- a/apps/sim/lib/copilot/orchestrator/tool-executor/workflow-tools.ts +++ b/apps/sim/lib/copilot/orchestrator/tool-executor/workflow-tools/queries.ts @@ -1,26 +1,24 @@ -import crypto from 'crypto' import { db } from '@sim/db' import { customTools, permissions, workflow, workflowFolder, workspace } from '@sim/db/schema' -import { and, asc, desc, eq, inArray, isNull, max, or } from 'drizzle-orm' +import { and, asc, desc, eq, isNull, or } from 'drizzle-orm' import type { ExecutionContext, ToolCallResult } from '@/lib/copilot/orchestrator/types' import { extractWorkflowNames, formatNormalizedWorkflowForCopilot, normalizeWorkflowName, } from '@/lib/copilot/tools/shared/workflow-utils' -import { generateRequestId } from '@/lib/core/utils/request' import { mcpService } from '@/lib/mcp/service' import { listWorkspaceFiles } from '@/lib/uploads/contexts/workspace' import { getBlockOutputPaths } from '@/lib/workflows/blocks/block-outputs' import { BlockPathCalculator } from '@/lib/workflows/blocks/block-path-calculator' -import { buildDefaultWorkflowArtifacts } from '@/lib/workflows/defaults' -import { executeWorkflow } from '@/lib/workflows/executor/execute-workflow' -import { - loadWorkflowFromNormalizedTables, - saveWorkflowToNormalizedTables, -} from '@/lib/workflows/persistence/utils' +import { loadWorkflowFromNormalizedTables } from '@/lib/workflows/persistence/utils' import { isInputDefinitionTrigger } from '@/lib/workflows/triggers/input-definition-triggers' -import { ensureWorkflowAccess, ensureWorkspaceAccess, getAccessibleWorkflowsForUser, getDefaultWorkspaceId } from './access' +import { + ensureWorkflowAccess, + ensureWorkspaceAccess, + getAccessibleWorkflowsForUser, + getDefaultWorkspaceId, +} from '../access' import { normalizeName } from '@/executor/constants' export async function executeGetUserWorkflow( @@ -180,112 +178,6 @@ export async function executeListFolders( } } -export async function executeCreateWorkflow( - params: Record, - context: ExecutionContext -): Promise { - try { - const name = typeof params?.name === 'string' ? params.name.trim() : '' - if (!name) { - return { success: false, error: 'name is required' } - } - - const workspaceId = params?.workspaceId || (await getDefaultWorkspaceId(context.userId)) - const folderId = params?.folderId || null - const description = typeof params?.description === 'string' ? params.description : null - - await ensureWorkspaceAccess(workspaceId, context.userId, true) - - const workflowId = crypto.randomUUID() - const now = new Date() - - const folderCondition = folderId ? eq(workflow.folderId, folderId) : isNull(workflow.folderId) - const [maxResult] = await db - .select({ maxOrder: max(workflow.sortOrder) }) - .from(workflow) - .where(and(eq(workflow.workspaceId, workspaceId), folderCondition)) - const sortOrder = (maxResult?.maxOrder ?? 0) + 1 - - await db.insert(workflow).values({ - id: workflowId, - userId: context.userId, - workspaceId, - folderId, - sortOrder, - name, - description, - color: '#3972F6', - lastSynced: now, - createdAt: now, - updatedAt: now, - isDeployed: false, - runCount: 0, - variables: {}, - }) - - const { workflowState } = buildDefaultWorkflowArtifacts() - const saveResult = await saveWorkflowToNormalizedTables(workflowId, workflowState) - if (!saveResult.success) { - throw new Error(saveResult.error || 'Failed to save workflow state') - } - - return { - success: true, - output: { - workflowId, - workflowName: name, - workspaceId, - folderId, - }, - } - } catch (error) { - return { success: false, error: error instanceof Error ? error.message : String(error) } - } -} - -export async function executeCreateFolder( - params: Record, - context: ExecutionContext -): Promise { - try { - const name = typeof params?.name === 'string' ? params.name.trim() : '' - if (!name) { - return { success: false, error: 'name is required' } - } - - const workspaceId = params?.workspaceId || (await getDefaultWorkspaceId(context.userId)) - const parentId = params?.parentId || null - - await ensureWorkspaceAccess(workspaceId, context.userId, true) - - const [maxResult] = await db - .select({ maxOrder: max(workflowFolder.sortOrder) }) - .from(workflowFolder) - .where( - and( - eq(workflowFolder.workspaceId, workspaceId), - parentId ? eq(workflowFolder.parentId, parentId) : isNull(workflowFolder.parentId) - ) - ) - const sortOrder = (maxResult?.maxOrder ?? 0) + 1 - - const folderId = crypto.randomUUID() - await db.insert(workflowFolder).values({ - id: folderId, - workspaceId, - parentId, - name, - sortOrder, - createdAt: new Date(), - updatedAt: new Date(), - }) - - return { success: true, output: { folderId, name, workspaceId, parentId } } - } catch (error) { - return { success: false, error: error instanceof Error ? error.message : String(error) } - } -} - export async function executeGetWorkflowData( params: Record, context: ExecutionContext @@ -587,140 +479,6 @@ export async function executeGetBlockUpstreamReferences( } } -export async function executeRunWorkflow( - params: Record, - context: ExecutionContext -): Promise { - try { - const workflowId = params.workflowId || context.workflowId - if (!workflowId) { - return { success: false, error: 'workflowId is required' } - } - - const { workflow: workflowRecord } = await ensureWorkflowAccess(workflowId, context.userId) - - const result = await executeWorkflow( - { - id: workflowRecord.id, - userId: workflowRecord.userId, - workspaceId: workflowRecord.workspaceId, - variables: workflowRecord.variables || {}, - }, - generateRequestId(), - params.workflow_input || params.input || undefined, - context.userId - ) - - return { - success: result.success, - output: { - executionId: result.executionId, - success: result.success, - output: result.output, - logs: result.logs, - }, - error: result.success ? undefined : result.error || 'Workflow execution failed', - } - } catch (error) { - return { success: false, error: error instanceof Error ? error.message : String(error) } - } -} - -export async function executeSetGlobalWorkflowVariables( - params: Record, - context: ExecutionContext -): Promise { - try { - const workflowId = params.workflowId || context.workflowId - if (!workflowId) { - return { success: false, error: 'workflowId is required' } - } - const operations = Array.isArray(params.operations) ? params.operations : [] - const { workflow: workflowRecord } = await ensureWorkflowAccess(workflowId, context.userId) - - const currentVarsRecord = (workflowRecord.variables as Record) || {} - const byName: Record = {} - Object.values(currentVarsRecord).forEach((v: any) => { - if (v && typeof v === 'object' && v.id && v.name) byName[String(v.name)] = v - }) - - for (const op of operations) { - const key = String(op?.name || '') - if (!key) continue - const nextType = op?.type || byName[key]?.type || 'plain' - const coerceValue = (value: any, type: string) => { - if (value === undefined) return value - if (type === 'number') { - const n = Number(value) - return Number.isNaN(n) ? value : n - } - if (type === 'boolean') { - const v = String(value).trim().toLowerCase() - if (v === 'true') return true - if (v === 'false') return false - return value - } - if (type === 'array' || type === 'object') { - try { - const parsed = JSON.parse(String(value)) - if (type === 'array' && Array.isArray(parsed)) return parsed - if (type === 'object' && parsed && typeof parsed === 'object' && !Array.isArray(parsed)) - return parsed - } catch {} - return value - } - return value - } - - if (op.operation === 'delete') { - delete byName[key] - continue - } - const typedValue = coerceValue(op.value, nextType) - if (op.operation === 'add') { - byName[key] = { - id: crypto.randomUUID(), - workflowId, - name: key, - type: nextType, - value: typedValue, - } - continue - } - if (op.operation === 'edit') { - if (!byName[key]) { - byName[key] = { - id: crypto.randomUUID(), - workflowId, - name: key, - type: nextType, - value: typedValue, - } - } else { - byName[key] = { - ...byName[key], - type: nextType, - value: typedValue, - } - } - } - } - - const nextVarsRecord = Object.fromEntries( - Object.values(byName).map((v: any) => [String(v.id), v]) - ) - - await db - .update(workflow) - .set({ variables: nextVarsRecord, updatedAt: new Date() }) - .where(eq(workflow.id, workflowId)) - - return { success: true, output: { updated: Object.values(byName).length } } - } catch (error) { - return { success: false, error: error instanceof Error ? error.message : String(error) } - } -} - async function getWorkflowVariablesForTool( workflowId: string ): Promise> { diff --git a/apps/sim/lib/copilot/orchestrator/types.ts b/apps/sim/lib/copilot/orchestrator/types.ts index d1f58b588..dd321bab3 100644 --- a/apps/sim/lib/copilot/orchestrator/types.ts +++ b/apps/sim/lib/copilot/orchestrator/types.ts @@ -19,12 +19,12 @@ export type SSEEventType = export interface SSEEvent { type: SSEEventType - data?: any + data?: unknown subagent?: string toolCallId?: string toolName?: string success?: boolean - result?: any + result?: unknown } export type ToolCallStatus = 'pending' | 'executing' | 'success' | 'error' | 'skipped' | 'rejected' @@ -33,16 +33,16 @@ export interface ToolCallState { id: string name: string status: ToolCallStatus - params?: Record + params?: Record result?: ToolCallResult error?: string startTime?: number endTime?: number } -export interface ToolCallResult { +export interface ToolCallResult { success: boolean - output?: any + output?: T error?: string } @@ -73,6 +73,14 @@ export interface StreamingContext { errors: string[] } +export interface FileAttachment { + id: string + key: string + name: string + mimeType: string + size: number +} + export interface OrchestratorRequest { message: string workflowId: string @@ -82,7 +90,7 @@ export interface OrchestratorRequest { model?: string conversationId?: string contexts?: Array<{ type: string; content: string }> - fileAttachments?: any[] + fileAttachments?: FileAttachment[] commands?: string[] provider?: CopilotProviderConfig streamToolCalls?: boolean @@ -116,8 +124,8 @@ export interface ToolCallSummary { id: string name: string status: ToolCallStatus - params?: Record - result?: any + params?: Record + result?: unknown error?: string durationMs?: number } diff --git a/apps/sim/lib/copilot/tools/client/base-tool.ts b/apps/sim/lib/copilot/tools/client/base-tool.ts index 8d7d396f9..d3640bea0 100644 --- a/apps/sim/lib/copilot/tools/client/base-tool.ts +++ b/apps/sim/lib/copilot/tools/client/base-tool.ts @@ -147,67 +147,13 @@ export class BaseClientTool { } /** - * Mark a tool as complete on the server (proxies to server-side route). - * Once called, the tool is considered complete and won't be marked again. + * Mark a tool as complete. Tool completion is now handled server-side by the + * orchestrator (which calls the Go backend directly). Client tools are retained + * for UI display only — this method just tracks local state. */ - async markToolComplete(status: number, message?: any, data?: any): Promise { - // Prevent double-marking - if (this.isMarkedComplete) { - baseToolLogger.warn('markToolComplete called but tool already marked complete', { - toolCallId: this.toolCallId, - toolName: this.name, - existingState: this.state, - attemptedStatus: status, - }) - return true - } - + async markToolComplete(_status: number, _message?: unknown, _data?: unknown): Promise { this.isMarkedComplete = true - - try { - baseToolLogger.info('markToolComplete called', { - toolCallId: this.toolCallId, - toolName: this.name, - state: this.state, - status, - hasMessage: message !== undefined, - hasData: data !== undefined, - }) - } catch {} - - try { - const res = await fetch('/api/copilot/tools/mark-complete', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - id: this.toolCallId, - name: this.name, - status, - message, - data, - }), - }) - - if (!res.ok) { - // Try to surface server error - let errorText = `Failed to mark tool complete (status ${res.status})` - try { - const { error } = await res.json() - if (error) errorText = String(error) - } catch {} - throw new Error(errorText) - } - - const json = (await res.json()) as { success?: boolean } - return json?.success === true - } catch (e) { - // Default failure path - but tool is still marked complete locally - baseToolLogger.error('Failed to mark tool complete on server', { - toolCallId: this.toolCallId, - error: e instanceof Error ? e.message : String(e), - }) - return false - } + return true } // Accept (continue) for interrupt flows: move pending -> executing diff --git a/apps/sim/lib/copilot/tools/client/blocks/get-block-config.ts b/apps/sim/lib/copilot/tools/client/blocks/get-block-config.ts index a76971df0..88e696397 100644 --- a/apps/sim/lib/copilot/tools/client/blocks/get-block-config.ts +++ b/apps/sim/lib/copilot/tools/client/blocks/get-block-config.ts @@ -1,23 +1,11 @@ -import { createLogger } from '@sim/logger' import { FileCode, Loader2, MinusCircle, XCircle } from 'lucide-react' import { BaseClientTool, type BaseClientToolMetadata, ClientToolCallState, } from '@/lib/copilot/tools/client/base-tool' -import { - ExecuteResponseSuccessSchema, - GetBlockConfigInput, - GetBlockConfigResult, -} from '@/lib/copilot/tools/shared/schemas' import { getLatestBlock } from '@/blocks/registry' -interface GetBlockConfigArgs { - blockType: string - operation?: string - trigger?: boolean -} - export class GetBlockConfigClientTool extends BaseClientTool { static readonly id = 'get_block_config' @@ -63,38 +51,9 @@ export class GetBlockConfigClientTool extends BaseClientTool { }, } - async execute(args?: GetBlockConfigArgs): Promise { - const logger = createLogger('GetBlockConfigClientTool') - try { - this.setState(ClientToolCallState.executing) - - const { blockType, operation, trigger } = GetBlockConfigInput.parse(args || {}) - - const res = await fetch('/api/copilot/execute-copilot-server-tool', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - toolName: 'get_block_config', - payload: { blockType, operation, trigger }, - }), - }) - if (!res.ok) { - const errorText = await res.text().catch(() => '') - throw new Error(errorText || `Server error (${res.status})`) - } - const json = await res.json() - const parsed = ExecuteResponseSuccessSchema.parse(json) - const result = GetBlockConfigResult.parse(parsed.result) - - const inputCount = Object.keys(result.inputs).length - const outputCount = Object.keys(result.outputs).length - await this.markToolComplete(200, { inputs: inputCount, outputs: outputCount }, result) - this.setState(ClientToolCallState.success) - } catch (error: any) { - const message = error instanceof Error ? error.message : String(error) - logger.error('Execute failed', { message }) - await this.markToolComplete(500, message) - this.setState(ClientToolCallState.error) - } + async execute(): Promise { + // Tool execution is handled server-side by the orchestrator. + // Client tool classes are retained for UI display configuration only. + this.setState(ClientToolCallState.success) } } diff --git a/apps/sim/lib/copilot/tools/client/blocks/get-block-options.ts b/apps/sim/lib/copilot/tools/client/blocks/get-block-options.ts index 06efb6ffc..993773f0e 100644 --- a/apps/sim/lib/copilot/tools/client/blocks/get-block-options.ts +++ b/apps/sim/lib/copilot/tools/client/blocks/get-block-options.ts @@ -1,21 +1,11 @@ -import { createLogger } from '@sim/logger' import { ListFilter, Loader2, MinusCircle, XCircle } from 'lucide-react' import { BaseClientTool, type BaseClientToolMetadata, ClientToolCallState, } from '@/lib/copilot/tools/client/base-tool' -import { - ExecuteResponseSuccessSchema, - GetBlockOptionsInput, - GetBlockOptionsResult, -} from '@/lib/copilot/tools/shared/schemas' import { getLatestBlock } from '@/blocks/registry' -interface GetBlockOptionsArgs { - blockId: string -} - export class GetBlockOptionsClientTool extends BaseClientTool { static readonly id = 'get_block_options' @@ -65,46 +55,9 @@ export class GetBlockOptionsClientTool extends BaseClientTool { }, } - async execute(args?: GetBlockOptionsArgs): Promise { - const logger = createLogger('GetBlockOptionsClientTool') - try { - this.setState(ClientToolCallState.executing) - - // Handle both camelCase and snake_case parameter names, plus blockType as an alias - const normalizedArgs = args - ? { - blockId: - args.blockId || - (args as any).block_id || - (args as any).blockType || - (args as any).block_type, - } - : {} - - logger.info('execute called', { originalArgs: args, normalizedArgs }) - - const { blockId } = GetBlockOptionsInput.parse(normalizedArgs) - - const res = await fetch('/api/copilot/execute-copilot-server-tool', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ toolName: 'get_block_options', payload: { blockId } }), - }) - if (!res.ok) { - const errorText = await res.text().catch(() => '') - throw new Error(errorText || `Server error (${res.status})`) - } - const json = await res.json() - const parsed = ExecuteResponseSuccessSchema.parse(json) - const result = GetBlockOptionsResult.parse(parsed.result) - - await this.markToolComplete(200, { operations: result.operations.length }, result) - this.setState(ClientToolCallState.success) - } catch (error: any) { - const message = error instanceof Error ? error.message : String(error) - logger.error('Execute failed', { message }) - await this.markToolComplete(500, message) - this.setState(ClientToolCallState.error) - } + async execute(): Promise { + // Tool execution is handled server-side by the orchestrator. + // Client tool classes are retained for UI display configuration only. + this.setState(ClientToolCallState.success) } } diff --git a/apps/sim/lib/copilot/tools/client/blocks/get-blocks-and-tools.ts b/apps/sim/lib/copilot/tools/client/blocks/get-blocks-and-tools.ts index 7532ca6c4..17108c6db 100644 --- a/apps/sim/lib/copilot/tools/client/blocks/get-blocks-and-tools.ts +++ b/apps/sim/lib/copilot/tools/client/blocks/get-blocks-and-tools.ts @@ -1,14 +1,9 @@ -import { createLogger } from '@sim/logger' import { Blocks, Loader2, MinusCircle, XCircle } from 'lucide-react' import { BaseClientTool, type BaseClientToolMetadata, ClientToolCallState, } from '@/lib/copilot/tools/client/base-tool' -import { - ExecuteResponseSuccessSchema, - GetBlocksAndToolsResult, -} from '@/lib/copilot/tools/shared/schemas' export class GetBlocksAndToolsClientTool extends BaseClientTool { static readonly id = 'get_blocks_and_tools' @@ -31,30 +26,8 @@ export class GetBlocksAndToolsClientTool extends BaseClientTool { } async execute(): Promise { - const logger = createLogger('GetBlocksAndToolsClientTool') - try { - this.setState(ClientToolCallState.executing) - - const res = await fetch('/api/copilot/execute-copilot-server-tool', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ toolName: 'get_blocks_and_tools', payload: {} }), - }) - if (!res.ok) { - const errorText = await res.text().catch(() => '') - throw new Error(errorText || `Server error (${res.status})`) - } - const json = await res.json() - const parsed = ExecuteResponseSuccessSchema.parse(json) - const result = GetBlocksAndToolsResult.parse(parsed.result) - - // TODO: Temporarily sending empty data to test 403 issue - await this.markToolComplete(200, 'Successfully retrieved blocks and tools', {}) - this.setState(ClientToolCallState.success) - } catch (error: any) { - const message = error instanceof Error ? error.message : String(error) - await this.markToolComplete(500, message) - this.setState(ClientToolCallState.error) - } + // Tool execution is handled server-side by the orchestrator. + // Client tool classes are retained for UI display configuration only. + this.setState(ClientToolCallState.success) } } diff --git a/apps/sim/lib/copilot/tools/client/blocks/get-blocks-metadata.ts b/apps/sim/lib/copilot/tools/client/blocks/get-blocks-metadata.ts index 8fd88b1a3..fd547fa0c 100644 --- a/apps/sim/lib/copilot/tools/client/blocks/get-blocks-metadata.ts +++ b/apps/sim/lib/copilot/tools/client/blocks/get-blocks-metadata.ts @@ -1,19 +1,9 @@ -import { createLogger } from '@sim/logger' import { ListFilter, Loader2, MinusCircle, XCircle } from 'lucide-react' import { BaseClientTool, type BaseClientToolMetadata, ClientToolCallState, } from '@/lib/copilot/tools/client/base-tool' -import { - ExecuteResponseSuccessSchema, - GetBlocksMetadataInput, - GetBlocksMetadataResult, -} from '@/lib/copilot/tools/shared/schemas' - -interface GetBlocksMetadataArgs { - blockIds: string[] -} export class GetBlocksMetadataClientTool extends BaseClientTool { static readonly id = 'get_blocks_metadata' @@ -63,33 +53,9 @@ export class GetBlocksMetadataClientTool extends BaseClientTool { }, } - async execute(args?: GetBlocksMetadataArgs): Promise { - const logger = createLogger('GetBlocksMetadataClientTool') - try { - this.setState(ClientToolCallState.executing) - - const { blockIds } = GetBlocksMetadataInput.parse(args || {}) - - const res = await fetch('/api/copilot/execute-copilot-server-tool', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ toolName: 'get_blocks_metadata', payload: { blockIds } }), - }) - if (!res.ok) { - const errorText = await res.text().catch(() => '') - throw new Error(errorText || `Server error (${res.status})`) - } - const json = await res.json() - const parsed = ExecuteResponseSuccessSchema.parse(json) - const result = GetBlocksMetadataResult.parse(parsed.result) - - await this.markToolComplete(200, { retrieved: Object.keys(result.metadata).length }, result) - this.setState(ClientToolCallState.success) - } catch (error: any) { - const message = error instanceof Error ? error.message : String(error) - logger.error('Execute failed', { message }) - await this.markToolComplete(500, message) - this.setState(ClientToolCallState.error) - } + async execute(): Promise { + // Tool execution is handled server-side by the orchestrator. + // Client tool classes are retained for UI display configuration only. + this.setState(ClientToolCallState.success) } } diff --git a/apps/sim/lib/copilot/tools/client/blocks/get-trigger-blocks.ts b/apps/sim/lib/copilot/tools/client/blocks/get-trigger-blocks.ts index c9fa0f78a..2d8bda809 100644 --- a/apps/sim/lib/copilot/tools/client/blocks/get-trigger-blocks.ts +++ b/apps/sim/lib/copilot/tools/client/blocks/get-trigger-blocks.ts @@ -1,14 +1,9 @@ -import { createLogger } from '@sim/logger' import { ListFilter, Loader2, MinusCircle, XCircle } from 'lucide-react' import { BaseClientTool, type BaseClientToolMetadata, ClientToolCallState, } from '@/lib/copilot/tools/client/base-tool' -import { - ExecuteResponseSuccessSchema, - GetTriggerBlocksResult, -} from '@/lib/copilot/tools/shared/schemas' export class GetTriggerBlocksClientTool extends BaseClientTool { static readonly id = 'get_trigger_blocks' @@ -31,34 +26,8 @@ export class GetTriggerBlocksClientTool extends BaseClientTool { } async execute(): Promise { - const logger = createLogger('GetTriggerBlocksClientTool') - try { - this.setState(ClientToolCallState.executing) - - const res = await fetch('/api/copilot/execute-copilot-server-tool', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ toolName: 'get_trigger_blocks', payload: {} }), - }) - if (!res.ok) { - const errorText = await res.text().catch(() => '') - try { - const errorJson = JSON.parse(errorText) - throw new Error(errorJson.error || errorText || `Server error (${res.status})`) - } catch { - throw new Error(errorText || `Server error (${res.status})`) - } - } - const json = await res.json() - const parsed = ExecuteResponseSuccessSchema.parse(json) - const result = GetTriggerBlocksResult.parse(parsed.result) - - await this.markToolComplete(200, 'Successfully retrieved trigger blocks', result) - this.setState(ClientToolCallState.success) - } catch (error: any) { - const message = error instanceof Error ? error.message : String(error) - await this.markToolComplete(500, message) - this.setState(ClientToolCallState.error) - } + // Tool execution is handled server-side by the orchestrator. + // Client tool classes are retained for UI display configuration only. + this.setState(ClientToolCallState.success) } } diff --git a/apps/sim/lib/copilot/tools/client/knowledge/knowledge-base.ts b/apps/sim/lib/copilot/tools/client/knowledge/knowledge-base.ts index 89f60b155..611caa68f 100644 --- a/apps/sim/lib/copilot/tools/client/knowledge/knowledge-base.ts +++ b/apps/sim/lib/copilot/tools/client/knowledge/knowledge-base.ts @@ -1,16 +1,11 @@ -import { createLogger } from '@sim/logger' import { Database, Loader2, MinusCircle, PlusCircle, XCircle } from 'lucide-react' import { BaseClientTool, type BaseClientToolMetadata, ClientToolCallState, } from '@/lib/copilot/tools/client/base-tool' -import { - ExecuteResponseSuccessSchema, - type KnowledgeBaseArgs, -} from '@/lib/copilot/tools/shared/schemas' +import { type KnowledgeBaseArgs } from '@/lib/copilot/tools/shared/schemas' import { useCopilotStore } from '@/stores/panel/copilot/store' -import { useWorkflowRegistry } from '@/stores/workflows/registry/store' /** * Client tool for knowledge base operations @@ -99,45 +94,9 @@ export class KnowledgeBaseClientTool extends BaseClientTool { await this.execute(args) } - async execute(args?: KnowledgeBaseArgs): Promise { - const logger = createLogger('KnowledgeBaseClientTool') - try { - this.setState(ClientToolCallState.executing) - - // Get the workspace ID from the workflow registry hydration state - const { hydration } = useWorkflowRegistry.getState() - const workspaceId = hydration.workspaceId - - // Build payload with workspace ID included in args - const payload: KnowledgeBaseArgs = { - ...(args || { operation: 'list' }), - args: { - ...(args?.args || {}), - workspaceId: workspaceId || undefined, - }, - } - - const res = await fetch('/api/copilot/execute-copilot-server-tool', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ toolName: 'knowledge_base', payload }), - }) - - if (!res.ok) { - const txt = await res.text().catch(() => '') - throw new Error(txt || `Server error (${res.status})`) - } - - const json = await res.json() - const parsed = ExecuteResponseSuccessSchema.parse(json) - - this.setState(ClientToolCallState.success) - await this.markToolComplete(200, 'Knowledge base operation completed', parsed.result) - this.setState(ClientToolCallState.success) - } catch (e: any) { - logger.error('execute failed', { message: e?.message }) - this.setState(ClientToolCallState.error) - await this.markToolComplete(500, e?.message || 'Failed to access knowledge base') - } + async execute(): Promise { + // Tool execution is handled server-side by the orchestrator. + // Client tool classes are retained for UI display configuration only. + this.setState(ClientToolCallState.success) } } diff --git a/apps/sim/lib/copilot/tools/client/other/make-api-request.ts b/apps/sim/lib/copilot/tools/client/other/make-api-request.ts index 051622c05..37d78b17c 100644 --- a/apps/sim/lib/copilot/tools/client/other/make-api-request.ts +++ b/apps/sim/lib/copilot/tools/client/other/make-api-request.ts @@ -93,33 +93,15 @@ export class MakeApiRequestClientTool extends BaseClientTool { this.setState(ClientToolCallState.rejected) } - async handleAccept(args?: MakeApiRequestArgs): Promise { - const logger = createLogger('MakeApiRequestClientTool') - try { - this.setState(ClientToolCallState.executing) - const res = await fetch('/api/copilot/execute-copilot-server-tool', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ toolName: 'make_api_request', payload: args || {} }), - }) - if (!res.ok) { - const txt = await res.text().catch(() => '') - throw new Error(txt || `Server error (${res.status})`) - } - const json = await res.json() - const parsed = ExecuteResponseSuccessSchema.parse(json) - this.setState(ClientToolCallState.success) - await this.markToolComplete(200, 'API request executed', parsed.result) - this.setState(ClientToolCallState.success) - } catch (e: any) { - logger.error('execute failed', { message: e?.message }) - this.setState(ClientToolCallState.error) - await this.markToolComplete(500, e?.message || 'API request failed') - } + async handleAccept(_args?: MakeApiRequestArgs): Promise { + // Tool execution is handled server-side by the orchestrator. + this.setState(ClientToolCallState.executing) } - async execute(args?: MakeApiRequestArgs): Promise { - await this.handleAccept(args) + async execute(): Promise { + // Tool execution is handled server-side by the orchestrator. + // Client tool classes are retained for UI display configuration only. + this.setState(ClientToolCallState.success) } } diff --git a/apps/sim/lib/copilot/tools/client/other/search-documentation.ts b/apps/sim/lib/copilot/tools/client/other/search-documentation.ts index cf784d3f2..07fa971bb 100644 --- a/apps/sim/lib/copilot/tools/client/other/search-documentation.ts +++ b/apps/sim/lib/copilot/tools/client/other/search-documentation.ts @@ -1,17 +1,9 @@ -import { createLogger } from '@sim/logger' import { BookOpen, Loader2, MinusCircle, XCircle } from 'lucide-react' import { BaseClientTool, type BaseClientToolMetadata, ClientToolCallState, } from '@/lib/copilot/tools/client/base-tool' -import { ExecuteResponseSuccessSchema } from '@/lib/copilot/tools/shared/schemas' - -interface SearchDocumentationArgs { - query: string - topK?: number - threshold?: number -} export class SearchDocumentationClientTool extends BaseClientTool { static readonly id = 'search_documentation' @@ -53,28 +45,9 @@ export class SearchDocumentationClientTool extends BaseClientTool { }, } - async execute(args?: SearchDocumentationArgs): Promise { - const logger = createLogger('SearchDocumentationClientTool') - try { - this.setState(ClientToolCallState.executing) - const res = await fetch('/api/copilot/execute-copilot-server-tool', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ toolName: 'search_documentation', payload: args || {} }), - }) - if (!res.ok) { - const txt = await res.text().catch(() => '') - throw new Error(txt || `Server error (${res.status})`) - } - const json = await res.json() - const parsed = ExecuteResponseSuccessSchema.parse(json) - this.setState(ClientToolCallState.success) - await this.markToolComplete(200, 'Documentation search complete', parsed.result) - this.setState(ClientToolCallState.success) - } catch (e: any) { - logger.error('execute failed', { message: e?.message }) - this.setState(ClientToolCallState.error) - await this.markToolComplete(500, e?.message || 'Documentation search failed') - } + async execute(): Promise { + // Tool execution is handled server-side by the orchestrator. + // Client tool classes are retained for UI display configuration only. + this.setState(ClientToolCallState.success) } } diff --git a/apps/sim/lib/copilot/tools/client/user/get-credentials.ts b/apps/sim/lib/copilot/tools/client/user/get-credentials.ts index 8ad821b14..0623693c4 100644 --- a/apps/sim/lib/copilot/tools/client/user/get-credentials.ts +++ b/apps/sim/lib/copilot/tools/client/user/get-credentials.ts @@ -1,17 +1,9 @@ -import { createLogger } from '@sim/logger' import { Key, Loader2, MinusCircle, XCircle } from 'lucide-react' import { BaseClientTool, type BaseClientToolMetadata, ClientToolCallState, } from '@/lib/copilot/tools/client/base-tool' -import { ExecuteResponseSuccessSchema } from '@/lib/copilot/tools/shared/schemas' -import { useWorkflowRegistry } from '@/stores/workflows/registry/store' - -interface GetCredentialsArgs { - userId?: string - workflowId?: string -} export class GetCredentialsClientTool extends BaseClientTool { static readonly id = 'get_credentials' @@ -41,33 +33,9 @@ export class GetCredentialsClientTool extends BaseClientTool { }, } - async execute(args?: GetCredentialsArgs): Promise { - const logger = createLogger('GetCredentialsClientTool') - try { - this.setState(ClientToolCallState.executing) - const payload: GetCredentialsArgs = { ...(args || {}) } - if (!payload.workflowId && !payload.userId) { - const { activeWorkflowId } = useWorkflowRegistry.getState() - if (activeWorkflowId) payload.workflowId = activeWorkflowId - } - const res = await fetch('/api/copilot/execute-copilot-server-tool', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ toolName: 'get_credentials', payload }), - }) - if (!res.ok) { - const txt = await res.text().catch(() => '') - throw new Error(txt || `Server error (${res.status})`) - } - const json = await res.json() - const parsed = ExecuteResponseSuccessSchema.parse(json) - this.setState(ClientToolCallState.success) - await this.markToolComplete(200, 'Connected integrations fetched', parsed.result) - this.setState(ClientToolCallState.success) - } catch (e: any) { - logger.error('execute failed', { message: e?.message }) - this.setState(ClientToolCallState.error) - await this.markToolComplete(500, e?.message || 'Failed to fetch connected integrations') - } + async execute(): Promise { + // Tool execution is handled server-side by the orchestrator. + // Client tool classes are retained for UI display configuration only. + this.setState(ClientToolCallState.success) } } diff --git a/apps/sim/lib/copilot/tools/client/user/set-environment-variables.ts b/apps/sim/lib/copilot/tools/client/user/set-environment-variables.ts index e4033ca85..415987c8e 100644 --- a/apps/sim/lib/copilot/tools/client/user/set-environment-variables.ts +++ b/apps/sim/lib/copilot/tools/client/user/set-environment-variables.ts @@ -107,46 +107,15 @@ export class SetEnvironmentVariablesClientTool extends BaseClientTool { this.setState(ClientToolCallState.rejected) } - async handleAccept(args?: SetEnvArgs): Promise { - const logger = createLogger('SetEnvironmentVariablesClientTool') - try { - this.setState(ClientToolCallState.executing) - const payload: SetEnvArgs = { ...(args || { variables: {} }) } - if (!payload.workflowId) { - const { activeWorkflowId } = useWorkflowRegistry.getState() - if (activeWorkflowId) payload.workflowId = activeWorkflowId - } - const res = await fetch('/api/copilot/execute-copilot-server-tool', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ toolName: 'set_environment_variables', payload }), - }) - if (!res.ok) { - const txt = await res.text().catch(() => '') - throw new Error(txt || `Server error (${res.status})`) - } - const json = await res.json() - const parsed = ExecuteResponseSuccessSchema.parse(json) - this.setState(ClientToolCallState.success) - await this.markToolComplete(200, 'Environment variables updated', parsed.result) - this.setState(ClientToolCallState.success) - - // Refresh the environment store so the UI reflects the new variables - try { - await useEnvironmentStore.getState().loadEnvironmentVariables() - logger.info('Environment store refreshed after setting variables') - } catch (error) { - logger.warn('Failed to refresh environment store:', error) - } - } catch (e: any) { - logger.error('execute failed', { message: e?.message }) - this.setState(ClientToolCallState.error) - await this.markToolComplete(500, e?.message || 'Failed to set environment variables') - } + async handleAccept(_args?: SetEnvArgs): Promise { + // Tool execution is handled server-side by the orchestrator. + this.setState(ClientToolCallState.executing) } - async execute(args?: SetEnvArgs): Promise { - await this.handleAccept(args) + async execute(): Promise { + // Tool execution is handled server-side by the orchestrator. + // Client tool classes are retained for UI display configuration only. + this.setState(ClientToolCallState.success) } } diff --git a/apps/sim/lib/copilot/tools/client/workflow/get-workflow-console.ts b/apps/sim/lib/copilot/tools/client/workflow/get-workflow-console.ts index 328ae5aad..24f27713b 100644 --- a/apps/sim/lib/copilot/tools/client/workflow/get-workflow-console.ts +++ b/apps/sim/lib/copilot/tools/client/workflow/get-workflow-console.ts @@ -1,18 +1,9 @@ -import { createLogger } from '@sim/logger' import { Loader2, MinusCircle, TerminalSquare, XCircle } from 'lucide-react' import { BaseClientTool, type BaseClientToolMetadata, ClientToolCallState, } from '@/lib/copilot/tools/client/base-tool' -import { ExecuteResponseSuccessSchema } from '@/lib/copilot/tools/shared/schemas' -import { useWorkflowRegistry } from '@/stores/workflows/registry/store' - -interface GetWorkflowConsoleArgs { - workflowId?: string - limit?: number - includeDetails?: boolean -} export class GetWorkflowConsoleClientTool extends BaseClientTool { static readonly id = 'get_workflow_console' @@ -61,52 +52,9 @@ export class GetWorkflowConsoleClientTool extends BaseClientTool { }, } - async execute(args?: GetWorkflowConsoleArgs): Promise { - const logger = createLogger('GetWorkflowConsoleClientTool') - try { - this.setState(ClientToolCallState.executing) - - const params = args || {} - let workflowId = params.workflowId - if (!workflowId) { - const { activeWorkflowId } = useWorkflowRegistry.getState() - workflowId = activeWorkflowId || undefined - } - if (!workflowId) { - logger.error('No active workflow found for console fetch') - this.setState(ClientToolCallState.error) - await this.markToolComplete(400, 'No active workflow found') - return - } - - const payload = { - workflowId, - limit: params.limit ?? 3, - includeDetails: params.includeDetails ?? true, - } - - const res = await fetch('/api/copilot/execute-copilot-server-tool', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ toolName: 'get_workflow_console', payload }), - }) - if (!res.ok) { - const text = await res.text().catch(() => '') - throw new Error(text || `Server error (${res.status})`) - } - - const json = await res.json() - const parsed = ExecuteResponseSuccessSchema.parse(json) - - // Mark success and include result data for UI rendering - this.setState(ClientToolCallState.success) - await this.markToolComplete(200, 'Workflow console fetched', parsed.result) - this.setState(ClientToolCallState.success) - } catch (e: any) { - const message = e instanceof Error ? e.message : String(e) - createLogger('GetWorkflowConsoleClientTool').error('execute failed', { message }) - this.setState(ClientToolCallState.error) - await this.markToolComplete(500, message) - } + async execute(): Promise { + // Tool execution is handled server-side by the orchestrator. + // Client tool classes are retained for UI display configuration only. + this.setState(ClientToolCallState.success) } } diff --git a/apps/sim/stores/panel/copilot/store.ts b/apps/sim/stores/panel/copilot/store.ts index a17044afb..be32d1c72 100644 --- a/apps/sim/stores/panel/copilot/store.ts +++ b/apps/sim/stores/panel/copilot/store.ts @@ -4169,10 +4169,8 @@ export const useCopilotStore = create()( // Credential masking loadSensitiveCredentialIds: async () => { try { - const res = await fetch('/api/copilot/execute-copilot-server-tool', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ toolName: 'get_credentials', payload: {} }), + const res = await fetch('/api/copilot/credentials', { + credentials: 'include', }) if (!res.ok) { logger.warn('[loadSensitiveCredentialIds] Failed to fetch credentials', {