Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
72 commits
Select commit Hold shift + click to select a range
06fbc29
v0
Sg312 Jan 30, 2026
cb3618a
v1
Sg312 Jan 30, 2026
8b7b331
Basic ss tes
Sg312 Jan 31, 2026
301e25c
Ss tests
Sg312 Jan 31, 2026
4c821d0
Stuff
Sg312 Jan 31, 2026
6cd8f1d
Add mcp
Sg312 Jan 31, 2026
793c877
mcp v1
Sg312 Jan 31, 2026
e04f379
Improvement
Sg312 Feb 1, 2026
4d84c54
Fix
Sg312 Feb 3, 2026
8f17bc4
BROKEN
Sg312 Feb 3, 2026
2d9a7c6
Checkpoint
Sg312 Feb 4, 2026
addc760
Streaming
Sg312 Feb 4, 2026
a58a61b
Fix abort
Sg312 Feb 4, 2026
be7fb8f
Things are broken
Sg312 Feb 4, 2026
b034b1c
Streaming seems to work but copilot is dumb
Sg312 Feb 4, 2026
8c48cdd
Fix edge issue
Sg312 Feb 4, 2026
d8daf3a
LUAAAA
Sg312 Feb 4, 2026
cbd7bb6
Fix stream buffer
Sg312 Feb 4, 2026
89782f6
Fix lint
Sg312 Feb 4, 2026
9a5a494
Checkpoint
Sg312 Feb 5, 2026
7402b38
Initial temp state, in the middle of a refactor
Sg312 Feb 5, 2026
4faa939
Initial test shows diff store still working
Sg312 Feb 5, 2026
7183448
Tool refactor
Sg312 Feb 5, 2026
b3e74e4
First cleanup pass complete - untested
Sg312 Feb 5, 2026
6b40d4f
Continued cleanup
Sg312 Feb 5, 2026
39968be
Refactor
Sg312 Feb 6, 2026
ba8f39f
Refactor complete - no testing yet
Sg312 Feb 6, 2026
fb4afeb
Fix - cursor makes me sad
Sg312 Feb 6, 2026
dd02395
Fix mcp
Sg312 Feb 6, 2026
08a8e14
Clean up mcp
Sg312 Feb 6, 2026
fd1e61b
Updated mcp
Sg312 Feb 6, 2026
69bdffa
Add respond to subagents
Sg312 Feb 6, 2026
74f863a
Fix definitions
Sg312 Feb 6, 2026
3dcd008
Add tools
Sg312 Feb 6, 2026
0c51ce5
Add tools
Sg312 Feb 6, 2026
df3523e
Add copilot mcp tracking
Sg312 Feb 6, 2026
6cb112e
Fix lint
Sg312 Feb 6, 2026
18e493e
Fix mcp
Sg312 Feb 6, 2026
a73e351
Fix
Sg312 Feb 6, 2026
67c2271
Updates
Sg312 Feb 6, 2026
a220455
Clean up mcp
Sg312 Feb 7, 2026
6735eaa
Fix copilot mcp tool names to be sim prefixed
Sg312 Feb 7, 2026
4d4d002
Add opus 4.6
Sg312 Feb 7, 2026
b07b812
Fix discovery tool
Sg312 Feb 7, 2026
220a540
Fix
Sg312 Feb 7, 2026
ebf4e90
Remove logs
Sg312 Feb 7, 2026
7e592e8
Fix go side tool rendering
Sg312 Feb 7, 2026
25d255a
Update docs
Sg312 Feb 9, 2026
b4361b8
Fix hydration
Sg312 Feb 9, 2026
d2c028f
Fix tool call resolution
Sg312 Feb 9, 2026
ed613c3
Fix
Sg312 Feb 9, 2026
4698f73
Fix lint
Sg312 Feb 9, 2026
79af303
Fix superagent and autoallow integrations
Sg312 Feb 9, 2026
c086912
Fix always allow
Sg312 Feb 9, 2026
9a47033
Update block
Sg312 Feb 9, 2026
7200421
Remove plan docs
Sg312 Feb 9, 2026
ab39a4f
Fix hardcoded ff
Sg312 Feb 9, 2026
dba4e61
Fix dropped provider
Sg312 Feb 9, 2026
b14e844
Fix lint
Sg312 Feb 9, 2026
395f890
Fix tests
Sg312 Feb 9, 2026
7458bbd
Fix dead messages array
Sg312 Feb 9, 2026
48c9a3a
Fix discovery
Sg312 Feb 9, 2026
7153141
Fix run workflow
Sg312 Feb 10, 2026
7670cdf
Fix run block
Sg312 Feb 10, 2026
1beb35c
Fix run from block in copilot
Sg312 Feb 10, 2026
18f1d76
Fix lint
Sg312 Feb 10, 2026
8a2eacf
Fix skip and mtb
Sg312 Feb 10, 2026
bd6a103
Fix typing
Sg312 Feb 10, 2026
ddc5164
Fix tool call
Sg312 Feb 10, 2026
bb4e072
Bump api version
Sg312 Feb 10, 2026
98df298
Fix bun lock
Sg312 Feb 10, 2026
621dd23
Nuke bad files
Sg312 Feb 10, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
v0
  • Loading branch information
