This commit is contained in:
Siddharth Ganesan
2026-02-04 12:35:12 -08:00
parent 728463ace7
commit 8b87a07508
19 changed files with 187 additions and 138 deletions

View File

@@ -9,6 +9,12 @@ import { generateChatTitle } from '@/lib/copilot/chat-title'
import { getCopilotModel } from '@/lib/copilot/config'
import { SIM_AGENT_VERSION } from '@/lib/copilot/constants'
import { COPILOT_MODEL_IDS, COPILOT_REQUEST_MODES } from '@/lib/copilot/models'
import { orchestrateCopilotStream } from '@/lib/copilot/orchestrator'
import {
createStreamEventWriter,
resetStreamBuffer,
setStreamMeta,
} from '@/lib/copilot/orchestrator/stream-buffer'
import {
authenticateCopilotRequestSessionOnly,
createBadRequestResponse,
@@ -24,16 +30,9 @@ 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'
import {
createStreamEventWriter,
resetStreamBuffer,
setStreamMeta,
} from '@/lib/copilot/orchestrator/stream-buffer'
const logger = createLogger('CopilotChatAPI')
const FileAttachmentSchema = z.object({
id: z.string(),
key: z.string(),

View File

@@ -1,12 +1,12 @@
import { type NextRequest, NextResponse } from 'next/server'
import { createLogger } from '@sim/logger'
import { SSE_HEADERS } from '@/lib/core/utils/sse'
import { authenticateCopilotRequestSessionOnly } from '@/lib/copilot/request-helpers'
import { type NextRequest, NextResponse } from 'next/server'
import {
getStreamMeta,
readStreamEvents,
type StreamMeta,
} from '@/lib/copilot/orchestrator/stream-buffer'
import { authenticateCopilotRequestSessionOnly } from '@/lib/copilot/request-helpers'
import { SSE_HEADERS } from '@/lib/core/utils/sse'
const logger = createLogger('CopilotChatStreamAPI')
const POLL_INTERVAL_MS = 250
@@ -56,9 +56,7 @@ export async function GET(request: NextRequest) {
// Batch mode: return all buffered events as JSON
if (batchMode) {
const events = await readStreamEvents(streamId, fromEventId)
const filteredEvents = toEventId
? events.filter((e) => e.eventId <= toEventId)
: events
const filteredEvents = toEventId ? events.filter((e) => e.eventId <= toEventId) : events
logger.info('[Resume] Batch response', {
streamId,
fromEventId,
@@ -130,4 +128,3 @@ export async function GET(request: NextRequest) {
return new Response(stream, { headers: SSE_HEADERS })
}

View File

@@ -15,8 +15,11 @@ 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 {
executeToolServerSide,
prepareExecutionContext,
} from '@/lib/copilot/orchestrator/tool-executor'
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')
@@ -408,4 +411,3 @@ async function handleSubagentToolCall(
return NextResponse.json(createResponse(id, response))
}

View File

@@ -1,12 +1,12 @@
import { createLogger } from '@sim/logger'
import { type NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import { authenticateV1Request } from '@/app/api/v1/auth'
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'
import { authenticateV1Request } from '@/app/api/v1/auth'
const logger = createLogger('CopilotHeadlessAPI')
@@ -24,7 +24,7 @@ const RequestSchema = z.object({
/**
* POST /api/v1/copilot/chat
* Headless copilot endpoint for server-side orchestration.
*
*
* workflowId is optional - if not provided:
* - If workflowName is provided, finds that workflow
* - Otherwise uses the user's first workflow as context
@@ -33,7 +33,10 @@ const RequestSchema = z.object({
export async function POST(req: NextRequest) {
const auth = await authenticateV1Request(req)
if (!auth.authenticated || !auth.userId) {
return NextResponse.json({ success: false, error: auth.error || 'Unauthorized' }, { status: 401 })
return NextResponse.json(
{ success: false, error: auth.error || 'Unauthorized' },
{ status: 401 }
)
}
try {
@@ -43,10 +46,17 @@ export async function POST(req: NextRequest) {
const selectedModel = parsed.model || defaults.model
// Resolve workflow ID
const resolved = await resolveWorkflowIdForUser(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.' },
{
success: false,
error: 'No workflows found. Create a workflow first or provide a valid workflowId.',
},
{ status: 400 }
)
}
@@ -104,4 +114,3 @@ export async function POST(req: NextRequest) {
return NextResponse.json({ success: false, error: 'Internal server error' }, { status: 500 })
}
}

View File

@@ -1,7 +1,7 @@
'use client'
import { createLogger } from '@sim/logger'
import { memo, useEffect, useMemo, useRef, useState } from 'react'
import { createLogger } from '@sim/logger'
import clsx from 'clsx'
import { ChevronUp, LayoutList } from 'lucide-react'
import Editor from 'react-simple-code-editor'
@@ -1260,7 +1260,10 @@ function shouldShowRunSkipButtons(toolCall: CopilotToolCall): boolean {
const toolCallLogger = createLogger('CopilotToolCall')
async function sendToolDecision(toolCallId: string, status: 'accepted' | 'rejected' | 'background') {
async function sendToolDecision(
toolCallId: string,
status: 'accepted' | 'rejected' | 'background'
) {
try {
await fetch('/api/copilot/confirm', {
method: 'POST',

View File

@@ -34,4 +34,3 @@ export const SUBAGENT_TOOL_NAMES = [
] as const
export const SUBAGENT_TOOL_SET = new Set<string>(SUBAGENT_TOOL_NAMES)

View File

@@ -1,7 +1,5 @@
import { createLogger } from '@sim/logger'
import { SIM_AGENT_API_URL_DEFAULT } from '@/lib/copilot/constants'
import { env } from '@/lib/core/config/env'
import { parseSSEStream } from '@/lib/copilot/orchestrator/sse-parser'
import {
getToolCallIdFromEvent,
handleSubagentRouting,
@@ -13,15 +11,16 @@ import {
wasToolCallSeen,
wasToolResultSeen,
} from '@/lib/copilot/orchestrator/sse-handlers'
import { parseSSEStream } from '@/lib/copilot/orchestrator/sse-parser'
import { prepareExecutionContext } from '@/lib/copilot/orchestrator/tool-executor'
import type {
ExecutionContext,
OrchestratorOptions,
OrchestratorResult,
SSEEvent,
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
@@ -73,7 +72,9 @@ export async function orchestrateCopilotStream(
if (!response.ok) {
const errorText = await response.text().catch(() => '')
throw new Error(`Copilot backend error (${response.status}): ${errorText || response.statusText}`)
throw new Error(
`Copilot backend error (${response.status}): ${errorText || response.statusText}`
)
}
if (!response.body) {
@@ -104,7 +105,8 @@ export async function orchestrateCopilotStream(
const toolCallId = getToolCallIdFromEvent(normalizedEvent)
const eventData = normalizedEvent.data
const isPartialToolCall = normalizedEvent.type === 'tool_call' && eventData?.partial === true
const isPartialToolCall =
normalizedEvent.type === 'tool_call' && eventData?.partial === true
const shouldSkipToolCall =
normalizedEvent.type === 'tool_call' &&
@@ -220,4 +222,3 @@ function buildResult(context: StreamingContext): OrchestratorResult {
errors: context.errors.length ? context.errors : undefined,
}
}

View File

@@ -69,7 +69,10 @@ export async function saveMessages(
/**
* Update the conversationId for a chat without overwriting messages.
*/
export async function updateChatConversationId(chatId: string, conversationId: string): Promise<void> {
export async function updateChatConversationId(
chatId: string,
conversationId: string
): Promise<void> {
await db
.update(copilotChats)
.set({
@@ -135,4 +138,3 @@ export async function getToolConfirmation(toolCallId: string): Promise<{
return null
}
}

View File

@@ -1,4 +1,7 @@
import { createLogger } from '@sim/logger'
import { INTERRUPT_TOOL_SET, SUBAGENT_TOOL_SET } from '@/lib/copilot/orchestrator/config'
import { getToolConfirmation } from '@/lib/copilot/orchestrator/persistence'
import { executeToolServerSide, markToolComplete } from '@/lib/copilot/orchestrator/tool-executor'
import type {
ContentBlock,
ExecutionContext,
@@ -7,9 +10,6 @@ import type {
StreamingContext,
ToolCallState,
} from '@/lib/copilot/orchestrator/types'
import { executeToolServerSide, markToolComplete } from '@/lib/copilot/orchestrator/tool-executor'
import { getToolConfirmation } from '@/lib/copilot/orchestrator/persistence'
import { INTERRUPT_TOOL_SET, SUBAGENT_TOOL_SET } from '@/lib/copilot/orchestrator/config'
const logger = createLogger('CopilotSseHandlers')
@@ -25,9 +25,12 @@ const seenToolResults = new Set<string>()
export function markToolCallSeen(toolCallId: string): void {
seenToolCalls.add(toolCallId)
setTimeout(() => {
seenToolCalls.delete(toolCallId)
}, 5 * 60 * 1000)
setTimeout(
() => {
seenToolCalls.delete(toolCallId)
},
5 * 60 * 1000
)
}
export function wasToolCallSeen(toolCallId: string): boolean {
@@ -99,9 +102,12 @@ export function normalizeSseEvent(event: SSEEvent): SSEEvent {
*/
export function markToolResultSeen(toolCallId: string): void {
seenToolResults.add(toolCallId)
setTimeout(() => {
seenToolResults.delete(toolCallId)
}, 5 * 60 * 1000)
setTimeout(
() => {
seenToolResults.delete(toolCallId)
},
5 * 60 * 1000
)
}
/**
@@ -134,10 +140,7 @@ export type SSEHandler = (
options: OrchestratorOptions
) => void | Promise<void>
function addContentBlock(
context: StreamingContext,
block: Omit<ContentBlock, 'timestamp'>
): void {
function addContentBlock(context: StreamingContext, block: Omit<ContentBlock, 'timestamp'>): void {
context.contentBlocks.push({
...block,
timestamp: Date.now(),
@@ -195,7 +198,7 @@ async function executeToolAndReport(
success: result.success,
result: result.output,
data: {
id: toolCall.id,
id: toolCall.id,
name: toolCall.name,
success: result.success,
result: result.output,
@@ -256,7 +259,7 @@ export const sseHandlers: Record<string, SSEHandler> = {
const hasError = !!data?.error || !!data?.result?.error
// If explicitly set, use that; otherwise infer from data presence
const success = hasExplicitSuccess ? !!explicitSuccess : (hasResultData && !hasError)
const success = hasExplicitSuccess ? !!explicitSuccess : hasResultData && !hasError
current.status = success ? 'success' : 'error'
current.endTime = Date.now()
@@ -306,7 +309,10 @@ export const sseHandlers: Record<string, SSEHandler> = {
// If we've already completed this tool call, ignore late/duplicate tool_call events
// to avoid resetting UI/state back to pending and re-executing.
if (existing?.endTime || (existing && existing.status !== 'pending' && existing.status !== 'executing')) {
if (
existing?.endTime ||
(existing && existing.status !== 'pending' && existing.status !== 'executing')
) {
if (!existing.params && args) {
existing.params = args
}
@@ -343,7 +349,10 @@ export const sseHandlers: Record<string, SSEHandler> = {
if (RESPOND_TOOL_SET.has(toolName)) {
toolCall.status = 'success'
toolCall.endTime = Date.now()
toolCall.result = { success: true, output: 'Internal respond tool - handled by copilot backend' }
toolCall.result = {
success: true,
output: 'Internal respond tool - handled by copilot backend',
}
return
}
@@ -429,12 +438,14 @@ 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 chunk =
typeof event.data === 'string' ? event.data : event.data?.data || event.data?.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 chunk =
typeof event.data === 'string' ? event.data : event.data?.content || event.data?.data
if (!chunk) return
context.accumulatedContent += chunk
addContentBlock(context, { type: 'text', content: chunk })
@@ -452,7 +463,9 @@ export const sseHandlers: Record<string, SSEHandler> = {
},
error: (event, context) => {
const message =
event.data?.message || event.data?.error || (typeof event.data === 'string' ? event.data : null)
event.data?.message ||
event.data?.error ||
(typeof event.data === 'string' ? event.data : null)
if (message) {
context.errors.push(message)
}
@@ -466,7 +479,8 @@ export const subAgentHandlers: Record<string, SSEHandler> = {
if (!parentToolCallId || !event.data) return
const chunk = typeof event.data === 'string' ? event.data : event.data?.content || ''
if (!chunk) return
context.subAgentContent[parentToolCallId] = (context.subAgentContent[parentToolCallId] || '') + chunk
context.subAgentContent[parentToolCallId] =
(context.subAgentContent[parentToolCallId] || '') + chunk
addContentBlock(context, { type: 'subagent_text', content: chunk })
},
tool_call: async (event, context, execContext, options) => {
@@ -510,7 +524,10 @@ export const subAgentHandlers: Record<string, SSEHandler> = {
if (RESPOND_TOOL_SET.has(toolName)) {
toolCall.status = 'success'
toolCall.endTime = Date.now()
toolCall.result = { success: true, output: 'Internal respond tool - handled by copilot backend' }
toolCall.result = {
success: true,
output: 'Internal respond tool - handled by copilot backend',
}
return
}
@@ -541,9 +558,7 @@ export const subAgentHandlers: Record<string, SSEHandler> = {
const status = success ? 'success' : 'error'
const endTime = Date.now()
const result = hasResultData
? { success, output: data?.result || data?.data }
: undefined
const result = hasResultData ? { success, output: data?.result || data?.data } : undefined
if (subAgentToolCall) {
subAgentToolCall.status = status
@@ -572,4 +587,3 @@ export function handleSubagentRouting(event: SSEEvent, context: StreamingContext
}
return true
}

View File

@@ -69,4 +69,3 @@ export async function* parseSSEStream(
}
}
}

View File

@@ -78,10 +78,7 @@ export async function resetStreamBuffer(streamId: string): Promise<void> {
}
}
export async function setStreamMeta(
streamId: string,
meta: StreamMeta
): Promise<void> {
export async function setStreamMeta(streamId: string, meta: StreamMeta): Promise<void> {
const redis = getRedisClient()
if (!redis) return
try {
@@ -263,4 +260,3 @@ export async function readStreamEvents(
return []
}
}

View File

@@ -1,6 +1,5 @@
import { createLogger } from '@sim/logger'
import { SIM_AGENT_API_URL_DEFAULT } from '@/lib/copilot/constants'
import { parseSSEStream } from '@/lib/copilot/orchestrator/sse-parser'
import {
getToolCallIdFromEvent,
handleSubagentRouting,
@@ -12,6 +11,7 @@ import {
wasToolCallSeen,
wasToolResultSeen,
} from '@/lib/copilot/orchestrator/sse-handlers'
import { parseSSEStream } from '@/lib/copilot/orchestrator/sse-parser'
import { prepareExecutionContext } from '@/lib/copilot/orchestrator/tool-executor'
import type {
ExecutionContext,
@@ -121,7 +121,8 @@ export async function orchestrateSubagentStream(
const toolCallId = getToolCallIdFromEvent(normalizedEvent)
const eventData = normalizedEvent.data
const isPartialToolCall = normalizedEvent.type === 'tool_call' && eventData?.partial === true
const isPartialToolCall =
normalizedEvent.type === 'tool_call' && eventData?.partial === true
const shouldSkipToolCall =
normalizedEvent.type === 'tool_call' &&
@@ -151,7 +152,10 @@ export async function orchestrateSubagentStream(
await forwardEvent(normalizedEvent, options)
}
if (normalizedEvent.type === 'structured_result' || normalizedEvent.type === 'subagent_result') {
if (
normalizedEvent.type === 'structured_result' ||
normalizedEvent.type === 'subagent_result'
) {
structuredResult = normalizeStructuredResult(normalizedEvent.data)
context.streamComplete = true
continue
@@ -280,4 +284,3 @@ function buildResult(
errors: context.errors.length ? context.errors : undefined,
}
}

View File

@@ -12,38 +12,42 @@ import {
} from '@sim/db/schema'
import { createLogger } from '@sim/logger'
import { and, asc, desc, eq, inArray, isNull, max, or } from 'drizzle-orm'
import { refreshTokenIfNeeded } from '@/app/api/auth/oauth/utils'
import { checkChatAccess, checkWorkflowAccessForChatCreation } from '@/app/api/chat/utils'
import { resolveEnvVarReferences } from '@/executor/utils/reference-validation'
import { normalizeName } from '@/executor/constants'
import { SIM_AGENT_API_URL_DEFAULT } from '@/lib/copilot/constants'
import { generateRequestId } from '@/lib/core/utils/request'
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 type {
ExecutionContext,
ToolCallResult,
ToolCallState,
} from '@/lib/copilot/orchestrator/types'
import { routeExecution } from '@/lib/copilot/tools/server/router'
import {
extractWorkflowNames,
formatNormalizedWorkflowForCopilot,
normalizeWorkflowName,
} from '@/lib/copilot/tools/shared/workflow-utils'
import { env } from '@/lib/core/config/env'
import { generateRequestId } from '@/lib/core/utils/request'
import { getEffectiveDecryptedEnv } from '@/lib/environment/utils'
import { mcpService } from '@/lib/mcp/service'
import { sanitizeToolName } from '@/lib/mcp/workflow-tool-schema'
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 {
deployWorkflow,
loadWorkflowFromNormalizedTables,
saveWorkflowToNormalizedTables,
undeployWorkflow,
} from '@/lib/workflows/persistence/utils'
import { executeWorkflow } from '@/lib/workflows/executor/execute-workflow'
import { BlockPathCalculator } from '@/lib/workflows/blocks/block-path-calculator'
import { getBlockOutputPaths } from '@/lib/workflows/blocks/block-outputs'
import { isInputDefinitionTrigger } from '@/lib/workflows/triggers/input-definition-triggers'
import { hasValidStartBlock } from '@/lib/workflows/triggers/trigger-utils.server'
import { refreshTokenIfNeeded } from '@/app/api/auth/oauth/utils'
import { checkChatAccess, checkWorkflowAccessForChatCreation } from '@/app/api/chat/utils'
import { normalizeName } from '@/executor/constants'
import { resolveEnvVarReferences } from '@/executor/utils/reference-validation'
import { executeTool } from '@/tools'
import { getTool, resolveToolId } from '@/tools/utils'
import { routeExecution } from '@/lib/copilot/tools/server/router'
import { sanitizeToolName } from '@/lib/mcp/workflow-tool-schema'
import type { ExecutionContext, ToolCallResult, ToolCallState } from '@/lib/copilot/orchestrator/types'
const logger = createLogger('CopilotToolExecutor')
const SIM_AGENT_API_URL = env.SIM_AGENT_API_URL || SIM_AGENT_API_URL_DEFAULT
@@ -171,11 +175,9 @@ async function executeIntegrationToolDirect(
const decryptedEnvVars =
context.decryptedEnvVars || (await getEffectiveDecryptedEnv(userId, workspaceId))
const executionParams: Record<string, any> = resolveEnvVarReferences(
toolArgs,
decryptedEnvVars,
{ deep: true }
) as Record<string, any>
const executionParams: Record<string, any> = resolveEnvVarReferences(toolArgs, decryptedEnvVars, {
deep: true,
}) as Record<string, any>
if (toolConfig.oauth?.required && toolConfig.oauth.provider) {
const provider = toolConfig.oauth.provider
@@ -285,7 +287,10 @@ async function executeSimWorkflowTool(
}
}
async function ensureWorkflowAccess(workflowId: string, userId: string): Promise<{
async function ensureWorkflowAccess(
workflowId: string,
userId: string
): Promise<{
workflow: typeof workflow.$inferSelect
workspaceId?: string | null
}> {
@@ -538,8 +543,8 @@ async function executeListFolders(
context: ExecutionContext
): Promise<ToolCallResult> {
try {
const workspaceId = (params?.workspaceId as string | undefined) ||
(await getDefaultWorkspaceId(context.userId))
const workspaceId =
(params?.workspaceId as string | undefined) || (await getDefaultWorkspaceId(context.userId))
await ensureWorkspaceAccess(workspaceId, context.userId, false)
@@ -794,9 +799,10 @@ async function executeGetBlockOutputs(
const blocks = normalized.blocks || {}
const loops = normalized.loops || {}
const parallels = normalized.parallels || {}
const blockIds = Array.isArray(params.blockIds) && params.blockIds.length > 0
? params.blockIds
: Object.keys(blocks)
const blockIds =
Array.isArray(params.blockIds) && params.blockIds.length > 0
? params.blockIds
: Object.keys(blocks)
const results: Array<{
blockId: string
@@ -935,8 +941,7 @@ async function executeGetBlockUpstreamReferences(
for (const accessibleBlockId of accessibleIds) {
const block = blocks[accessibleBlockId]
if (!block?.type) continue
const canSelfReference =
block.type === 'approval' || block.type === 'human_in_the_loop'
const canSelfReference = block.type === 'approval' || block.type === 'human_in_the_loop'
if (accessibleBlockId === blockId && !canSelfReference) continue
const blockName = block.name || block.type
@@ -1149,7 +1154,12 @@ async function executeDeployApi(
return {
success: true,
output: { workflowId, isDeployed: true, deployedAt: result.deployedAt, version: result.version },
output: {
workflowId,
isDeployed: true,
deployedAt: result.deployedAt,
version: result.version,
},
}
} catch (error) {
return { success: false, error: error instanceof Error ? error.message : String(error) }
@@ -1196,7 +1206,10 @@ async function executeDeployChat(
const identifierPattern = /^[a-z0-9-]+$/
if (!identifierPattern.test(identifier)) {
return { success: false, error: 'Identifier can only contain lowercase letters, numbers, and hyphens' }
return {
success: false,
error: 'Identifier can only contain lowercase letters, numbers, and hyphens',
}
}
const existingIdentifier = await db
@@ -1273,7 +1286,10 @@ async function executeDeployChat(
})
}
return { success: true, output: { success: true, action: 'deploy', isDeployed: true, identifier } }
return {
success: true,
output: { success: true, action: 'deploy', isDeployed: true, identifier },
}
} catch (error) {
return { success: false, error: error instanceof Error ? error.message : String(error) }
}
@@ -1313,12 +1329,18 @@ async function executeDeployMcp(
const existingTool = await db
.select()
.from(workflowMcpTool)
.where(and(eq(workflowMcpTool.serverId, serverId), eq(workflowMcpTool.workflowId, workflowId)))
.where(
and(eq(workflowMcpTool.serverId, serverId), eq(workflowMcpTool.workflowId, workflowId))
)
.limit(1)
const toolName = sanitizeToolName(params.toolName || workflowRecord.name || `workflow_${workflowId}`)
const toolName = sanitizeToolName(
params.toolName || workflowRecord.name || `workflow_${workflowId}`
)
const toolDescription =
params.toolDescription || workflowRecord.description || `Execute ${workflowRecord.name} workflow`
params.toolDescription ||
workflowRecord.description ||
`Execute ${workflowRecord.name} workflow`
const parameterSchema = params.parameterSchema || {}
if (existingTool.length > 0) {
@@ -1387,11 +1409,7 @@ async function executeCheckDeploymentStatus(
const workspaceId = workflowRecord.workspaceId
const [apiDeploy, chatDeploy] = await Promise.all([
db
.select()
.from(workflow)
.where(eq(workflow.id, workflowId))
.limit(1),
db.select().from(workflow).where(eq(workflow.id, workflowId)).limit(1),
db.select().from(chat).where(eq(chat.workflowId, workflowId)).limit(1),
])
@@ -1546,10 +1564,7 @@ async function executeCreateWorkspaceMcpServer(
const addedTools: Array<{ workflowId: string; toolName: string }> = []
if (workflowIds.length > 0) {
const workflows = await db
.select()
.from(workflow)
.where(inArray(workflow.id, workflowIds))
const workflows = await db.select().from(workflow).where(inArray(workflow.id, workflowIds))
for (const wf of workflows) {
if (wf.workspaceId !== workspaceId || !wf.isDeployed) {
@@ -1559,7 +1574,7 @@ async function executeCreateWorkspaceMcpServer(
if (!hasStartBlock) {
continue
}
const toolName = sanitizeToolName(wf.name || `workflow_${wf.id}`)
const toolName = sanitizeToolName(wf.name || `workflow_${wf.id}`)
await db.insert(workflowMcpTool).values({
id: crypto.randomUUID(),
serverId,
@@ -1674,13 +1689,12 @@ export async function prepareExecutionContext(
userId: string,
workflowId: string
): Promise<ExecutionContext> {
let workspaceId: string | undefined
const workflowResult = await db
.select({ workspaceId: workflow.workspaceId })
.from(workflow)
.where(eq(workflow.id, workflowId))
.limit(1)
workspaceId = workflowResult[0]?.workspaceId ?? undefined
const workspaceId = workflowResult[0]?.workspaceId ?? undefined
const decryptedEnvVars = await getEffectiveDecryptedEnv(userId, workspaceId)
@@ -1691,4 +1705,3 @@ export async function prepareExecutionContext(
decryptedEnvVars,
}
}

View File

@@ -128,4 +128,3 @@ export interface ExecutionContext {
workspaceId?: string
decryptedEnvVars?: Record<string, string>
}

View File

@@ -20,7 +20,8 @@ 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.',
description:
'List all workflows the user has access to. Returns workflow IDs, names, and workspace info.',
inputSchema: {
type: 'object',
properties: {
@@ -38,7 +39,8 @@ export const DIRECT_TOOL_DEFS: DirectToolDef[] = [
{
name: 'list_workspaces',
toolId: 'list_user_workspaces',
description: 'List all workspaces the user has access to. Returns workspace IDs, names, and roles.',
description:
'List all workspaces the user has access to. Returns workspace IDs, names, and roles.',
inputSchema: {
type: 'object',
properties: {},
@@ -225,10 +227,14 @@ IMPORTANT: Pass the returned plan EXACTLY to copilot_edit - do not modify or sum
inputSchema: {
type: 'object',
properties: {
request: { type: 'string', description: 'What you want to build or modify in the workflow.' },
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.',
description:
'REQUIRED. The workflow ID. For new workflows, call create_workflow first to get this.',
},
context: { type: 'object' },
},
@@ -261,15 +267,18 @@ IMPORTANT: After copilot_edit completes, you MUST call copilot_deploy before the
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.',
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.',
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.',
description:
'Additional context. Put the plan in context.plan if not using the plan field directly.',
},
},
required: ['workflowId'],
@@ -463,4 +472,3 @@ USE THIS:
},
},
]

View File

@@ -6,9 +6,9 @@ import { eq } from 'drizzle-orm'
import type { BaseServerTool } from '@/lib/copilot/tools/server/base-tool'
import { validateSelectorIds } from '@/lib/copilot/validation/selector-validator'
import type { PermissionGroupConfig } from '@/lib/permission-groups/types'
import { applyAutoLayout } from '@/lib/workflows/autolayout'
import { getBlockOutputs } from '@/lib/workflows/blocks/block-outputs'
import { extractAndPersistCustomTools } from '@/lib/workflows/persistence/custom-tools-persistence'
import { applyAutoLayout } from '@/lib/workflows/autolayout'
import {
loadWorkflowFromNormalizedTables,
saveWorkflowToNormalizedTables,
@@ -3276,9 +3276,8 @@ export const editWorkflowServerTool: BaseServerTool<EditWorkflowParams, any> = {
padding: { x: 100, y: 100 },
})
const layoutedBlocks = layoutResult.success && layoutResult.blocks
? layoutResult.blocks
: finalWorkflowState.blocks
const layoutedBlocks =
layoutResult.success && layoutResult.blocks ? layoutResult.blocks : finalWorkflowState.blocks
if (!layoutResult.success) {
logger.warn('Autolayout failed, using default positions', {

View File

@@ -26,7 +26,9 @@ export function formatNormalizedWorkflowForCopilot(
}
export function normalizeWorkflowName(name?: string | null): string {
return String(name || '').trim().toLowerCase()
return String(name || '')
.trim()
.toLowerCase()
}
export function extractWorkflowNames(workflows: Array<{ name?: string | null }>): string[] {
@@ -34,4 +36,3 @@ export function extractWorkflowNames(workflows: Array<{ name?: string | null }>)
.map((workflow) => (typeof workflow?.name === 'string' ? workflow.name : null))
.filter((name): name is string => Boolean(name))
}

View File

@@ -48,7 +48,10 @@ export async function resolveWorkflowIdForUser(
if (workflowName) {
const match = workflows.find(
(w) => String(w.name || '').trim().toLowerCase() === workflowName.toLowerCase()
(w) =>
String(w.name || '')
.trim()
.toLowerCase() === workflowName.toLowerCase()
)
if (match) {
return { workflowId: match.id, workflowName: match.name || undefined }

View File

@@ -80,8 +80,8 @@ import { subscriptionKeys } from '@/hooks/queries/subscription'
import type {
ChatContext,
CopilotMessage,
CopilotStreamInfo,
CopilotStore,
CopilotStreamInfo,
CopilotToolCall,
MessageFileAttachment,
} from '@/stores/panel/copilot/types'
@@ -2251,7 +2251,7 @@ function createOptimizedContentBlocks(contentBlocks: any[]): any[] {
}
return result
}
``
;``
function updateStreamingMessage(set: any, context: StreamingContext) {
if (context.suppressStreamingUpdates) return
const now = performance.now()
@@ -3124,8 +3124,8 @@ export const useCopilotStore = create<CopilotStore>()(
replayBlocks && replayBlocks.length > 0
? replayBlocks
: bufferedContent
? [{ type: TEXT_BLOCK_TYPE, content: bufferedContent, timestamp: Date.now() }]
: [],
? [{ type: TEXT_BLOCK_TYPE, content: bufferedContent, timestamp: Date.now() }]
: [],
}
nextMessages = [...nextMessages, assistantMessage]
} else if (bufferedContent || (replayBlocks && replayBlocks.length > 0)) {
@@ -3207,7 +3207,10 @@ export const useCopilotStore = create<CopilotStore>()(
set({ isSendingMessage: false, abortController: null })
} catch (error) {
// Handle AbortError gracefully - expected when user aborts
if (error instanceof Error && (error.name === 'AbortError' || error.message.includes('aborted'))) {
if (
error instanceof Error &&
(error.name === 'AbortError' || error.message.includes('aborted'))
) {
logger.info('[Copilot] Resume stream aborted by user')
set({ isSendingMessage: false, abortController: null })
return false
@@ -4123,7 +4126,6 @@ export const useCopilotStore = create<CopilotStore>()(
logger.info('[AutoAllowedTools] API returned', { toolId, tools: data.autoAllowedTools })
set({ autoAllowedTools: data.autoAllowedTools || [] })
logger.info('[AutoAllowedTools] Added tool to store', { toolId })
}
} catch (err) {
logger.error('[AutoAllowedTools] Failed to add tool', { toolId, error: err })