mirror of
https://github.com/simstudioai/sim.git
synced 2026-02-04 11:45:07 -05:00
BROKEN
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
import { db } from '@sim/db'
|
||||
import { copilotChats, permissions, workflow } from '@sim/db/schema'
|
||||
import { copilotChats } from '@sim/db/schema'
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { and, asc, desc, eq, inArray, or } from 'drizzle-orm'
|
||||
import { and, desc, eq } from 'drizzle-orm'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { getSession } from '@/lib/auth'
|
||||
@@ -21,6 +21,7 @@ import type { CopilotProviderConfig } from '@/lib/copilot/types'
|
||||
import { env } from '@/lib/core/config/env'
|
||||
import { CopilotFiles } from '@/lib/uploads'
|
||||
import { createFileContent } from '@/lib/uploads/utils/file-utils'
|
||||
import { resolveWorkflowIdForUser } from '@/lib/workflows/utils'
|
||||
import { tools } from '@/tools/registry'
|
||||
import { getLatestVersionTools, stripVersionSuffix } from '@/tools/utils'
|
||||
import { orchestrateCopilotStream } from '@/lib/copilot/orchestrator'
|
||||
@@ -79,54 +80,6 @@ const ChatMessageSchema = z.object({
|
||||
commands: z.array(z.string()).optional(),
|
||||
})
|
||||
|
||||
async function resolveWorkflowId(
|
||||
userId: string,
|
||||
workflowId?: string,
|
||||
workflowName?: string
|
||||
): Promise<{ workflowId: string; workflowName?: string } | null> {
|
||||
// If workflowId provided, use it directly
|
||||
if (workflowId) {
|
||||
return { workflowId }
|
||||
}
|
||||
|
||||
// Get user's accessible workflows
|
||||
const workspaceIds = await db
|
||||
.select({ entityId: permissions.entityId })
|
||||
.from(permissions)
|
||||
.where(and(eq(permissions.userId, userId), eq(permissions.entityType, 'workspace')))
|
||||
|
||||
const workspaceIdList = workspaceIds.map((row) => row.entityId)
|
||||
|
||||
const workflowConditions = [eq(workflow.userId, userId)]
|
||||
if (workspaceIdList.length > 0) {
|
||||
workflowConditions.push(inArray(workflow.workspaceId, workspaceIdList))
|
||||
}
|
||||
|
||||
const workflows = await db
|
||||
.select()
|
||||
.from(workflow)
|
||||
.where(or(...workflowConditions))
|
||||
.orderBy(asc(workflow.sortOrder), asc(workflow.createdAt), asc(workflow.id))
|
||||
|
||||
if (workflows.length === 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
// If workflowName provided, find matching workflow
|
||||
if (workflowName) {
|
||||
const match = workflows.find(
|
||||
(w) => String(w.name || '').trim().toLowerCase() === workflowName.toLowerCase()
|
||||
)
|
||||
if (match) {
|
||||
return { workflowId: match.id, workflowName: match.name || undefined }
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
// Default to first workflow
|
||||
return { workflowId: workflows[0].id, workflowName: workflows[0].name || undefined }
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /api/copilot/chat
|
||||
* Send messages to sim agent and handle chat persistence
|
||||
@@ -165,7 +118,11 @@ export async function POST(req: NextRequest) {
|
||||
} = ChatMessageSchema.parse(body)
|
||||
|
||||
// Resolve workflowId - if not provided, use first workflow or find by name
|
||||
const resolved = await resolveWorkflowId(authenticatedUserId, providedWorkflowId, workflowName)
|
||||
const resolved = await resolveWorkflowIdForUser(
|
||||
authenticatedUserId,
|
||||
providedWorkflowId,
|
||||
workflowName
|
||||
)
|
||||
if (!resolved) {
|
||||
return createBadRequestResponse(
|
||||
'No workflows found. Create a workflow first or provide a valid workflowId.'
|
||||
|
||||
@@ -15,6 +15,7 @@ import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { checkHybridAuth } from '@/lib/auth/hybrid'
|
||||
import { getCopilotModel } from '@/lib/copilot/config'
|
||||
import { orchestrateSubagentStream } from '@/lib/copilot/orchestrator/subagent'
|
||||
import { DIRECT_TOOL_DEFS, SUBAGENT_TOOL_DEFS } from '@/lib/copilot/tools/mcp/definitions'
|
||||
import { executeToolServerSide, prepareExecutionContext } from '@/lib/copilot/orchestrator/tool-executor'
|
||||
|
||||
const logger = createLogger('CopilotMcpAPI')
|
||||
@@ -167,468 +168,6 @@ Don't guess. Don't plan. Debug first to find the actual problem.
|
||||
- copilot_debug: Diagnose errors (REQUIRES workflowId) - USE THIS FIRST for issues
|
||||
`
|
||||
|
||||
/**
|
||||
* Direct tools that execute immediately without LLM orchestration.
|
||||
* These are fast database queries that don't need AI reasoning.
|
||||
*/
|
||||
const DIRECT_TOOL_DEFS: Array<{
|
||||
name: string
|
||||
description: string
|
||||
inputSchema: { type: 'object'; properties?: Record<string, unknown>; required?: string[] }
|
||||
toolId: string
|
||||
}> = [
|
||||
{
|
||||
name: 'list_workflows',
|
||||
toolId: 'list_user_workflows',
|
||||
description: 'List all workflows the user has access to. Returns workflow IDs, names, and workspace info.',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
workspaceId: {
|
||||
type: 'string',
|
||||
description: 'Optional workspace ID to filter workflows.',
|
||||
},
|
||||
folderId: {
|
||||
type: 'string',
|
||||
description: 'Optional folder ID to filter workflows.',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'list_workspaces',
|
||||
toolId: 'list_user_workspaces',
|
||||
description: 'List all workspaces the user has access to. Returns workspace IDs, names, and roles.',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'list_folders',
|
||||
toolId: 'list_folders',
|
||||
description: 'List all folders in a workspace.',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
workspaceId: {
|
||||
type: 'string',
|
||||
description: 'Workspace ID to list folders from.',
|
||||
},
|
||||
},
|
||||
required: ['workspaceId'],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'get_workflow',
|
||||
toolId: 'get_workflow_from_name',
|
||||
description: 'Get a workflow by name or ID. Returns the full workflow definition.',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
name: {
|
||||
type: 'string',
|
||||
description: 'Workflow name to search for.',
|
||||
},
|
||||
workflowId: {
|
||||
type: 'string',
|
||||
description: 'Workflow ID to retrieve directly.',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'create_workflow',
|
||||
toolId: 'create_workflow',
|
||||
description: 'Create a new workflow. Returns the new workflow ID.',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
name: {
|
||||
type: 'string',
|
||||
description: 'Name for the new workflow.',
|
||||
},
|
||||
workspaceId: {
|
||||
type: 'string',
|
||||
description: 'Optional workspace ID. Uses default workspace if not provided.',
|
||||
},
|
||||
folderId: {
|
||||
type: 'string',
|
||||
description: 'Optional folder ID to place the workflow in.',
|
||||
},
|
||||
description: {
|
||||
type: 'string',
|
||||
description: 'Optional description for the workflow.',
|
||||
},
|
||||
},
|
||||
required: ['name'],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'create_folder',
|
||||
toolId: 'create_folder',
|
||||
description: 'Create a new folder in a workspace.',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
name: {
|
||||
type: 'string',
|
||||
description: 'Name for the new folder.',
|
||||
},
|
||||
workspaceId: {
|
||||
type: 'string',
|
||||
description: 'Optional workspace ID. Uses default workspace if not provided.',
|
||||
},
|
||||
parentId: {
|
||||
type: 'string',
|
||||
description: 'Optional parent folder ID for nested folders.',
|
||||
},
|
||||
},
|
||||
required: ['name'],
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
const SUBAGENT_TOOL_DEFS: Array<{
|
||||
name: string
|
||||
description: string
|
||||
inputSchema: { type: 'object'; properties?: Record<string, unknown>; required?: string[] }
|
||||
agentId: string
|
||||
}> = [
|
||||
{
|
||||
name: 'copilot_build',
|
||||
agentId: 'build',
|
||||
description: `Build a workflow end-to-end in a single step. This is the fast mode equivalent for headless/MCP usage.
|
||||
|
||||
USE THIS WHEN:
|
||||
- Building a new workflow from scratch
|
||||
- Modifying an existing workflow
|
||||
- You want to gather information and build in one pass without separate plan→edit steps
|
||||
|
||||
WORKFLOW ID (REQUIRED):
|
||||
- For NEW workflows: First call create_workflow to get a workflowId, then pass it here
|
||||
- For EXISTING workflows: Always pass the workflowId parameter
|
||||
|
||||
CAN DO:
|
||||
- Gather information about blocks, credentials, patterns
|
||||
- Search documentation and patterns for best practices
|
||||
- Add, modify, or remove blocks
|
||||
- Configure block settings and connections
|
||||
- Set environment variables and workflow variables
|
||||
|
||||
CANNOT DO:
|
||||
- Run or test workflows (use copilot_test separately after deploying)
|
||||
- Deploy workflows (use copilot_deploy separately)
|
||||
|
||||
WORKFLOW:
|
||||
1. Call create_workflow to get a workflowId (for new workflows)
|
||||
2. Call copilot_build with the request and workflowId
|
||||
3. Build agent gathers info and builds in one pass
|
||||
4. Call copilot_deploy to deploy the workflow
|
||||
5. Optionally call copilot_test to verify it works`,
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
request: {
|
||||
type: 'string',
|
||||
description: 'What you want to build or modify in the workflow.',
|
||||
},
|
||||
workflowId: {
|
||||
type: 'string',
|
||||
description:
|
||||
'REQUIRED. The workflow ID. For new workflows, call create_workflow first to get this.',
|
||||
},
|
||||
context: { type: 'object' },
|
||||
},
|
||||
required: ['request', 'workflowId'],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'copilot_discovery',
|
||||
agentId: 'discovery',
|
||||
description: `Find workflows by their contents or functionality when the user doesn't know the exact name or ID.
|
||||
|
||||
USE THIS WHEN:
|
||||
- User describes a workflow by what it does: "the one that sends emails", "my Slack notification workflow"
|
||||
- User refers to workflow contents: "the workflow with the OpenAI block"
|
||||
- User needs to search/match workflows by functionality or description
|
||||
|
||||
DO NOT USE (use direct tools instead):
|
||||
- User knows the workflow name → use get_workflow
|
||||
- User wants to list all workflows → use list_workflows
|
||||
- User wants to list workspaces → use list_workspaces
|
||||
- User wants to list folders → use list_folders`,
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
request: { type: 'string' },
|
||||
workspaceId: { type: 'string' },
|
||||
context: { type: 'object' },
|
||||
},
|
||||
required: ['request'],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'copilot_plan',
|
||||
agentId: 'plan',
|
||||
description: `Plan workflow changes by gathering required information.
|
||||
|
||||
USE THIS WHEN:
|
||||
- Building a new workflow
|
||||
- Modifying an existing workflow
|
||||
- You need to understand what blocks and integrations are available
|
||||
- The workflow requires multiple blocks or connections
|
||||
|
||||
WORKFLOW ID (REQUIRED):
|
||||
- For NEW workflows: First call create_workflow to get a workflowId, then pass it here
|
||||
- For EXISTING workflows: Always pass the workflowId parameter
|
||||
|
||||
This tool gathers information about available blocks, credentials, and the current workflow state.
|
||||
|
||||
RETURNS: A plan object containing block configurations, connections, and technical details.
|
||||
IMPORTANT: Pass the returned plan EXACTLY to copilot_edit - do not modify or summarize it.`,
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
request: { type: 'string', description: 'What you want to build or modify in the workflow.' },
|
||||
workflowId: {
|
||||
type: 'string',
|
||||
description: 'REQUIRED. The workflow ID. For new workflows, call create_workflow first to get this.',
|
||||
},
|
||||
context: { type: 'object' },
|
||||
},
|
||||
required: ['request', 'workflowId'],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'copilot_edit',
|
||||
agentId: 'edit',
|
||||
description: `Execute a workflow plan and apply edits.
|
||||
|
||||
USE THIS WHEN:
|
||||
- You have a plan from copilot_plan that needs to be executed
|
||||
- Building or modifying a workflow based on the plan
|
||||
- Making changes to blocks, connections, or configurations
|
||||
|
||||
WORKFLOW ID (REQUIRED):
|
||||
- You MUST provide the workflowId parameter
|
||||
- For new workflows, get the workflowId from create_workflow first
|
||||
|
||||
PLAN (REQUIRED):
|
||||
- Pass the EXACT plan object from copilot_plan in the context.plan field
|
||||
- Do NOT modify, summarize, or interpret the plan - pass it verbatim
|
||||
- The plan contains technical details the edit agent needs exactly as-is
|
||||
|
||||
IMPORTANT: After copilot_edit completes, you MUST call copilot_deploy before the workflow can be run or tested.`,
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
message: { type: 'string', description: 'Optional additional instructions for the edit.' },
|
||||
workflowId: {
|
||||
type: 'string',
|
||||
description: 'REQUIRED. The workflow ID to edit. Get this from create_workflow for new workflows.',
|
||||
},
|
||||
plan: {
|
||||
type: 'object',
|
||||
description: 'The plan object from copilot_plan. Pass it EXACTLY as returned, do not modify.',
|
||||
},
|
||||
context: {
|
||||
type: 'object',
|
||||
description: 'Additional context. Put the plan in context.plan if not using the plan field directly.',
|
||||
},
|
||||
},
|
||||
required: ['workflowId'],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'copilot_debug',
|
||||
agentId: 'debug',
|
||||
description: `Diagnose errors or unexpected workflow behavior.
|
||||
|
||||
WORKFLOW ID (REQUIRED): Always provide the workflowId of the workflow to debug.`,
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
error: { type: 'string', description: 'The error message or description of the issue.' },
|
||||
workflowId: { type: 'string', description: 'REQUIRED. The workflow ID to debug.' },
|
||||
context: { type: 'object' },
|
||||
},
|
||||
required: ['error', 'workflowId'],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'copilot_deploy',
|
||||
agentId: 'deploy',
|
||||
description: `Deploy or manage workflow deployments.
|
||||
|
||||
CRITICAL: You MUST deploy a workflow after building before it can be run or tested.
|
||||
Workflows without an active deployment will fail with "no active deployment" error.
|
||||
|
||||
WORKFLOW ID (REQUIRED):
|
||||
- Always provide the workflowId parameter
|
||||
- This must match the workflow you built with copilot_edit
|
||||
|
||||
USE THIS:
|
||||
- After copilot_edit completes to activate the workflow
|
||||
- To update deployment settings
|
||||
- To redeploy after making changes
|
||||
|
||||
DEPLOYMENT TYPES:
|
||||
- "deploy as api" - REST API endpoint
|
||||
- "deploy as chat" - Chat interface
|
||||
- "deploy as mcp" - MCP server`,
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
request: {
|
||||
type: 'string',
|
||||
description: 'The deployment request, e.g. "deploy as api" or "deploy as chat"',
|
||||
},
|
||||
workflowId: {
|
||||
type: 'string',
|
||||
description: 'REQUIRED. The workflow ID to deploy.',
|
||||
},
|
||||
context: { type: 'object' },
|
||||
},
|
||||
required: ['request', 'workflowId'],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'copilot_auth',
|
||||
agentId: 'auth',
|
||||
description: 'Handle OAuth connection flows.',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
request: { type: 'string' },
|
||||
context: { type: 'object' },
|
||||
},
|
||||
required: ['request'],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'copilot_knowledge',
|
||||
agentId: 'knowledge',
|
||||
description: 'Create and manage knowledge bases.',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
request: { type: 'string' },
|
||||
context: { type: 'object' },
|
||||
},
|
||||
required: ['request'],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'copilot_custom_tool',
|
||||
agentId: 'custom_tool',
|
||||
description: 'Create or manage custom tools.',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
request: { type: 'string' },
|
||||
context: { type: 'object' },
|
||||
},
|
||||
required: ['request'],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'copilot_info',
|
||||
agentId: 'info',
|
||||
description: 'Inspect blocks, outputs, and workflow metadata.',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
request: { type: 'string' },
|
||||
workflowId: { type: 'string' },
|
||||
context: { type: 'object' },
|
||||
},
|
||||
required: ['request'],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'copilot_workflow',
|
||||
agentId: 'workflow',
|
||||
description: 'Manage workflow environment and configuration.',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
request: { type: 'string' },
|
||||
workflowId: { type: 'string' },
|
||||
context: { type: 'object' },
|
||||
},
|
||||
required: ['request'],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'copilot_research',
|
||||
agentId: 'research',
|
||||
description: 'Research external APIs and documentation.',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
request: { type: 'string' },
|
||||
context: { type: 'object' },
|
||||
},
|
||||
required: ['request'],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'copilot_tour',
|
||||
agentId: 'tour',
|
||||
description: 'Explain platform features and usage.',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
request: { type: 'string' },
|
||||
context: { type: 'object' },
|
||||
},
|
||||
required: ['request'],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'copilot_test',
|
||||
agentId: 'test',
|
||||
description: `Run workflows and verify outputs.
|
||||
|
||||
PREREQUISITE: The workflow MUST be deployed first using copilot_deploy.
|
||||
Undeployed workflows will fail with "no active deployment" error.
|
||||
|
||||
WORKFLOW ID (REQUIRED):
|
||||
- Always provide the workflowId parameter
|
||||
|
||||
USE THIS:
|
||||
- After deploying to verify the workflow works correctly
|
||||
- To test with sample inputs
|
||||
- To validate workflow behavior before sharing with user`,
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
request: { type: 'string' },
|
||||
workflowId: {
|
||||
type: 'string',
|
||||
description: 'REQUIRED. The workflow ID to test.',
|
||||
},
|
||||
context: { type: 'object' },
|
||||
},
|
||||
required: ['request', 'workflowId'],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'copilot_superagent',
|
||||
agentId: 'superagent',
|
||||
description: 'Execute direct external actions (email, Slack, etc.).',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
request: { type: 'string' },
|
||||
context: { type: 'object' },
|
||||
},
|
||||
required: ['request'],
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
function createResponse(id: RequestId, result: unknown): JSONRPCResponse {
|
||||
return {
|
||||
jsonrpc: '2.0',
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
import { db } from '@sim/db'
|
||||
import { permissions, workflow } from '@sim/db/schema'
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { and, asc, eq, inArray, or } from 'drizzle-orm'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { authenticateV1Request } from '@/app/api/v1/auth'
|
||||
@@ -9,6 +6,7 @@ import { getCopilotModel } from '@/lib/copilot/config'
|
||||
import { SIM_AGENT_VERSION } from '@/lib/copilot/constants'
|
||||
import { COPILOT_REQUEST_MODES } from '@/lib/copilot/models'
|
||||
import { orchestrateCopilotStream } from '@/lib/copilot/orchestrator'
|
||||
import { resolveWorkflowIdForUser } from '@/lib/workflows/utils'
|
||||
|
||||
const logger = createLogger('CopilotHeadlessAPI')
|
||||
|
||||
@@ -23,54 +21,6 @@ const RequestSchema = z.object({
|
||||
timeout: z.number().optional().default(300000),
|
||||
})
|
||||
|
||||
async function resolveWorkflowId(
|
||||
userId: string,
|
||||
workflowId?: string,
|
||||
workflowName?: string
|
||||
): Promise<{ workflowId: string; workflowName?: string } | null> {
|
||||
// If workflowId provided, use it directly
|
||||
if (workflowId) {
|
||||
return { workflowId }
|
||||
}
|
||||
|
||||
// Get user's accessible workflows
|
||||
const workspaceIds = await db
|
||||
.select({ entityId: permissions.entityId })
|
||||
.from(permissions)
|
||||
.where(and(eq(permissions.userId, userId), eq(permissions.entityType, 'workspace')))
|
||||
|
||||
const workspaceIdList = workspaceIds.map((row) => row.entityId)
|
||||
|
||||
const workflowConditions = [eq(workflow.userId, userId)]
|
||||
if (workspaceIdList.length > 0) {
|
||||
workflowConditions.push(inArray(workflow.workspaceId, workspaceIdList))
|
||||
}
|
||||
|
||||
const workflows = await db
|
||||
.select()
|
||||
.from(workflow)
|
||||
.where(or(...workflowConditions))
|
||||
.orderBy(asc(workflow.sortOrder), asc(workflow.createdAt), asc(workflow.id))
|
||||
|
||||
if (workflows.length === 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
// If workflowName provided, find matching workflow
|
||||
if (workflowName) {
|
||||
const match = workflows.find(
|
||||
(w) => String(w.name || '').trim().toLowerCase() === workflowName.toLowerCase()
|
||||
)
|
||||
if (match) {
|
||||
return { workflowId: match.id, workflowName: match.name || undefined }
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
// Default to first workflow
|
||||
return { workflowId: workflows[0].id, workflowName: workflows[0].name || undefined }
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /api/v1/copilot/chat
|
||||
* Headless copilot endpoint for server-side orchestration.
|
||||
@@ -93,7 +43,7 @@ export async function POST(req: NextRequest) {
|
||||
const selectedModel = parsed.model || defaults.model
|
||||
|
||||
// Resolve workflow ID
|
||||
const resolved = await resolveWorkflowId(auth.userId, parsed.workflowId, parsed.workflowName)
|
||||
const resolved = await resolveWorkflowIdForUser(auth.userId, parsed.workflowId, parsed.workflowName)
|
||||
if (!resolved) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: 'No workflows found. Create a workflow first or provide a valid workflowId.' },
|
||||
|
||||
@@ -26,7 +26,6 @@ import { getBlock } from '@/blocks/registry'
|
||||
import type { CopilotToolCall } from '@/stores/panel'
|
||||
import { useCopilotStore } from '@/stores/panel'
|
||||
import { CLASS_TOOL_METADATA } from '@/stores/panel/copilot/store'
|
||||
import { COPILOT_SERVER_ORCHESTRATED } from '@/lib/copilot/orchestrator/config'
|
||||
import type { SubAgentContentBlock } from '@/stores/panel/copilot/types'
|
||||
import { useWorkflowStore } from '@/stores/workflows/workflow/store'
|
||||
|
||||
@@ -1263,7 +1262,7 @@ function shouldShowRunSkipButtons(toolCall: CopilotToolCall): boolean {
|
||||
|
||||
const toolCallLogger = createLogger('CopilotToolCall')
|
||||
|
||||
async function sendToolDecision(toolCallId: string, status: 'accepted' | 'rejected') {
|
||||
async function sendToolDecision(toolCallId: string, status: 'accepted' | 'rejected' | 'background') {
|
||||
try {
|
||||
await fetch('/api/copilot/confirm', {
|
||||
method: 'POST',
|
||||
@@ -1285,105 +1284,15 @@ async function handleRun(
|
||||
onStateChange?: any,
|
||||
editedParams?: any
|
||||
) {
|
||||
if (COPILOT_SERVER_ORCHESTRATED) {
|
||||
setToolCallState(toolCall, 'executing')
|
||||
onStateChange?.('executing')
|
||||
await sendToolDecision(toolCall.id, 'accepted')
|
||||
return
|
||||
}
|
||||
const instance = getClientTool(toolCall.id)
|
||||
|
||||
if (!instance && isIntegrationTool(toolCall.name)) {
|
||||
onStateChange?.('executing')
|
||||
try {
|
||||
await useCopilotStore.getState().executeIntegrationTool(toolCall.id)
|
||||
} catch (e) {
|
||||
setToolCallState(toolCall, 'error', { error: e instanceof Error ? e.message : String(e) })
|
||||
onStateChange?.('error')
|
||||
try {
|
||||
await fetch('/api/copilot/tools/mark-complete', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
id: toolCall.id,
|
||||
name: toolCall.name,
|
||||
status: 500,
|
||||
message: e instanceof Error ? e.message : 'Tool execution failed',
|
||||
data: { error: e instanceof Error ? e.message : String(e) },
|
||||
}),
|
||||
})
|
||||
} catch {
|
||||
console.error('[handleRun] Failed to notify backend of tool error:', toolCall.id)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if (!instance) return
|
||||
try {
|
||||
const mergedParams =
|
||||
editedParams ||
|
||||
(toolCall as any).params ||
|
||||
(toolCall as any).parameters ||
|
||||
(toolCall as any).input ||
|
||||
{}
|
||||
await instance.handleAccept?.(mergedParams)
|
||||
onStateChange?.('executing')
|
||||
} catch (e) {
|
||||
setToolCallState(toolCall, 'error', { error: e instanceof Error ? e.message : String(e) })
|
||||
}
|
||||
setToolCallState(toolCall, 'executing', editedParams ? { params: editedParams } : undefined)
|
||||
onStateChange?.('executing')
|
||||
await sendToolDecision(toolCall.id, 'accepted')
|
||||
}
|
||||
|
||||
async function handleSkip(toolCall: CopilotToolCall, setToolCallState: any, onStateChange?: any) {
|
||||
if (COPILOT_SERVER_ORCHESTRATED) {
|
||||
setToolCallState(toolCall, 'rejected')
|
||||
onStateChange?.('rejected')
|
||||
await sendToolDecision(toolCall.id, 'rejected')
|
||||
return
|
||||
}
|
||||
const instance = getClientTool(toolCall.id)
|
||||
|
||||
if (!instance && isIntegrationTool(toolCall.name)) {
|
||||
setToolCallState(toolCall, 'rejected')
|
||||
onStateChange?.('rejected')
|
||||
|
||||
let notified = false
|
||||
for (let attempt = 0; attempt < 3 && !notified; attempt++) {
|
||||
try {
|
||||
const res = await fetch('/api/copilot/tools/mark-complete', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
id: toolCall.id,
|
||||
name: toolCall.name,
|
||||
status: 400,
|
||||
message: 'Tool execution skipped by user',
|
||||
data: { skipped: true, reason: 'user_skipped' },
|
||||
}),
|
||||
})
|
||||
if (res.ok) {
|
||||
notified = true
|
||||
}
|
||||
} catch (e) {
|
||||
if (attempt < 2) {
|
||||
await new Promise((resolve) => setTimeout(resolve, 500))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!notified) {
|
||||
console.error('[handleSkip] Failed to notify backend after 3 attempts:', toolCall.id)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if (instance) {
|
||||
try {
|
||||
await instance.handleReject?.()
|
||||
} catch {}
|
||||
}
|
||||
setToolCallState(toolCall, 'rejected')
|
||||
onStateChange?.('rejected')
|
||||
await sendToolDecision(toolCall.id, 'rejected')
|
||||
}
|
||||
|
||||
function getDisplayName(toolCall: CopilotToolCall): string {
|
||||
@@ -1543,7 +1452,7 @@ export function ToolCall({
|
||||
// Check if this integration tool is auto-allowed
|
||||
// Subscribe to autoAllowedTools so we re-render when it changes
|
||||
const autoAllowedTools = useCopilotStore((s) => s.autoAllowedTools)
|
||||
const { removeAutoAllowedTool } = useCopilotStore()
|
||||
const { removeAutoAllowedTool, setToolCallState } = useCopilotStore()
|
||||
const isAutoAllowed = isIntegrationTool(toolCall.name) && autoAllowedTools.includes(toolCall.name)
|
||||
|
||||
// Update edited params when toolCall params change (deep comparison to avoid resetting user edits on ref change)
|
||||
@@ -2245,16 +2154,9 @@ export function ToolCall({
|
||||
<div className='mt-[10px]'>
|
||||
<Button
|
||||
onClick={async () => {
|
||||
try {
|
||||
const instance = getClientTool(toolCall.id)
|
||||
instance?.setState?.((ClientToolCallState as any).background)
|
||||
await instance?.markToolComplete?.(
|
||||
200,
|
||||
'The user has chosen to move the workflow execution to the background. Check back with them later to know when the workflow execution is complete'
|
||||
)
|
||||
forceUpdate({})
|
||||
onStateChange?.('background')
|
||||
} catch {}
|
||||
setToolCallState(toolCall, ClientToolCallState.background)
|
||||
onStateChange?.('background')
|
||||
await sendToolDecision(toolCall.id, 'background')
|
||||
}}
|
||||
variant='tertiary'
|
||||
title='Move to Background'
|
||||
@@ -2266,21 +2168,9 @@ export function ToolCall({
|
||||
<div className='mt-[10px]'>
|
||||
<Button
|
||||
onClick={async () => {
|
||||
try {
|
||||
const instance = getClientTool(toolCall.id)
|
||||
const elapsedSeconds = instance?.getElapsedSeconds?.() || 0
|
||||
instance?.setState?.((ClientToolCallState as any).background, {
|
||||
result: { _elapsedSeconds: elapsedSeconds },
|
||||
})
|
||||
const { updateToolCallParams } = useCopilotStore.getState()
|
||||
updateToolCallParams?.(toolCall.id, { _elapsedSeconds: Math.round(elapsedSeconds) })
|
||||
await instance?.markToolComplete?.(
|
||||
200,
|
||||
`User woke you up after ${Math.round(elapsedSeconds)} seconds`
|
||||
)
|
||||
forceUpdate({})
|
||||
onStateChange?.('background')
|
||||
} catch {}
|
||||
setToolCallState(toolCall, ClientToolCallState.background)
|
||||
onStateChange?.('background')
|
||||
await sendToolDecision(toolCall.id, 'background')
|
||||
}}
|
||||
variant='tertiary'
|
||||
title='Wake'
|
||||
|
||||
@@ -1,8 +1,3 @@
|
||||
/**
|
||||
* Feature flag for server-side copilot orchestration.
|
||||
*/
|
||||
export const COPILOT_SERVER_ORCHESTRATED = true
|
||||
|
||||
export const INTERRUPT_TOOL_NAMES = [
|
||||
'set_global_workflow_variables',
|
||||
'run_workflow',
|
||||
|
||||
@@ -22,7 +22,11 @@ import { env } from '@/lib/core/config/env'
|
||||
import { getEffectiveDecryptedEnv } from '@/lib/environment/utils'
|
||||
import { listWorkspaceFiles } from '@/lib/uploads/contexts/workspace'
|
||||
import { mcpService } from '@/lib/mcp/service'
|
||||
import { sanitizeForCopilot } from '@/lib/workflows/sanitization/json-sanitizer'
|
||||
import {
|
||||
extractWorkflowNames,
|
||||
formatNormalizedWorkflowForCopilot,
|
||||
normalizeWorkflowName,
|
||||
} from '@/lib/copilot/tools/shared/workflow-utils'
|
||||
import { buildDefaultWorkflowArtifacts } from '@/lib/workflows/defaults'
|
||||
import {
|
||||
deployWorkflow,
|
||||
@@ -373,6 +377,35 @@ async function ensureWorkspaceAccess(
|
||||
}
|
||||
}
|
||||
|
||||
async function getAccessibleWorkflowsForUser(
|
||||
userId: string,
|
||||
options?: { workspaceId?: string; folderId?: string }
|
||||
) {
|
||||
const workspaceIds = await db
|
||||
.select({ entityId: permissions.entityId })
|
||||
.from(permissions)
|
||||
.where(and(eq(permissions.userId, userId), eq(permissions.entityType, 'workspace')))
|
||||
|
||||
const workspaceIdList = workspaceIds.map((row) => row.entityId)
|
||||
|
||||
const workflowConditions = [eq(workflow.userId, userId)]
|
||||
if (workspaceIdList.length > 0) {
|
||||
workflowConditions.push(inArray(workflow.workspaceId, workspaceIdList))
|
||||
}
|
||||
if (options?.workspaceId) {
|
||||
workflowConditions.push(eq(workflow.workspaceId, options.workspaceId))
|
||||
}
|
||||
if (options?.folderId) {
|
||||
workflowConditions.push(eq(workflow.folderId, options.folderId))
|
||||
}
|
||||
|
||||
return db
|
||||
.select()
|
||||
.from(workflow)
|
||||
.where(or(...workflowConditions))
|
||||
.orderBy(asc(workflow.sortOrder), asc(workflow.createdAt), asc(workflow.id))
|
||||
}
|
||||
|
||||
async function executeGetUserWorkflow(
|
||||
params: Record<string, any>,
|
||||
context: ExecutionContext
|
||||
@@ -389,19 +422,11 @@ async function executeGetUserWorkflow(
|
||||
)
|
||||
|
||||
const normalized = await loadWorkflowFromNormalizedTables(workflowId)
|
||||
if (!normalized) {
|
||||
const userWorkflow = formatNormalizedWorkflowForCopilot(normalized)
|
||||
if (!userWorkflow) {
|
||||
return { success: false, error: 'Workflow has no normalized data' }
|
||||
}
|
||||
|
||||
const workflowState = {
|
||||
blocks: normalized.blocks || {},
|
||||
edges: normalized.edges || [],
|
||||
loops: normalized.loops || {},
|
||||
parallels: normalized.parallels || {},
|
||||
}
|
||||
const sanitized = sanitizeForCopilot(workflowState)
|
||||
const userWorkflow = JSON.stringify(sanitized, null, 2)
|
||||
|
||||
// Return workflow ID so copilot can use it for subsequent tool calls
|
||||
return {
|
||||
success: true,
|
||||
@@ -427,43 +452,20 @@ async function executeGetWorkflowFromName(
|
||||
return { success: false, error: 'workflow_name is required' }
|
||||
}
|
||||
|
||||
const workspaceIds = await db
|
||||
.select({ entityId: permissions.entityId })
|
||||
.from(permissions)
|
||||
.where(and(eq(permissions.userId, context.userId), eq(permissions.entityType, 'workspace')))
|
||||
const workflows = await getAccessibleWorkflowsForUser(context.userId)
|
||||
|
||||
const workspaceIdList = workspaceIds.map((row) => row.entityId)
|
||||
|
||||
const workflowConditions = [eq(workflow.userId, context.userId)]
|
||||
if (workspaceIdList.length > 0) {
|
||||
workflowConditions.push(inArray(workflow.workspaceId, workspaceIdList))
|
||||
}
|
||||
const workflows = await db
|
||||
.select()
|
||||
.from(workflow)
|
||||
.where(or(...workflowConditions))
|
||||
|
||||
const match = workflows.find(
|
||||
(w) => String(w.name || '').trim().toLowerCase() === workflowName.toLowerCase()
|
||||
)
|
||||
const targetName = normalizeWorkflowName(workflowName)
|
||||
const match = workflows.find((w) => normalizeWorkflowName(w.name) === targetName)
|
||||
if (!match) {
|
||||
return { success: false, error: `Workflow not found: ${workflowName}` }
|
||||
}
|
||||
|
||||
const normalized = await loadWorkflowFromNormalizedTables(match.id)
|
||||
if (!normalized) {
|
||||
const userWorkflow = formatNormalizedWorkflowForCopilot(normalized)
|
||||
if (!userWorkflow) {
|
||||
return { success: false, error: 'Workflow has no normalized data' }
|
||||
}
|
||||
|
||||
const workflowState = {
|
||||
blocks: normalized.blocks || {},
|
||||
edges: normalized.edges || [],
|
||||
loops: normalized.loops || {},
|
||||
parallels: normalized.parallels || {},
|
||||
}
|
||||
const sanitized = sanitizeForCopilot(workflowState)
|
||||
const userWorkflow = JSON.stringify(sanitized, null, 2)
|
||||
|
||||
// Return workflow ID and workspaceId so copilot can use them for subsequent tool calls
|
||||
return {
|
||||
success: true,
|
||||
@@ -487,33 +489,10 @@ async function executeListUserWorkflows(
|
||||
const workspaceId = params?.workspaceId as string | undefined
|
||||
const folderId = params?.folderId as string | undefined
|
||||
|
||||
const workspaceIds = await db
|
||||
.select({ entityId: permissions.entityId })
|
||||
.from(permissions)
|
||||
.where(and(eq(permissions.userId, context.userId), eq(permissions.entityType, 'workspace')))
|
||||
|
||||
const workspaceIdList = workspaceIds.map((row) => row.entityId)
|
||||
|
||||
const workflowConditions = [eq(workflow.userId, context.userId)]
|
||||
if (workspaceIdList.length > 0) {
|
||||
workflowConditions.push(inArray(workflow.workspaceId, workspaceIdList))
|
||||
}
|
||||
if (workspaceId) {
|
||||
workflowConditions.push(eq(workflow.workspaceId, workspaceId))
|
||||
}
|
||||
if (folderId) {
|
||||
workflowConditions.push(eq(workflow.folderId, folderId))
|
||||
}
|
||||
const workflows = await db
|
||||
.select()
|
||||
.from(workflow)
|
||||
.where(or(...workflowConditions))
|
||||
.orderBy(asc(workflow.sortOrder), asc(workflow.createdAt), asc(workflow.id))
|
||||
const workflows = await getAccessibleWorkflowsForUser(context.userId, { workspaceId, folderId })
|
||||
|
||||
// Return both names (for backward compatibility) and full workflow info with IDs
|
||||
const names = workflows
|
||||
.map((w) => (typeof w.name === 'string' ? w.name : null))
|
||||
.filter((n): n is string => Boolean(n))
|
||||
const names = extractWorkflowNames(workflows)
|
||||
|
||||
const workflowList = workflows.map((w) => ({
|
||||
workflowId: w.id,
|
||||
|
||||
@@ -5,7 +5,10 @@ import {
|
||||
type BaseClientToolMetadata,
|
||||
ClientToolCallState,
|
||||
} from '@/lib/copilot/tools/client/base-tool'
|
||||
import { sanitizeForCopilot } from '@/lib/workflows/sanitization/json-sanitizer'
|
||||
import {
|
||||
formatWorkflowStateForCopilot,
|
||||
normalizeWorkflowName,
|
||||
} from '@/lib/copilot/tools/shared/workflow-utils'
|
||||
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
|
||||
|
||||
const logger = createLogger('GetWorkflowFromNameClientTool')
|
||||
@@ -67,11 +70,9 @@ export class GetWorkflowFromNameClientTool extends BaseClientTool {
|
||||
|
||||
// Try to find by name from registry first to get ID
|
||||
const registry = useWorkflowRegistry.getState()
|
||||
const targetName = normalizeWorkflowName(workflowName)
|
||||
const match = Object.values((registry as any).workflows || {}).find(
|
||||
(w: any) =>
|
||||
String(w?.name || '')
|
||||
.trim()
|
||||
.toLowerCase() === workflowName.toLowerCase()
|
||||
(w: any) => normalizeWorkflowName(w?.name) === targetName
|
||||
) as any
|
||||
|
||||
if (!match?.id) {
|
||||
@@ -98,15 +99,12 @@ export class GetWorkflowFromNameClientTool extends BaseClientTool {
|
||||
}
|
||||
|
||||
// Convert state to the same string format as get_user_workflow
|
||||
const workflowState = {
|
||||
const userWorkflow = formatWorkflowStateForCopilot({
|
||||
blocks: wf.state.blocks || {},
|
||||
edges: wf.state.edges || [],
|
||||
loops: wf.state.loops || {},
|
||||
parallels: wf.state.parallels || {},
|
||||
}
|
||||
// Sanitize workflow state for copilot (remove UI-specific data)
|
||||
const sanitizedState = sanitizeForCopilot(workflowState)
|
||||
const userWorkflow = JSON.stringify(sanitizedState, null, 2)
|
||||
})
|
||||
|
||||
await this.markToolComplete(200, `Retrieved workflow ${workflowName}`, { userWorkflow })
|
||||
this.setState(ClientToolCallState.success)
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
type BaseClientToolMetadata,
|
||||
ClientToolCallState,
|
||||
} from '@/lib/copilot/tools/client/base-tool'
|
||||
import { extractWorkflowNames } from '@/lib/copilot/tools/shared/workflow-utils'
|
||||
|
||||
const logger = createLogger('ListUserWorkflowsClientTool')
|
||||
|
||||
@@ -41,9 +42,7 @@ export class ListUserWorkflowsClientTool extends BaseClientTool {
|
||||
|
||||
const json = await res.json()
|
||||
const workflows = Array.isArray(json?.data) ? json.data : []
|
||||
const names = workflows
|
||||
.map((w: any) => (typeof w?.name === 'string' ? w.name : null))
|
||||
.filter((n: string | null) => !!n)
|
||||
const names = extractWorkflowNames(workflows)
|
||||
|
||||
logger.info('Found workflows', { count: names.length })
|
||||
|
||||
|
||||
466
apps/sim/lib/copilot/tools/mcp/definitions.ts
Normal file
466
apps/sim/lib/copilot/tools/mcp/definitions.ts
Normal file
@@ -0,0 +1,466 @@
|
||||
export type DirectToolDef = {
|
||||
name: string
|
||||
description: string
|
||||
inputSchema: { type: 'object'; properties?: Record<string, unknown>; required?: string[] }
|
||||
toolId: string
|
||||
}
|
||||
|
||||
export type SubagentToolDef = {
|
||||
name: string
|
||||
description: string
|
||||
inputSchema: { type: 'object'; properties?: Record<string, unknown>; required?: string[] }
|
||||
agentId: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Direct tools that execute immediately without LLM orchestration.
|
||||
* These are fast database queries that don't need AI reasoning.
|
||||
*/
|
||||
export const DIRECT_TOOL_DEFS: DirectToolDef[] = [
|
||||
{
|
||||
name: 'list_workflows',
|
||||
toolId: 'list_user_workflows',
|
||||
description: 'List all workflows the user has access to. Returns workflow IDs, names, and workspace info.',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
workspaceId: {
|
||||
type: 'string',
|
||||
description: 'Optional workspace ID to filter workflows.',
|
||||
},
|
||||
folderId: {
|
||||
type: 'string',
|
||||
description: 'Optional folder ID to filter workflows.',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'list_workspaces',
|
||||
toolId: 'list_user_workspaces',
|
||||
description: 'List all workspaces the user has access to. Returns workspace IDs, names, and roles.',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'list_folders',
|
||||
toolId: 'list_folders',
|
||||
description: 'List all folders in a workspace.',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
workspaceId: {
|
||||
type: 'string',
|
||||
description: 'Workspace ID to list folders from.',
|
||||
},
|
||||
},
|
||||
required: ['workspaceId'],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'get_workflow',
|
||||
toolId: 'get_workflow_from_name',
|
||||
description: 'Get a workflow by name or ID. Returns the full workflow definition.',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
name: {
|
||||
type: 'string',
|
||||
description: 'Workflow name to search for.',
|
||||
},
|
||||
workflowId: {
|
||||
type: 'string',
|
||||
description: 'Workflow ID to retrieve directly.',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'create_workflow',
|
||||
toolId: 'create_workflow',
|
||||
description: 'Create a new workflow. Returns the new workflow ID.',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
name: {
|
||||
type: 'string',
|
||||
description: 'Name for the new workflow.',
|
||||
},
|
||||
workspaceId: {
|
||||
type: 'string',
|
||||
description: 'Optional workspace ID. Uses default workspace if not provided.',
|
||||
},
|
||||
folderId: {
|
||||
type: 'string',
|
||||
description: 'Optional folder ID to place the workflow in.',
|
||||
},
|
||||
description: {
|
||||
type: 'string',
|
||||
description: 'Optional description for the workflow.',
|
||||
},
|
||||
},
|
||||
required: ['name'],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'create_folder',
|
||||
toolId: 'create_folder',
|
||||
description: 'Create a new folder in a workspace.',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
name: {
|
||||
type: 'string',
|
||||
description: 'Name for the new folder.',
|
||||
},
|
||||
workspaceId: {
|
||||
type: 'string',
|
||||
description: 'Optional workspace ID. Uses default workspace if not provided.',
|
||||
},
|
||||
parentId: {
|
||||
type: 'string',
|
||||
description: 'Optional parent folder ID for nested folders.',
|
||||
},
|
||||
},
|
||||
required: ['name'],
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
export const SUBAGENT_TOOL_DEFS: SubagentToolDef[] = [
|
||||
{
|
||||
name: 'copilot_build',
|
||||
agentId: 'build',
|
||||
description: `Build a workflow end-to-end in a single step. This is the fast mode equivalent for headless/MCP usage.
|
||||
|
||||
USE THIS WHEN:
|
||||
- Building a new workflow from scratch
|
||||
- Modifying an existing workflow
|
||||
- You want to gather information and build in one pass without separate plan→edit steps
|
||||
|
||||
WORKFLOW ID (REQUIRED):
|
||||
- For NEW workflows: First call create_workflow to get a workflowId, then pass it here
|
||||
- For EXISTING workflows: Always pass the workflowId parameter
|
||||
|
||||
CAN DO:
|
||||
- Gather information about blocks, credentials, patterns
|
||||
- Search documentation and patterns for best practices
|
||||
- Add, modify, or remove blocks
|
||||
- Configure block settings and connections
|
||||
- Set environment variables and workflow variables
|
||||
|
||||
CANNOT DO:
|
||||
- Run or test workflows (use copilot_test separately after deploying)
|
||||
- Deploy workflows (use copilot_deploy separately)
|
||||
|
||||
WORKFLOW:
|
||||
1. Call create_workflow to get a workflowId (for new workflows)
|
||||
2. Call copilot_build with the request and workflowId
|
||||
3. Build agent gathers info and builds in one pass
|
||||
4. Call copilot_deploy to deploy the workflow
|
||||
5. Optionally call copilot_test to verify it works`,
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
request: {
|
||||
type: 'string',
|
||||
description: 'What you want to build or modify in the workflow.',
|
||||
},
|
||||
workflowId: {
|
||||
type: 'string',
|
||||
description:
|
||||
'REQUIRED. The workflow ID. For new workflows, call create_workflow first to get this.',
|
||||
},
|
||||
context: { type: 'object' },
|
||||
},
|
||||
required: ['request', 'workflowId'],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'copilot_discovery',
|
||||
agentId: 'discovery',
|
||||
description: `Find workflows by their contents or functionality when the user doesn't know the exact name or ID.
|
||||
|
||||
USE THIS WHEN:
|
||||
- User describes a workflow by what it does: "the one that sends emails", "my Slack notification workflow"
|
||||
- User refers to workflow contents: "the workflow with the OpenAI block"
|
||||
- User needs to search/match workflows by functionality or description
|
||||
|
||||
DO NOT USE (use direct tools instead):
|
||||
- User knows the workflow name → use get_workflow
|
||||
- User wants to list all workflows → use list_workflows
|
||||
- User wants to list workspaces → use list_workspaces
|
||||
- User wants to list folders → use list_folders`,
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
request: { type: 'string' },
|
||||
workspaceId: { type: 'string' },
|
||||
context: { type: 'object' },
|
||||
},
|
||||
required: ['request'],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'copilot_plan',
|
||||
agentId: 'plan',
|
||||
description: `Plan workflow changes by gathering required information.
|
||||
|
||||
USE THIS WHEN:
|
||||
- Building a new workflow
|
||||
- Modifying an existing workflow
|
||||
- You need to understand what blocks and integrations are available
|
||||
- The workflow requires multiple blocks or connections
|
||||
|
||||
WORKFLOW ID (REQUIRED):
|
||||
- For NEW workflows: First call create_workflow to get a workflowId, then pass it here
|
||||
- For EXISTING workflows: Always pass the workflowId parameter
|
||||
|
||||
This tool gathers information about available blocks, credentials, and the current workflow state.
|
||||
|
||||
RETURNS: A plan object containing block configurations, connections, and technical details.
|
||||
IMPORTANT: Pass the returned plan EXACTLY to copilot_edit - do not modify or summarize it.`,
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
request: { type: 'string', description: 'What you want to build or modify in the workflow.' },
|
||||
workflowId: {
|
||||
type: 'string',
|
||||
description: 'REQUIRED. The workflow ID. For new workflows, call create_workflow first to get this.',
|
||||
},
|
||||
context: { type: 'object' },
|
||||
},
|
||||
required: ['request', 'workflowId'],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'copilot_edit',
|
||||
agentId: 'edit',
|
||||
description: `Execute a workflow plan and apply edits.
|
||||
|
||||
USE THIS WHEN:
|
||||
- You have a plan from copilot_plan that needs to be executed
|
||||
- Building or modifying a workflow based on the plan
|
||||
- Making changes to blocks, connections, or configurations
|
||||
|
||||
WORKFLOW ID (REQUIRED):
|
||||
- You MUST provide the workflowId parameter
|
||||
- For new workflows, get the workflowId from create_workflow first
|
||||
|
||||
PLAN (REQUIRED):
|
||||
- Pass the EXACT plan object from copilot_plan in the context.plan field
|
||||
- Do NOT modify, summarize, or interpret the plan - pass it verbatim
|
||||
- The plan contains technical details the edit agent needs exactly as-is
|
||||
|
||||
IMPORTANT: After copilot_edit completes, you MUST call copilot_deploy before the workflow can be run or tested.`,
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
message: { type: 'string', description: 'Optional additional instructions for the edit.' },
|
||||
workflowId: {
|
||||
type: 'string',
|
||||
description: 'REQUIRED. The workflow ID to edit. Get this from create_workflow for new workflows.',
|
||||
},
|
||||
plan: {
|
||||
type: 'object',
|
||||
description: 'The plan object from copilot_plan. Pass it EXACTLY as returned, do not modify.',
|
||||
},
|
||||
context: {
|
||||
type: 'object',
|
||||
description: 'Additional context. Put the plan in context.plan if not using the plan field directly.',
|
||||
},
|
||||
},
|
||||
required: ['workflowId'],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'copilot_debug',
|
||||
agentId: 'debug',
|
||||
description: `Diagnose errors or unexpected workflow behavior.
|
||||
|
||||
WORKFLOW ID (REQUIRED): Always provide the workflowId of the workflow to debug.`,
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
error: { type: 'string', description: 'The error message or description of the issue.' },
|
||||
workflowId: { type: 'string', description: 'REQUIRED. The workflow ID to debug.' },
|
||||
context: { type: 'object' },
|
||||
},
|
||||
required: ['error', 'workflowId'],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'copilot_deploy',
|
||||
agentId: 'deploy',
|
||||
description: `Deploy or manage workflow deployments.
|
||||
|
||||
CRITICAL: You MUST deploy a workflow after building before it can be run or tested.
|
||||
Workflows without an active deployment will fail with "no active deployment" error.
|
||||
|
||||
WORKFLOW ID (REQUIRED):
|
||||
- Always provide the workflowId parameter
|
||||
- This must match the workflow you built with copilot_edit
|
||||
|
||||
USE THIS:
|
||||
- After copilot_edit completes to activate the workflow
|
||||
- To update deployment settings
|
||||
- To redeploy after making changes
|
||||
|
||||
DEPLOYMENT TYPES:
|
||||
- "deploy as api" - REST API endpoint
|
||||
- "deploy as chat" - Chat interface
|
||||
- "deploy as mcp" - MCP server`,
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
request: {
|
||||
type: 'string',
|
||||
description: 'The deployment request, e.g. "deploy as api" or "deploy as chat"',
|
||||
},
|
||||
workflowId: {
|
||||
type: 'string',
|
||||
description: 'REQUIRED. The workflow ID to deploy.',
|
||||
},
|
||||
context: { type: 'object' },
|
||||
},
|
||||
required: ['request', 'workflowId'],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'copilot_auth',
|
||||
agentId: 'auth',
|
||||
description: 'Handle OAuth connection flows.',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
request: { type: 'string' },
|
||||
context: { type: 'object' },
|
||||
},
|
||||
required: ['request'],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'copilot_knowledge',
|
||||
agentId: 'knowledge',
|
||||
description: 'Create and manage knowledge bases.',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
request: { type: 'string' },
|
||||
context: { type: 'object' },
|
||||
},
|
||||
required: ['request'],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'copilot_custom_tool',
|
||||
agentId: 'custom_tool',
|
||||
description: 'Create or manage custom tools.',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
request: { type: 'string' },
|
||||
context: { type: 'object' },
|
||||
},
|
||||
required: ['request'],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'copilot_info',
|
||||
agentId: 'info',
|
||||
description: 'Inspect blocks, outputs, and workflow metadata.',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
request: { type: 'string' },
|
||||
workflowId: { type: 'string' },
|
||||
context: { type: 'object' },
|
||||
},
|
||||
required: ['request'],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'copilot_workflow',
|
||||
agentId: 'workflow',
|
||||
description: 'Manage workflow environment and configuration.',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
request: { type: 'string' },
|
||||
workflowId: { type: 'string' },
|
||||
context: { type: 'object' },
|
||||
},
|
||||
required: ['request'],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'copilot_research',
|
||||
agentId: 'research',
|
||||
description: 'Research external APIs and documentation.',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
request: { type: 'string' },
|
||||
context: { type: 'object' },
|
||||
},
|
||||
required: ['request'],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'copilot_tour',
|
||||
agentId: 'tour',
|
||||
description: 'Explain platform features and usage.',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
request: { type: 'string' },
|
||||
context: { type: 'object' },
|
||||
},
|
||||
required: ['request'],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'copilot_test',
|
||||
agentId: 'test',
|
||||
description: `Run workflows and verify outputs.
|
||||
|
||||
PREREQUISITE: The workflow MUST be deployed first using copilot_deploy.
|
||||
Undeployed workflows will fail with "no active deployment" error.
|
||||
|
||||
WORKFLOW ID (REQUIRED):
|
||||
- Always provide the workflowId parameter
|
||||
|
||||
USE THIS:
|
||||
- After deploying to verify the workflow works correctly
|
||||
- To test with sample inputs
|
||||
- To validate workflow behavior before sharing with user`,
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
request: { type: 'string' },
|
||||
workflowId: {
|
||||
type: 'string',
|
||||
description: 'REQUIRED. The workflow ID to test.',
|
||||
},
|
||||
context: { type: 'object' },
|
||||
},
|
||||
required: ['request', 'workflowId'],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'copilot_superagent',
|
||||
agentId: 'superagent',
|
||||
description: 'Execute direct external actions (email, Slack, etc.).',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
request: { type: 'string' },
|
||||
context: { type: 'object' },
|
||||
},
|
||||
required: ['request'],
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
37
apps/sim/lib/copilot/tools/shared/workflow-utils.ts
Normal file
37
apps/sim/lib/copilot/tools/shared/workflow-utils.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import { sanitizeForCopilot } from '@/lib/workflows/sanitization/json-sanitizer'
|
||||
|
||||
type CopilotWorkflowState = {
|
||||
blocks?: Record<string, any>
|
||||
edges?: any[]
|
||||
loops?: Record<string, any>
|
||||
parallels?: Record<string, any>
|
||||
}
|
||||
|
||||
export function formatWorkflowStateForCopilot(state: CopilotWorkflowState): string {
|
||||
const workflowState = {
|
||||
blocks: state.blocks || {},
|
||||
edges: state.edges || [],
|
||||
loops: state.loops || {},
|
||||
parallels: state.parallels || {},
|
||||
}
|
||||
const sanitized = sanitizeForCopilot(workflowState)
|
||||
return JSON.stringify(sanitized, null, 2)
|
||||
}
|
||||
|
||||
export function formatNormalizedWorkflowForCopilot(
|
||||
normalized: CopilotWorkflowState | null | undefined
|
||||
): string | null {
|
||||
if (!normalized) return null
|
||||
return formatWorkflowStateForCopilot(normalized)
|
||||
}
|
||||
|
||||
export function normalizeWorkflowName(name?: string | null): string {
|
||||
return String(name || '').trim().toLowerCase()
|
||||
}
|
||||
|
||||
export function extractWorkflowNames(workflows: Array<{ name?: string | null }>): string[] {
|
||||
return workflows
|
||||
.map((workflow) => (typeof workflow?.name === 'string' ? workflow.name : null))
|
||||
.filter((name): name is string => Boolean(name))
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { db } from '@sim/db'
|
||||
import { permissions, userStats, workflow as workflowTable } from '@sim/db/schema'
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { and, eq } from 'drizzle-orm'
|
||||
import { and, asc, eq, inArray, or } from 'drizzle-orm'
|
||||
import { NextResponse } from 'next/server'
|
||||
import { getSession } from '@/lib/auth'
|
||||
import { getWorkspaceWithOwner, type PermissionType } from '@/lib/workspaces/permissions/utils'
|
||||
@@ -15,6 +15,50 @@ export async function getWorkflowById(id: string) {
|
||||
return rows[0]
|
||||
}
|
||||
|
||||
export async function resolveWorkflowIdForUser(
|
||||
userId: string,
|
||||
workflowId?: string,
|
||||
workflowName?: string
|
||||
): Promise<{ workflowId: string; workflowName?: string } | null> {
|
||||
if (workflowId) {
|
||||
return { workflowId }
|
||||
}
|
||||
|
||||
const workspaceIds = await db
|
||||
.select({ entityId: permissions.entityId })
|
||||
.from(permissions)
|
||||
.where(and(eq(permissions.userId, userId), eq(permissions.entityType, 'workspace')))
|
||||
|
||||
const workspaceIdList = workspaceIds.map((row) => row.entityId)
|
||||
|
||||
const workflowConditions = [eq(workflowTable.userId, userId)]
|
||||
if (workspaceIdList.length > 0) {
|
||||
workflowConditions.push(inArray(workflowTable.workspaceId, workspaceIdList))
|
||||
}
|
||||
|
||||
const workflows = await db
|
||||
.select()
|
||||
.from(workflowTable)
|
||||
.where(or(...workflowConditions))
|
||||
.orderBy(asc(workflowTable.sortOrder), asc(workflowTable.createdAt), asc(workflowTable.id))
|
||||
|
||||
if (workflows.length === 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
if (workflowName) {
|
||||
const match = workflows.find(
|
||||
(w) => String(w.name || '').trim().toLowerCase() === workflowName.toLowerCase()
|
||||
)
|
||||
if (match) {
|
||||
return { workflowId: match.id, workflowName: match.name || undefined }
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
return { workflowId: workflows[0].id, workflowName: workflows[0].name || undefined }
|
||||
}
|
||||
|
||||
type WorkflowRecord = ReturnType<typeof getWorkflowById> extends Promise<infer R>
|
||||
? NonNullable<R>
|
||||
: never
|
||||
|
||||
@@ -53,8 +53,7 @@ import { SleepClientTool } from '@/lib/copilot/tools/client/other/sleep'
|
||||
import { TestClientTool } from '@/lib/copilot/tools/client/other/test'
|
||||
import { TourClientTool } from '@/lib/copilot/tools/client/other/tour'
|
||||
import { WorkflowClientTool } from '@/lib/copilot/tools/client/other/workflow'
|
||||
import { createExecutionContext, getTool } from '@/lib/copilot/tools/client/registry'
|
||||
import { COPILOT_SERVER_ORCHESTRATED } from '@/lib/copilot/orchestrator/config'
|
||||
import { getTool } from '@/lib/copilot/tools/client/registry'
|
||||
import { GetCredentialsClientTool } from '@/lib/copilot/tools/client/user/get-credentials'
|
||||
import { SetEnvironmentVariablesClientTool } from '@/lib/copilot/tools/client/user/set-environment-variables'
|
||||
import { CheckDeploymentStatusClientTool } from '@/lib/copilot/tools/client/workflow/check-deployment-status'
|
||||
@@ -1200,7 +1199,7 @@ const sseHandlers: Record<string, SSEHandler> = {
|
||||
} catch {}
|
||||
}
|
||||
|
||||
if (COPILOT_SERVER_ORCHESTRATED && current.name === 'edit_workflow') {
|
||||
if (current.name === 'edit_workflow') {
|
||||
try {
|
||||
const resultPayload =
|
||||
data?.result || data?.data?.result || data?.data?.data || data?.data || {}
|
||||
@@ -1375,229 +1374,7 @@ const sseHandlers: Record<string, SSEHandler> = {
|
||||
return
|
||||
}
|
||||
|
||||
if (COPILOT_SERVER_ORCHESTRATED) {
|
||||
return
|
||||
}
|
||||
|
||||
// Prefer interface-based registry to determine interrupt and execute
|
||||
try {
|
||||
const def = name ? getTool(name) : undefined
|
||||
if (def) {
|
||||
const hasInterrupt =
|
||||
typeof def.hasInterrupt === 'function'
|
||||
? !!def.hasInterrupt(args || {})
|
||||
: !!def.hasInterrupt
|
||||
// Check if tool is auto-allowed - if so, execute even if it has an interrupt
|
||||
const { autoAllowedTools } = get()
|
||||
const isAutoAllowed = name ? autoAllowedTools.includes(name) : false
|
||||
if ((!hasInterrupt || isAutoAllowed) && typeof def.execute === 'function') {
|
||||
if (isAutoAllowed && hasInterrupt) {
|
||||
logger.info('[toolCallsById] Auto-executing tool with interrupt (auto-allowed)', {
|
||||
id,
|
||||
name,
|
||||
})
|
||||
}
|
||||
const ctx = createExecutionContext({ toolCallId: id, toolName: name || 'unknown_tool' })
|
||||
// Defer executing transition by a tick to let pending render
|
||||
setTimeout(() => {
|
||||
// Guard against duplicate execution - check if already executing or terminal
|
||||
const currentState = get().toolCallsById[id]?.state
|
||||
if (currentState === ClientToolCallState.executing || isTerminalState(currentState)) {
|
||||
return
|
||||
}
|
||||
|
||||
const executingMap = { ...get().toolCallsById }
|
||||
executingMap[id] = {
|
||||
...executingMap[id],
|
||||
state: ClientToolCallState.executing,
|
||||
display: resolveToolDisplay(name, ClientToolCallState.executing, id, args),
|
||||
}
|
||||
set({ toolCallsById: executingMap })
|
||||
logger.info('[toolCallsById] pending → executing (registry)', { id, name })
|
||||
|
||||
// Update inline content block to executing
|
||||
for (let i = 0; i < context.contentBlocks.length; i++) {
|
||||
const b = context.contentBlocks[i] as any
|
||||
if (b.type === 'tool_call' && b.toolCall?.id === id) {
|
||||
context.contentBlocks[i] = {
|
||||
...b,
|
||||
toolCall: { ...b.toolCall, state: ClientToolCallState.executing },
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
updateStreamingMessage(set, context)
|
||||
|
||||
Promise.resolve()
|
||||
.then(async () => {
|
||||
const result = await def.execute(ctx, args || {})
|
||||
const success =
|
||||
result && typeof result.status === 'number'
|
||||
? result.status >= 200 && result.status < 300
|
||||
: true
|
||||
const completeMap = { ...get().toolCallsById }
|
||||
// Do not override terminal review/rejected
|
||||
if (
|
||||
isRejectedState(completeMap[id]?.state) ||
|
||||
isReviewState(completeMap[id]?.state) ||
|
||||
isBackgroundState(completeMap[id]?.state)
|
||||
) {
|
||||
return
|
||||
}
|
||||
completeMap[id] = {
|
||||
...completeMap[id],
|
||||
state: success ? ClientToolCallState.success : ClientToolCallState.error,
|
||||
display: resolveToolDisplay(
|
||||
name,
|
||||
success ? ClientToolCallState.success : ClientToolCallState.error,
|
||||
id,
|
||||
args
|
||||
),
|
||||
}
|
||||
set({ toolCallsById: completeMap })
|
||||
logger.info(
|
||||
`[toolCallsById] executing → ${success ? 'success' : 'error'} (registry)`,
|
||||
{ id, name }
|
||||
)
|
||||
|
||||
// Notify backend tool mark-complete endpoint
|
||||
try {
|
||||
await fetch('/api/copilot/tools/mark-complete', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
id,
|
||||
name: name || 'unknown_tool',
|
||||
status:
|
||||
typeof result?.status === 'number' ? result.status : success ? 200 : 500,
|
||||
message: result?.message,
|
||||
data: result?.data,
|
||||
}),
|
||||
})
|
||||
} catch {}
|
||||
})
|
||||
.catch((e) => {
|
||||
const errorMap = { ...get().toolCallsById }
|
||||
// Do not override terminal review/rejected
|
||||
if (
|
||||
isRejectedState(errorMap[id]?.state) ||
|
||||
isReviewState(errorMap[id]?.state) ||
|
||||
isBackgroundState(errorMap[id]?.state)
|
||||
) {
|
||||
return
|
||||
}
|
||||
errorMap[id] = {
|
||||
...errorMap[id],
|
||||
state: ClientToolCallState.error,
|
||||
display: resolveToolDisplay(name, ClientToolCallState.error, id, args),
|
||||
}
|
||||
set({ toolCallsById: errorMap })
|
||||
logger.error('Registry auto-execute tool failed', { id, name, error: e })
|
||||
})
|
||||
}, 0)
|
||||
return
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
logger.warn('tool_call registry auto-exec check failed', { id, name, error: e })
|
||||
}
|
||||
|
||||
// Class-based auto-exec for non-interrupt tools or auto-allowed tools
|
||||
try {
|
||||
const inst = getClientTool(id) as any
|
||||
const hasInterrupt = !!inst?.getInterruptDisplays?.()
|
||||
// Check if tool is auto-allowed - if so, execute even if it has an interrupt
|
||||
const { autoAllowedTools: classAutoAllowed } = get()
|
||||
const isClassAutoAllowed = name ? classAutoAllowed.includes(name) : false
|
||||
if (
|
||||
(!hasInterrupt || isClassAutoAllowed) &&
|
||||
(typeof inst?.execute === 'function' || typeof inst?.handleAccept === 'function')
|
||||
) {
|
||||
if (isClassAutoAllowed && hasInterrupt) {
|
||||
logger.info('[toolCallsById] Auto-executing class tool with interrupt (auto-allowed)', {
|
||||
id,
|
||||
name,
|
||||
})
|
||||
}
|
||||
setTimeout(() => {
|
||||
// Guard against duplicate execution - check if already executing or terminal
|
||||
const currentState = get().toolCallsById[id]?.state
|
||||
if (currentState === ClientToolCallState.executing || isTerminalState(currentState)) {
|
||||
return
|
||||
}
|
||||
|
||||
const executingMap = { ...get().toolCallsById }
|
||||
executingMap[id] = {
|
||||
...executingMap[id],
|
||||
state: ClientToolCallState.executing,
|
||||
display: resolveToolDisplay(name, ClientToolCallState.executing, id, args),
|
||||
}
|
||||
set({ toolCallsById: executingMap })
|
||||
logger.info('[toolCallsById] pending → executing (class)', { id, name })
|
||||
|
||||
Promise.resolve()
|
||||
.then(async () => {
|
||||
// Use handleAccept for tools with interrupts, execute for others
|
||||
if (hasInterrupt && typeof inst?.handleAccept === 'function') {
|
||||
await inst.handleAccept(args || {})
|
||||
} else {
|
||||
await inst.execute(args || {})
|
||||
}
|
||||
// Success/error will be synced via registerToolStateSync
|
||||
})
|
||||
.catch(() => {
|
||||
const errorMap = { ...get().toolCallsById }
|
||||
// Do not override terminal review/rejected
|
||||
if (
|
||||
isRejectedState(errorMap[id]?.state) ||
|
||||
isReviewState(errorMap[id]?.state) ||
|
||||
isBackgroundState(errorMap[id]?.state)
|
||||
) {
|
||||
return
|
||||
}
|
||||
errorMap[id] = {
|
||||
...errorMap[id],
|
||||
state: ClientToolCallState.error,
|
||||
display: resolveToolDisplay(name, ClientToolCallState.error, id, args),
|
||||
}
|
||||
set({ toolCallsById: errorMap })
|
||||
})
|
||||
}, 0)
|
||||
return
|
||||
}
|
||||
} catch {}
|
||||
|
||||
// Integration tools: Check auto-allowed or stay in pending state until user confirms
|
||||
// This handles tools like google_calendar_*, exa_*, gmail_read, etc. that aren't in the client registry
|
||||
// Only relevant if mode is 'build' (agent)
|
||||
const { mode, workflowId, autoAllowedTools, executeIntegrationTool } = get()
|
||||
if (mode === 'build' && workflowId) {
|
||||
// Check if tool was NOT found in client registry
|
||||
const def = name ? getTool(name) : undefined
|
||||
const inst = getClientTool(id) as any
|
||||
if (!def && !inst && name) {
|
||||
// Check if this integration tool is auto-allowed - if so, execute it immediately
|
||||
if (autoAllowedTools.includes(name)) {
|
||||
logger.info('[build mode] Auto-executing integration tool (auto-allowed)', { id, name })
|
||||
// Defer to allow pending state to render briefly
|
||||
setTimeout(() => {
|
||||
executeIntegrationTool(id).catch((err) => {
|
||||
logger.error('[build mode] Auto-execute integration tool failed', {
|
||||
id,
|
||||
name,
|
||||
error: err,
|
||||
})
|
||||
})
|
||||
}, 0)
|
||||
} else {
|
||||
// Integration tools stay in pending state until user confirms
|
||||
logger.info('[build mode] Integration tool awaiting user confirmation', {
|
||||
id,
|
||||
name,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
return
|
||||
},
|
||||
reasoning: (data, context, _get, set) => {
|
||||
const phase = (data && (data.phase || data?.data?.phase)) as string | undefined
|
||||
@@ -2082,91 +1859,6 @@ const subAgentSSEHandlers: Record<string, SSEHandler> = {
|
||||
if (isPartial) {
|
||||
return
|
||||
}
|
||||
|
||||
// Execute client tools in parallel (non-blocking) - same pattern as main tool_call handler
|
||||
// Check if tool is auto-allowed
|
||||
const { autoAllowedTools: subAgentAutoAllowed } = get()
|
||||
const isSubAgentAutoAllowed = name ? subAgentAutoAllowed.includes(name) : false
|
||||
|
||||
try {
|
||||
const def = getTool(name)
|
||||
if (def) {
|
||||
const hasInterrupt =
|
||||
typeof def.hasInterrupt === 'function'
|
||||
? !!def.hasInterrupt(args || {})
|
||||
: !!def.hasInterrupt
|
||||
// Auto-execute if no interrupt OR if auto-allowed
|
||||
if (!hasInterrupt || isSubAgentAutoAllowed) {
|
||||
if (isSubAgentAutoAllowed && hasInterrupt) {
|
||||
logger.info('[SubAgent] Auto-executing tool with interrupt (auto-allowed)', {
|
||||
id,
|
||||
name,
|
||||
})
|
||||
}
|
||||
// Auto-execute tools - non-blocking
|
||||
const ctx = createExecutionContext({ toolCallId: id, toolName: name })
|
||||
Promise.resolve()
|
||||
.then(() => def.execute(ctx, args || {}))
|
||||
.catch((execErr: any) => {
|
||||
logger.error('[SubAgent] Tool execution failed', {
|
||||
id,
|
||||
name,
|
||||
error: execErr?.message,
|
||||
})
|
||||
})
|
||||
}
|
||||
} else {
|
||||
// Fallback to class-based tools - non-blocking
|
||||
const instance = getClientTool(id)
|
||||
if (instance) {
|
||||
const hasInterruptDisplays = !!instance.getInterruptDisplays?.()
|
||||
// Auto-execute if no interrupt OR if auto-allowed
|
||||
if (!hasInterruptDisplays || isSubAgentAutoAllowed) {
|
||||
if (isSubAgentAutoAllowed && hasInterruptDisplays) {
|
||||
logger.info('[SubAgent] Auto-executing class tool with interrupt (auto-allowed)', {
|
||||
id,
|
||||
name,
|
||||
})
|
||||
}
|
||||
Promise.resolve()
|
||||
.then(() => {
|
||||
// Use handleAccept for tools with interrupts, execute for others
|
||||
if (hasInterruptDisplays && typeof instance.handleAccept === 'function') {
|
||||
return instance.handleAccept(args || {})
|
||||
}
|
||||
return instance.execute(args || {})
|
||||
})
|
||||
.catch((execErr: any) => {
|
||||
logger.error('[SubAgent] Class tool execution failed', {
|
||||
id,
|
||||
name,
|
||||
error: execErr?.message,
|
||||
})
|
||||
})
|
||||
}
|
||||
} else {
|
||||
// Check if this is an integration tool (server-side) that should be auto-executed
|
||||
const isIntegrationTool = !CLASS_TOOL_METADATA[name]
|
||||
if (isIntegrationTool && isSubAgentAutoAllowed) {
|
||||
logger.info('[SubAgent] Auto-executing integration tool (auto-allowed)', {
|
||||
id,
|
||||
name,
|
||||
})
|
||||
// Execute integration tool via the store method
|
||||
const { executeIntegrationTool } = get()
|
||||
executeIntegrationTool(id).catch((err) => {
|
||||
logger.error('[SubAgent] Integration tool auto-execution failed', {
|
||||
id,
|
||||
name,
|
||||
error: err?.message || err,
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e: any) {
|
||||
logger.error('[SubAgent] Tool registry/execution error', { id, name, error: e?.message })
|
||||
}
|
||||
},
|
||||
|
||||
// Handle subagent tool results
|
||||
@@ -3208,21 +2900,13 @@ export const useCopilotStore = create<CopilotStore>()(
|
||||
return { messages }
|
||||
})
|
||||
|
||||
// Notify backend mark-complete to finalize tool server-side
|
||||
try {
|
||||
fetch('/api/copilot/tools/mark-complete', {
|
||||
fetch('/api/copilot/confirm', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
id,
|
||||
name: current.name,
|
||||
status:
|
||||
targetState === ClientToolCallState.success
|
||||
? 200
|
||||
: targetState === ClientToolCallState.rejected
|
||||
? 409
|
||||
: 500,
|
||||
message: toolCallState,
|
||||
toolCallId: id,
|
||||
status: toolCallState,
|
||||
}),
|
||||
}).catch(() => {})
|
||||
} catch {}
|
||||
@@ -3836,150 +3520,6 @@ export const useCopilotStore = create<CopilotStore>()(
|
||||
setAgentPrefetch: (prefetch) => set({ agentPrefetch: prefetch }),
|
||||
setEnabledModels: (models) => set({ enabledModels: models }),
|
||||
|
||||
executeIntegrationTool: async (toolCallId: string) => {
|
||||
if (COPILOT_SERVER_ORCHESTRATED) {
|
||||
return
|
||||
}
|
||||
const { toolCallsById, workflowId } = get()
|
||||
const toolCall = toolCallsById[toolCallId]
|
||||
if (!toolCall || !workflowId) return
|
||||
|
||||
const { id, name, params } = toolCall
|
||||
|
||||
// Guard against double execution - skip if already executing or in terminal state
|
||||
if (toolCall.state === ClientToolCallState.executing || isTerminalState(toolCall.state)) {
|
||||
logger.info('[executeIntegrationTool] Skipping - already executing or terminal', {
|
||||
id,
|
||||
name,
|
||||
state: toolCall.state,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// Set to executing state
|
||||
const executingMap = { ...get().toolCallsById }
|
||||
executingMap[id] = {
|
||||
...executingMap[id],
|
||||
state: ClientToolCallState.executing,
|
||||
display: resolveToolDisplay(name, ClientToolCallState.executing, id, params),
|
||||
}
|
||||
set({ toolCallsById: executingMap })
|
||||
logger.info('[toolCallsById] pending → executing (integration tool)', { id, name })
|
||||
|
||||
try {
|
||||
const res = await fetch('/api/copilot/execute-tool', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
toolCallId: id,
|
||||
toolName: name,
|
||||
arguments: params || {},
|
||||
workflowId,
|
||||
}),
|
||||
})
|
||||
|
||||
const result = await res.json()
|
||||
const success = result.success && result.result?.success
|
||||
const completeMap = { ...get().toolCallsById }
|
||||
|
||||
// Do not override terminal review/rejected
|
||||
if (
|
||||
isRejectedState(completeMap[id]?.state) ||
|
||||
isReviewState(completeMap[id]?.state) ||
|
||||
isBackgroundState(completeMap[id]?.state)
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
completeMap[id] = {
|
||||
...completeMap[id],
|
||||
state: success ? ClientToolCallState.success : ClientToolCallState.error,
|
||||
display: resolveToolDisplay(
|
||||
name,
|
||||
success ? ClientToolCallState.success : ClientToolCallState.error,
|
||||
id,
|
||||
params
|
||||
),
|
||||
}
|
||||
set({ toolCallsById: completeMap })
|
||||
logger.info(`[toolCallsById] executing → ${success ? 'success' : 'error'} (integration)`, {
|
||||
id,
|
||||
name,
|
||||
result,
|
||||
})
|
||||
|
||||
// Notify backend tool mark-complete endpoint
|
||||
try {
|
||||
await fetch('/api/copilot/tools/mark-complete', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
id,
|
||||
name: name || 'unknown_tool',
|
||||
status: success ? 200 : 500,
|
||||
message: success
|
||||
? result.result?.output?.content
|
||||
: result.result?.error || result.error || 'Tool execution failed',
|
||||
data: success
|
||||
? result.result?.output
|
||||
: {
|
||||
error: result.result?.error || result.error,
|
||||
output: result.result?.output,
|
||||
},
|
||||
}),
|
||||
})
|
||||
} catch {}
|
||||
} catch (e) {
|
||||
const errorMap = { ...get().toolCallsById }
|
||||
// Do not override terminal review/rejected
|
||||
if (
|
||||
isRejectedState(errorMap[id]?.state) ||
|
||||
isReviewState(errorMap[id]?.state) ||
|
||||
isBackgroundState(errorMap[id]?.state)
|
||||
) {
|
||||
return
|
||||
}
|
||||
errorMap[id] = {
|
||||
...errorMap[id],
|
||||
state: ClientToolCallState.error,
|
||||
display: resolveToolDisplay(name, ClientToolCallState.error, id, params),
|
||||
}
|
||||
set({ toolCallsById: errorMap })
|
||||
logger.error('Integration tool execution failed', { id, name, error: e })
|
||||
}
|
||||
},
|
||||
|
||||
skipIntegrationTool: (toolCallId: string) => {
|
||||
const { toolCallsById } = get()
|
||||
const toolCall = toolCallsById[toolCallId]
|
||||
if (!toolCall) return
|
||||
|
||||
const { id, name, params } = toolCall
|
||||
|
||||
// Set to rejected state
|
||||
const rejectedMap = { ...get().toolCallsById }
|
||||
rejectedMap[id] = {
|
||||
...rejectedMap[id],
|
||||
state: ClientToolCallState.rejected,
|
||||
display: resolveToolDisplay(name, ClientToolCallState.rejected, id, params),
|
||||
}
|
||||
set({ toolCallsById: rejectedMap })
|
||||
logger.info('[toolCallsById] pending → rejected (integration tool skipped)', { id, name })
|
||||
|
||||
// Notify backend tool mark-complete endpoint with skip status
|
||||
fetch('/api/copilot/tools/mark-complete', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
id,
|
||||
name: name || 'unknown_tool',
|
||||
status: 200,
|
||||
message: 'Tool execution skipped by user',
|
||||
data: { skipped: true },
|
||||
}),
|
||||
}).catch(() => {})
|
||||
},
|
||||
|
||||
loadAutoAllowedTools: async () => {
|
||||
try {
|
||||
logger.info('[AutoAllowedTools] Loading from API...')
|
||||
@@ -4013,45 +3553,6 @@ export const useCopilotStore = create<CopilotStore>()(
|
||||
set({ autoAllowedTools: data.autoAllowedTools || [] })
|
||||
logger.info('[AutoAllowedTools] Added tool to store', { toolId })
|
||||
|
||||
// Auto-execute all pending tools of the same type
|
||||
const { toolCallsById, executeIntegrationTool } = get()
|
||||
const pendingToolCalls = Object.values(toolCallsById).filter(
|
||||
(tc) => tc.name === toolId && tc.state === ClientToolCallState.pending
|
||||
)
|
||||
if (pendingToolCalls.length > 0) {
|
||||
const isIntegrationTool = !CLASS_TOOL_METADATA[toolId]
|
||||
logger.info('[AutoAllowedTools] Auto-executing pending tools', {
|
||||
toolId,
|
||||
count: pendingToolCalls.length,
|
||||
isIntegrationTool,
|
||||
})
|
||||
for (const tc of pendingToolCalls) {
|
||||
if (isIntegrationTool) {
|
||||
// Integration tools use executeIntegrationTool
|
||||
executeIntegrationTool(tc.id).catch((err) => {
|
||||
logger.error('[AutoAllowedTools] Auto-execute pending integration tool failed', {
|
||||
toolCallId: tc.id,
|
||||
toolId,
|
||||
error: err,
|
||||
})
|
||||
})
|
||||
} else {
|
||||
// Client tools with interrupts use handleAccept
|
||||
const inst = getClientTool(tc.id) as any
|
||||
if (inst && typeof inst.handleAccept === 'function') {
|
||||
Promise.resolve()
|
||||
.then(() => inst.handleAccept(tc.params || {}))
|
||||
.catch((err: any) => {
|
||||
logger.error('[AutoAllowedTools] Auto-execute pending client tool failed', {
|
||||
toolCallId: tc.id,
|
||||
toolId,
|
||||
error: err,
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
logger.error('[AutoAllowedTools] Failed to add tool', { toolId, error: err })
|
||||
|
||||
@@ -231,8 +231,6 @@ export interface CopilotActions {
|
||||
triggerUserMessageId?: string
|
||||
) => Promise<void>
|
||||
handleNewChatCreation: (newChatId: string) => Promise<void>
|
||||
executeIntegrationTool: (toolCallId: string) => Promise<void>
|
||||
skipIntegrationTool: (toolCallId: string) => void
|
||||
loadAutoAllowedTools: () => Promise<void>
|
||||
addAutoAllowedTool: (toolId: string) => Promise<void>
|
||||
removeAutoAllowedTool: (toolId: string) => Promise<void>
|
||||
|
||||
Reference in New Issue
Block a user