Sg312 committed Feb 9, 2026
commit 06fbc29eaaf23d5377e5f846ebf29d412e7c36bb
439 changes: 61 additions & 378 deletions apps/sim/app/api/copilot/chat/route.ts

Large diffs are not rendered by default.

81 changes: 81 additions & 0 deletions apps/sim/app/api/v1/copilot/chat/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
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 { orchestrateCopilotStream } from '@/lib/copilot/orchestrator'

const logger = createLogger('CopilotHeadlessAPI')

const RequestSchema = z.object({
message: z.string().min(1, 'message is required'),
workflowId: z.string().min(1, 'workflowId is required'),
chatId: z.string().optional(),
mode: z.enum(['agent', 'ask', 'plan']).optional().default('agent'),
model: z.string().optional(),
autoExecuteTools: z.boolean().optional().default(true),
timeout: z.number().optional().default(300000),
})

/**
* POST /api/v1/copilot/chat
* Headless copilot endpoint for server-side orchestration.
*/
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 })
}

try {
const body = await req.json()
const parsed = RequestSchema.parse(body)
const defaults = getCopilotModel('chat')
const selectedModel = parsed.model || defaults.model

const requestPayload = {
message: parsed.message,
workflowId: parsed.workflowId,
userId: auth.userId,
stream: true,
streamToolCalls: true,
model: selectedModel,
mode: parsed.mode,
messageId: crypto.randomUUID(),
version: SIM_AGENT_VERSION,
...(parsed.chatId ? { chatId: parsed.chatId } : {}),
}

const result = await orchestrateCopilotStream(requestPayload, {
userId: auth.userId,
workflowId: parsed.workflowId,
chatId: parsed.chatId,
autoExecuteTools: parsed.autoExecuteTools,
timeout: parsed.timeout,
interactive: false,
})

return NextResponse.json({
success: result.success,
content: result.content,
toolCalls: result.toolCalls,
chatId: result.chatId,
conversationId: result.conversationId,
error: result.error,
})
} catch (error) {
if (error instanceof z.ZodError) {
return NextResponse.json(
{ success: false, error: 'Invalid request', details: error.errors },
{ status: 400 }
)
}

logger.error('Headless copilot request failed', {
error: error instanceof Error ? error.message : String(error),
})
return NextResponse.json({ success: false, error: 'Internal server error' }, { status: 500 })
}
}

Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
'use client'

import { createLogger } from '@sim/logger'
import { memo, useEffect, useMemo, useRef, useState } from 'react'
import clsx from 'clsx'
import { ChevronUp, LayoutList } from 'lucide-react'
Expand All @@ -26,6 +27,7 @@ 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'

Expand Down Expand Up @@ -1257,12 +1259,36 @@ function shouldShowRunSkipButtons(toolCall: CopilotToolCall): boolean {
return false
}

const toolCallLogger = createLogger('CopilotToolCall')

async function sendToolDecision(toolCallId: string, status: 'accepted' | 'rejected') {
try {
await fetch('/api/copilot/confirm', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ toolCallId, status }),
})
} catch (error) {
toolCallLogger.warn('Failed to send tool decision', {
toolCallId,
status,
error: error instanceof Error ? error.message : String(error),
})
}
}

