mirror of
https://github.com/simstudioai/sim.git
synced 2026-01-31 01:37:58 -05:00
Ss tests
This commit is contained in:
@@ -7,6 +7,7 @@ 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'
|
||||
|
||||
const logger = createLogger('CopilotHeadlessAPI')
|
||||
@@ -16,7 +17,7 @@ const RequestSchema = z.object({
|
||||
workflowId: z.string().optional(),
|
||||
workflowName: z.string().optional(),
|
||||
chatId: z.string().optional(),
|
||||
mode: z.enum(['agent', 'ask', 'plan', 'fast']).optional().default('fast'),
|
||||
mode: z.enum(COPILOT_REQUEST_MODES).optional().default('agent'),
|
||||
model: z.string().optional(),
|
||||
autoExecuteTools: z.boolean().optional().default(true),
|
||||
timeout: z.number().optional().default(300000),
|
||||
@@ -100,6 +101,14 @@ export async function POST(req: NextRequest) {
|
||||
)
|
||||
}
|
||||
|
||||
// Transform mode to transport mode (same as client API)
|
||||
// build and agent both map to 'agent' on the backend
|
||||
const effectiveMode = parsed.mode === 'agent' ? 'build' : parsed.mode
|
||||
const transportMode = effectiveMode === 'build' ? 'agent' : effectiveMode
|
||||
|
||||
// Always generate a chatId - required for artifacts system to work with subagents
|
||||
const chatId = parsed.chatId || crypto.randomUUID()
|
||||
|
||||
const requestPayload = {
|
||||
message: parsed.message,
|
||||
workflowId: resolved.workflowId,
|
||||
@@ -107,17 +116,17 @@ export async function POST(req: NextRequest) {
|
||||
stream: true,
|
||||
streamToolCalls: true,
|
||||
model: selectedModel,
|
||||
mode: parsed.mode,
|
||||
mode: transportMode,
|
||||
messageId: crypto.randomUUID(),
|
||||
version: SIM_AGENT_VERSION,
|
||||
headless: true, // Enable cross-workflow operations via workflowId params
|
||||
...(parsed.chatId ? { chatId: parsed.chatId } : {}),
|
||||
chatId,
|
||||
}
|
||||
|
||||
const result = await orchestrateCopilotStream(requestPayload, {
|
||||
userId: auth.userId,
|
||||
workflowId: resolved.workflowId,
|
||||
chatId: parsed.chatId,
|
||||
chatId,
|
||||
autoExecuteTools: parsed.autoExecuteTools,
|
||||
timeout: parsed.timeout,
|
||||
interactive: false,
|
||||
@@ -127,7 +136,7 @@ export async function POST(req: NextRequest) {
|
||||
success: result.success,
|
||||
content: result.content,
|
||||
toolCalls: result.toolCalls,
|
||||
chatId: result.chatId,
|
||||
chatId: result.chatId || chatId, // Return the chatId for conversation continuity
|
||||
conversationId: result.conversationId,
|
||||
error: result.error,
|
||||
})
|
||||
|
||||
@@ -13,6 +13,21 @@ import { INTERRUPT_TOOL_SET, SUBAGENT_TOOL_SET } from '@/lib/copilot/orchestrato
|
||||
|
||||
const logger = createLogger('CopilotSseHandlers')
|
||||
|
||||
/**
|
||||
* Respond tools are internal to the copilot's subagent system.
|
||||
* They're used by subagents to signal completion and should NOT be executed by the sim side.
|
||||
* The copilot backend handles these internally.
|
||||
*/
|
||||
const RESPOND_TOOL_SET = new Set([
|
||||
'plan_respond',
|
||||
'edit_respond',
|
||||
'debug_respond',
|
||||
'info_respond',
|
||||
'research_respond',
|
||||
'deploy_respond',
|
||||
'superagent_respond',
|
||||
])
|
||||
|
||||
export type SSEHandler = (
|
||||
event: SSEEvent,
|
||||
context: StreamingContext,
|
||||
@@ -112,15 +127,26 @@ export const sseHandlers: Record<string, SSEHandler> = {
|
||||
const current = context.toolCalls.get(toolCallId)
|
||||
if (!current) return
|
||||
|
||||
const success = event.data?.success ?? event.data?.result?.success
|
||||
// Determine success: explicit success field, or if there's result data without explicit failure
|
||||
const hasExplicitSuccess = event.data?.success !== undefined || event.data?.result?.success !== undefined
|
||||
const explicitSuccess = event.data?.success ?? event.data?.result?.success
|
||||
const hasResultData = event.data?.result !== undefined || event.data?.data !== undefined
|
||||
const hasError = !!event.data?.error || !!event.data?.result?.error
|
||||
|
||||
// If explicitly set, use that; otherwise infer from data presence
|
||||
const success = hasExplicitSuccess ? !!explicitSuccess : (hasResultData && !hasError)
|
||||
|
||||
current.status = success ? 'success' : 'error'
|
||||
current.endTime = Date.now()
|
||||
if (event.data?.result || event.data?.data) {
|
||||
if (hasResultData) {
|
||||
current.result = {
|
||||
success: !!success,
|
||||
success,
|
||||
output: event.data?.result || event.data?.data,
|
||||
}
|
||||
}
|
||||
if (hasError) {
|
||||
current.error = event.data?.error || event.data?.result?.error
|
||||
}
|
||||
},
|
||||
tool_error: (event, context) => {
|
||||
const toolCallId = event.toolCallId || event.data?.id
|
||||
@@ -168,10 +194,17 @@ export const sseHandlers: Record<string, SSEHandler> = {
|
||||
|
||||
if (isPartial) return
|
||||
|
||||
// Subagent tools are executed by the copilot backend, not sim side
|
||||
if (SUBAGENT_TOOL_SET.has(toolName)) {
|
||||
return
|
||||
}
|
||||
|
||||
// Respond tools are internal to copilot's subagent system - skip execution
|
||||
// The copilot backend handles these internally to signal subagent completion
|
||||
if (RESPOND_TOOL_SET.has(toolName)) {
|
||||
return
|
||||
}
|
||||
|
||||
const isInterruptTool = INTERRUPT_TOOL_SET.has(toolName)
|
||||
const isInteractive = options.interactive === true
|
||||
|
||||
@@ -309,12 +342,21 @@ export const subAgentHandlers: Record<string, SSEHandler> = {
|
||||
params: args,
|
||||
startTime: Date.now(),
|
||||
}
|
||||
|
||||
// Store in both places - subAgentToolCalls for tracking and toolCalls for executeToolAndReport
|
||||
if (!context.subAgentToolCalls[parentToolCallId]) {
|
||||
context.subAgentToolCalls[parentToolCallId] = []
|
||||
}
|
||||
context.subAgentToolCalls[parentToolCallId].push(toolCall)
|
||||
context.toolCalls.set(toolCallId, toolCall)
|
||||
|
||||
if (isPartial) return
|
||||
|
||||
// Respond tools are internal to copilot's subagent system - skip execution
|
||||
if (RESPOND_TOOL_SET.has(toolName)) {
|
||||
return
|
||||
}
|
||||
|
||||
if (options.autoExecuteTools !== false) {
|
||||
await executeToolAndReport(toolCallId, context, execContext, options)
|
||||
}
|
||||
@@ -324,11 +366,41 @@ export const subAgentHandlers: Record<string, SSEHandler> = {
|
||||
if (!parentToolCallId) return
|
||||
const toolCallId = event.toolCallId || event.data?.id
|
||||
if (!toolCallId) return
|
||||
|
||||
// Update in subAgentToolCalls
|
||||
const toolCalls = context.subAgentToolCalls[parentToolCallId] || []
|
||||
const toolCall = toolCalls.find((tc) => tc.id === toolCallId)
|
||||
if (!toolCall) return
|
||||
toolCall.status = event.data?.success ? 'success' : 'error'
|
||||
toolCall.endTime = Date.now()
|
||||
const subAgentToolCall = toolCalls.find((tc) => tc.id === toolCallId)
|
||||
|
||||
// Also update in main toolCalls (where we added it for execution)
|
||||
const mainToolCall = context.toolCalls.get(toolCallId)
|
||||
|
||||
// Use same success inference logic as main handler
|
||||
const hasExplicitSuccess =
|
||||
event.data?.success !== undefined || event.data?.result?.success !== undefined
|
||||
const explicitSuccess = event.data?.success ?? event.data?.result?.success
|
||||
const hasResultData = event.data?.result !== undefined || event.data?.data !== undefined
|
||||
const hasError = !!event.data?.error || !!event.data?.result?.error
|
||||
const success = hasExplicitSuccess ? !!explicitSuccess : hasResultData && !hasError
|
||||
|
||||
const status = success ? 'success' : 'error'
|
||||
const endTime = Date.now()
|
||||
const result = hasResultData
|
||||
? { success, output: event.data?.result || event.data?.data }
|
||||
: undefined
|
||||
|
||||
if (subAgentToolCall) {
|
||||
subAgentToolCall.status = status
|
||||
subAgentToolCall.endTime = endTime
|
||||
if (result) subAgentToolCall.result = result
|
||||
if (hasError) subAgentToolCall.error = event.data?.error || event.data?.result?.error
|
||||
}
|
||||
|
||||
if (mainToolCall) {
|
||||
mainToolCall.status = status
|
||||
mainToolCall.endTime = endTime
|
||||
if (result) mainToolCall.result = result
|
||||
if (hasError) mainToolCall.error = event.data?.error || event.data?.result?.error
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -8,7 +8,10 @@ import { validateSelectorIds } from '@/lib/copilot/validation/selector-validator
|
||||
import type { PermissionGroupConfig } from '@/lib/permission-groups/types'
|
||||
import { getBlockOutputs } from '@/lib/workflows/blocks/block-outputs'
|
||||
import { extractAndPersistCustomTools } from '@/lib/workflows/persistence/custom-tools-persistence'
|
||||
import { loadWorkflowFromNormalizedTables } from '@/lib/workflows/persistence/utils'
|
||||
import {
|
||||
loadWorkflowFromNormalizedTables,
|
||||
saveWorkflowToNormalizedTables,
|
||||
} from '@/lib/workflows/persistence/utils'
|
||||
import { isValidKey } from '@/lib/workflows/sanitization/key-validation'
|
||||
import { validateWorkflowState } from '@/lib/workflows/sanitization/validation'
|
||||
import { buildCanonicalIndex, isCanonicalPair } from '@/lib/workflows/subblocks/visibility'
|
||||
@@ -3067,6 +3070,37 @@ export const editWorkflowServerTool: BaseServerTool<EditWorkflowParams, any> = {
|
||||
const skippedMessages =
|
||||
skippedItems.length > 0 ? skippedItems.map((item) => item.reason) : undefined
|
||||
|
||||
// Persist the workflow state to the database
|
||||
const finalWorkflowState = validation.sanitizedState || modifiedWorkflowState
|
||||
const workflowStateForDb = {
|
||||
blocks: finalWorkflowState.blocks,
|
||||
edges: finalWorkflowState.edges,
|
||||
loops: generateLoopBlocks(finalWorkflowState.blocks as any),
|
||||
parallels: generateParallelBlocks(finalWorkflowState.blocks as any),
|
||||
lastSaved: Date.now(),
|
||||
isDeployed: false,
|
||||
}
|
||||
|
||||
const saveResult = await saveWorkflowToNormalizedTables(workflowId, workflowStateForDb as any)
|
||||
if (!saveResult.success) {
|
||||
logger.error('Failed to persist workflow state to database', {
|
||||
workflowId,
|
||||
error: saveResult.error,
|
||||
})
|
||||
throw new Error(`Failed to save workflow: ${saveResult.error}`)
|
||||
}
|
||||
|
||||
// Update workflow's lastSynced timestamp
|
||||
await db
|
||||
.update(workflowTable)
|
||||
.set({
|
||||
lastSynced: new Date(),
|
||||
updatedAt: new Date(),
|
||||
})
|
||||
.where(eq(workflowTable.id, workflowId))
|
||||
|
||||
logger.info('Workflow state persisted to database', { workflowId })
|
||||
|
||||
// Return the modified workflow state for the client to convert to YAML if needed
|
||||
return {
|
||||
success: true,
|
||||
|
||||
Reference in New Issue
Block a user