mirror of
https://github.com/simstudioai/sim.git
synced 2026-02-06 04:35:03 -05:00
Initial temp state, in the middle of a refactor
This commit is contained in:
@@ -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)
|
||||
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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<boolean> {
|
||||
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
|
||||
}
|
||||
|
||||
26
apps/sim/app/api/copilot/credentials/route.ts
Normal file
26
apps/sim/app/api/copilot/credentials/route.ts
Normal file
@@ -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 }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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<string, any> = resolveEnvVarReferences(
|
||||
toolArgs,
|
||||
decryptedEnvVars,
|
||||
{ deep: true }
|
||||
) as Record<string, any>
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -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(),
|
||||
|
||||
@@ -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')
|
||||
}
|
||||
}
|
||||
@@ -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<string, unknown>,
|
||||
userId: string
|
||||
): Promise<NextResponse> {
|
||||
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<string, unknown>) || {}
|
||||
|
||||
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<string, unknown>,
|
||||
userId: string
|
||||
): Promise<NextResponse> {
|
||||
// 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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -19,6 +19,7 @@ export const INTERRUPT_TOOL_SET = new Set<string>(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<string>(SUBAGENT_TOOL_NAMES)
|
||||
|
||||
/**
|
||||
* Respond tools are internal to the copilot's subagent system.
|
||||
* They're used by subagents to signal completion and should NOT be executed by the sim side.
|
||||
* The copilot backend handles these internally.
|
||||
*/
|
||||
export const RESPOND_TOOL_NAMES = [
|
||||
'plan_respond',
|
||||
'edit_respond',
|
||||
'build_respond',
|
||||
'debug_respond',
|
||||
'info_respond',
|
||||
'research_respond',
|
||||
'deploy_respond',
|
||||
'superagent_respond',
|
||||
'discovery_respond',
|
||||
] as const
|
||||
|
||||
export const RESPOND_TOOL_SET = new Set<string>(RESPOND_TOOL_NAMES)
|
||||
|
||||
@@ -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<string, unknown> | undefined
|
||||
const toolCallId = eventData?.tool_call_id as string | undefined
|
||||
if (toolCallId) {
|
||||
context.subAgentParentToolCallId = toolCallId
|
||||
context.subAgentContent[toolCallId] = ''
|
||||
|
||||
@@ -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<void> {
|
||||
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<void> {
|
||||
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<boolean> {
|
||||
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.
|
||||
*/
|
||||
|
||||
@@ -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<string, SSEHandler> = {
|
||||
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<string, SSEHandler> = {
|
||||
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<string, SSEHandler> = {
|
||||
}
|
||||
},
|
||||
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<string, SSEHandler> = {
|
||||
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<string, SSEHandler> = {
|
||||
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<string, SSEHandler> = {
|
||||
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
|
||||
|
||||
@@ -2,6 +2,10 @@ import type { SSEEvent } from '@/lib/copilot/orchestrator/types'
|
||||
|
||||
type EventDataObject = Record<string, any> | undefined
|
||||
|
||||
/** Safely cast event.data to a record for property access. */
|
||||
export const asRecord = (data: unknown): Record<string, any> =>
|
||||
(data && typeof data === 'object' && !Array.isArray(data) ? data : {}) as Record<string, any>
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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<OrchestratorOptions, 'onComplete'> {
|
||||
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<string, unknown> | undefined
|
||||
const toolCallId = eventData?.tool_call_id as string | undefined
|
||||
if (toolCallId) {
|
||||
context.subAgentParentToolCallId = toolCallId
|
||||
context.subAgentContent[toolCallId] = ''
|
||||
|
||||
@@ -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<string, any>,
|
||||
@@ -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<ToolCa
|
||||
return { success: false, error: error instanceof Error ? error.message : String(error) }
|
||||
}
|
||||
}
|
||||
|
||||
export async function executeCheckDeploymentStatus(
|
||||
params: Record<string, any>,
|
||||
context: ExecutionContext
|
||||
): Promise<ToolCallResult> {
|
||||
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<string, any>,
|
||||
context: ExecutionContext
|
||||
): Promise<ToolCallResult> {
|
||||
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<string, string[]> = {}
|
||||
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<string, any>,
|
||||
context: ExecutionContext
|
||||
): Promise<ToolCallResult> {
|
||||
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) }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from './deploy'
|
||||
export * from './manage'
|
||||
@@ -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<string, any>,
|
||||
context: ExecutionContext
|
||||
): Promise<ToolCallResult> {
|
||||
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<string, any>,
|
||||
context: ExecutionContext
|
||||
): Promise<ToolCallResult> {
|
||||
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<string, string[]> = {}
|
||||
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<string, any>,
|
||||
context: ExecutionContext
|
||||
): Promise<ToolCallResult> {
|
||||
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) }
|
||||
}
|
||||
}
|
||||
@@ -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<string>([
|
||||
'get_blocks_and_tools',
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from './queries'
|
||||
export * from './mutations'
|
||||
@@ -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<string, any>,
|
||||
context: ExecutionContext
|
||||
): Promise<ToolCallResult> {
|
||||
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<string, any>,
|
||||
context: ExecutionContext
|
||||
): Promise<ToolCallResult> {
|
||||
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<string, any>,
|
||||
context: ExecutionContext
|
||||
): Promise<ToolCallResult> {
|
||||
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<string, any>,
|
||||
context: ExecutionContext
|
||||
): Promise<ToolCallResult> {
|
||||
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<string, any>) || {}
|
||||
const byName: Record<string, any> = {}
|
||||
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) }
|
||||
}
|
||||
}
|
||||
@@ -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<string, any>,
|
||||
context: ExecutionContext
|
||||
): Promise<ToolCallResult> {
|
||||
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<string, any>,
|
||||
context: ExecutionContext
|
||||
): Promise<ToolCallResult> {
|
||||
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<string, any>,
|
||||
context: ExecutionContext
|
||||
@@ -587,140 +479,6 @@ export async function executeGetBlockUpstreamReferences(
|
||||
}
|
||||
}
|
||||
|
||||
export async function executeRunWorkflow(
|
||||
params: Record<string, any>,
|
||||
context: ExecutionContext
|
||||
): Promise<ToolCallResult> {
|
||||
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<string, any>,
|
||||
context: ExecutionContext
|
||||
): Promise<ToolCallResult> {
|
||||
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<string, any>) || {}
|
||||
const byName: Record<string, any> = {}
|
||||
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<Array<{ id: string; name: string; type: string; tag: string }>> {
|
||||
@@ -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<string, any>
|
||||
params?: Record<string, unknown>
|
||||
result?: ToolCallResult
|
||||
error?: string
|
||||
startTime?: number
|
||||
endTime?: number
|
||||
}
|
||||
|
||||
export interface ToolCallResult {
|
||||
export interface ToolCallResult<T = unknown> {
|
||||
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<string, any>
|
||||
result?: any
|
||||
params?: Record<string, unknown>
|
||||
result?: unknown
|
||||
error?: string
|
||||
durationMs?: number
|
||||
}
|
||||
|
||||
@@ -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<boolean> {
|
||||
// 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<boolean> {
|
||||
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
|
||||
|
||||
@@ -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<void> {
|
||||
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<void> {
|
||||
// Tool execution is handled server-side by the orchestrator.
|
||||
// Client tool classes are retained for UI display configuration only.
|
||||
this.setState(ClientToolCallState.success)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<void> {
|
||||
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<void> {
|
||||
// Tool execution is handled server-side by the orchestrator.
|
||||
// Client tool classes are retained for UI display configuration only.
|
||||
this.setState(ClientToolCallState.success)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<void> {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<void> {
|
||||
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<void> {
|
||||
// Tool execution is handled server-side by the orchestrator.
|
||||
// Client tool classes are retained for UI display configuration only.
|
||||
this.setState(ClientToolCallState.success)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<void> {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<void> {
|
||||
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<void> {
|
||||
// Tool execution is handled server-side by the orchestrator.
|
||||
// Client tool classes are retained for UI display configuration only.
|
||||
this.setState(ClientToolCallState.success)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -93,33 +93,15 @@ export class MakeApiRequestClientTool extends BaseClientTool {
|
||||
this.setState(ClientToolCallState.rejected)
|
||||
}
|
||||
|
||||
async handleAccept(args?: MakeApiRequestArgs): Promise<void> {
|
||||
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<void> {
|
||||
// Tool execution is handled server-side by the orchestrator.
|
||||
this.setState(ClientToolCallState.executing)
|
||||
}
|
||||
|
||||
async execute(args?: MakeApiRequestArgs): Promise<void> {
|
||||
await this.handleAccept(args)
|
||||
async execute(): Promise<void> {
|
||||
// Tool execution is handled server-side by the orchestrator.
|
||||
// Client tool classes are retained for UI display configuration only.
|
||||
this.setState(ClientToolCallState.success)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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<void> {
|
||||
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<void> {
|
||||
// Tool execution is handled server-side by the orchestrator.
|
||||
// Client tool classes are retained for UI display configuration only.
|
||||
this.setState(ClientToolCallState.success)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<void> {
|
||||
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<void> {
|
||||
// Tool execution is handled server-side by the orchestrator.
|
||||
// Client tool classes are retained for UI display configuration only.
|
||||
this.setState(ClientToolCallState.success)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -107,46 +107,15 @@ export class SetEnvironmentVariablesClientTool extends BaseClientTool {
|
||||
this.setState(ClientToolCallState.rejected)
|
||||
}
|
||||
|
||||
async handleAccept(args?: SetEnvArgs): Promise<void> {
|
||||
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<void> {
|
||||
// Tool execution is handled server-side by the orchestrator.
|
||||
this.setState(ClientToolCallState.executing)
|
||||
}
|
||||
|
||||
async execute(args?: SetEnvArgs): Promise<void> {
|
||||
await this.handleAccept(args)
|
||||
async execute(): Promise<void> {
|
||||
// Tool execution is handled server-side by the orchestrator.
|
||||
// Client tool classes are retained for UI display configuration only.
|
||||
this.setState(ClientToolCallState.success)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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<void> {
|
||||
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<void> {
|
||||
// Tool execution is handled server-side by the orchestrator.
|
||||
// Client tool classes are retained for UI display configuration only.
|
||||
this.setState(ClientToolCallState.success)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4169,10 +4169,8 @@ export const useCopilotStore = create<CopilotStore>()(
|
||||
// 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', {
|
||||
|
||||
Reference in New Issue
Block a user