async function handleRun(
toolCall: CopilotToolCall,
setToolCallState: any,
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)) {
Expand Down Expand Up @@ -1307,6 +1333,12 @@ async function handleRun(
}

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)) {
Expand Down
23 changes: 23 additions & 0 deletions apps/sim/lib/copilot/orchestrator/config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/**
* Feature flag for server-side copilot orchestration.
*/
export const COPILOT_SERVER_ORCHESTRATED = true

export const INTERRUPT_TOOL_NAMES = [
'set_global_workflow_variables',
'run_workflow',
'manage_mcp_tool',
'manage_custom_tool',
'deploy_mcp',
'deploy_chat',
'deploy_api',
'create_workspace_mcp_server',
'set_environment_variables',
'make_api_request',
'oauth_request_access',
'navigate_ui',
'knowledge_base',
] as const

export const INTERRUPT_TOOL_SET = new Set<string>(INTERRUPT_TOOL_NAMES)

181 changes: 181 additions & 0 deletions apps/sim/lib/copilot/orchestrator/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
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 {
handleSubagentRouting,
sseHandlers,
subAgentHandlers,
} from '@/lib/copilot/orchestrator/sse-handlers'
import { prepareExecutionContext } from '@/lib/copilot/orchestrator/tool-executor'
import type {
ExecutionContext,
OrchestratorOptions,
OrchestratorResult,
SSEEvent,
StreamingContext,
ToolCallSummary,
} from '@/lib/copilot/orchestrator/types'

const logger = createLogger('CopilotOrchestrator')
const SIM_AGENT_API_URL = env.SIM_AGENT_API_URL || SIM_AGENT_API_URL_DEFAULT

export interface OrchestrateStreamOptions extends OrchestratorOptions {
userId: string
workflowId: string
chatId?: string
}

/**
* Orchestrate a copilot SSE stream and execute tool calls server-side.
*/
export async function orchestrateCopilotStream(
requestPayload: Record<string, any>,
options: OrchestrateStreamOptions
): Promise<OrchestratorResult> {
const { userId, workflowId, chatId, timeout = 300000, abortSignal } = options
const execContext = await prepareExecutionContext(userId, workflowId)

const context: StreamingContext = {
chatId,
conversationId: undefined,
messageId: requestPayload?.messageId || crypto.randomUUID(),
accumulatedContent: '',
contentBlocks: [],
toolCalls: new Map(),
currentThinkingBlock: null,
isInThinkingBlock: false,
subAgentParentToolCallId: undefined,
subAgentContent: {},
subAgentToolCalls: {},
pendingContent: '',
streamComplete: false,
wasAborted: false,
errors: [],
}

try {
const response = await fetch(`${SIM_AGENT_API_URL}/api/chat-completion-streaming`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
...(env.COPILOT_API_KEY ? { 'x-api-key': env.COPILOT_API_KEY } : {}),
},
body: JSON.stringify(requestPayload),
signal: abortSignal,
})

if (!response.ok) {
const errorText = await response.text().catch(() => '')
throw new Error(`Copilot backend error (${response.status}): ${errorText || response.statusText}`)
}

if (!response.body) {
throw new Error('Copilot backend response missing body')
}

const reader = response.body.getReader()
const decoder = new TextDecoder()

const timeoutId = setTimeout(() => {
context.errors.push('Request timed out')
context.streamComplete = true
reader.cancel().catch(() => {})
}, timeout)

try {
for await (const event of parseSSEStream(reader, decoder, abortSignal)) {
if (abortSignal?.aborted) {
context.wasAborted = true
break
}

await forwardEvent(event, options)

if (event.type === 'subagent_start') {
const toolCallId = event.data?.tool_call_id
if (toolCallId) {
context.subAgentParentToolCallId = toolCallId
context.subAgentContent[toolCallId] = ''
context.subAgentToolCalls[toolCallId] = []
}
continue
}

if (event.type === 'subagent_end') {
context.subAgentParentToolCallId = undefined
continue
}

if (handleSubagentRouting(event, context)) {
const handler = subAgentHandlers[event.type]
if (handler) {
await handler(event, context, execContext, options)
}
if (context.streamComplete) break
continue
}

const handler = sseHandlers[event.type]
if (handler) {
await handler(event, context, execContext, options)
}
if (context.streamComplete) break
}
} finally {
clearTimeout(timeoutId)
}

const result = buildResult(context)
await options.onComplete?.(result)
return result
} catch (error) {
const err = error instanceof Error ? error : new Error('Copilot orchestration failed')
logger.error('Copilot orchestration failed', { error: err.message })
await options.onError?.(err)
return {
success: false,
content: '',
contentBlocks: [],
toolCalls: [],
chatId: context.chatId,
conversationId: context.conversationId,
error: err.message,
}
}
}

async function forwardEvent(event: SSEEvent, options: OrchestratorOptions): Promise<void> {
try {
await options.onEvent?.(event)
} catch (error) {
logger.warn('Failed to forward SSE event', {
type: event.type,
error: error instanceof Error ? error.message : String(error),
})
}
}

function buildResult(context: StreamingContext): OrchestratorResult {
const toolCalls: ToolCallSummary[] = Array.from(context.toolCalls.values()).map((toolCall) => ({
id: toolCall.id,
name: toolCall.name,
status: toolCall.status,
params: toolCall.params,
result: toolCall.result?.output,
error: toolCall.error,
durationMs:
toolCall.endTime && toolCall.startTime ? toolCall.endTime - toolCall.startTime : undefined,
}))

return {
success: context.errors.length === 0,
content: context.accumulatedContent,
contentBlocks: context.contentBlocks,
toolCalls,
chatId: context.chatId,
conversationId: context.conversationId,
errors: context.errors.length ? context.errors : undefined,
}
}

Loading