mirror of
https://github.com/simstudioai/sim.git
synced 2026-01-29 16:58:11 -05:00
v3
This commit is contained in:
@@ -158,10 +158,12 @@ export async function POST(req: NextRequest) {
|
||||
}
|
||||
|
||||
// Build execution context that will be passed to Go and used for tool execution
|
||||
// Source is 'ui' since this route is called from the browser UI
|
||||
const executionContext = {
|
||||
userId: authenticatedUserId,
|
||||
workflowId: resolvedWorkflowId,
|
||||
workspaceId: resolvedWorkspaceId,
|
||||
source: 'ui' as const,
|
||||
}
|
||||
|
||||
logger.debug(`[${tracker.requestId}] Resolved execution context`, executionContext)
|
||||
@@ -686,6 +688,7 @@ export async function POST(req: NextRequest) {
|
||||
workspaceId:
|
||||
event.data.executionContext?.workspaceId || resolvedWorkspaceId,
|
||||
chatId: actualChatId,
|
||||
source: 'ui' as const,
|
||||
}
|
||||
handleToolCallEvent(
|
||||
{
|
||||
|
||||
@@ -44,10 +44,12 @@ export async function POST(req: NextRequest) {
|
||||
logger.info('Test copilot request', { query, userId, workflowId, workspaceId, stream })
|
||||
|
||||
// Build execution context
|
||||
// Source is 'headless' since this is a test/API endpoint without UI
|
||||
const executionContext = {
|
||||
userId,
|
||||
workflowId,
|
||||
workspaceId,
|
||||
source: 'headless' as const,
|
||||
}
|
||||
|
||||
// Build request payload for Go copilot
|
||||
@@ -119,6 +121,7 @@ export async function POST(req: NextRequest) {
|
||||
workflowId: event.data.executionContext?.workflowId || workflowId,
|
||||
workspaceId: event.data.executionContext?.workspaceId || workspaceId,
|
||||
chatId: undefined,
|
||||
source: 'headless' as const,
|
||||
}
|
||||
|
||||
handleToolCallEvent(
|
||||
|
||||
@@ -363,17 +363,38 @@ const TOOL_REGISTRY: Record<string, ToolRegistration> = {
|
||||
},
|
||||
}
|
||||
|
||||
/**
|
||||
* Tools that should only be auto-intercepted in headless mode.
|
||||
* In UI mode, the client tool handles these (e.g., to show diff review).
|
||||
*/
|
||||
const HEADLESS_ONLY_TOOLS = new Set(['edit_workflow'])
|
||||
|
||||
/**
|
||||
* List of all server-executed tool names.
|
||||
* Export this so clients know which tools NOT to execute locally.
|
||||
* Note: edit_workflow is excluded because it needs client-side diff review in UI mode.
|
||||
*/
|
||||
export const SERVER_EXECUTED_TOOLS = Object.keys(TOOL_REGISTRY)
|
||||
export const SERVER_EXECUTED_TOOLS = Object.keys(TOOL_REGISTRY).filter(
|
||||
(name) => !HEADLESS_ONLY_TOOLS.has(name)
|
||||
)
|
||||
|
||||
/**
|
||||
* Check if a tool is registered for server execution.
|
||||
* @param toolName - The tool name to check
|
||||
* @param source - Optional execution source. If 'ui', headless-only tools return false.
|
||||
*/
|
||||
export function isServerExecutedTool(toolName: string): boolean {
|
||||
return toolName in TOOL_REGISTRY
|
||||
export function isServerExecutedTool(
|
||||
toolName: string,
|
||||
source?: 'ui' | 'headless'
|
||||
): boolean {
|
||||
if (!(toolName in TOOL_REGISTRY)) {
|
||||
return false
|
||||
}
|
||||
// In UI mode, headless-only tools are NOT server-executed (client handles them)
|
||||
if (source === 'ui' && HEADLESS_ONLY_TOOLS.has(toolName)) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -182,10 +182,12 @@ export async function handleToolCallEvent(
|
||||
}
|
||||
|
||||
// Check if this tool should be executed server-side
|
||||
if (!isServerExecutedTool(event.name)) {
|
||||
// Pass source to handle headless-only tools (e.g., edit_workflow in UI mode is client-handled)
|
||||
if (!isServerExecutedTool(event.name, context.source)) {
|
||||
logger.debug('Tool not server-executed, client will handle', {
|
||||
toolCallId: event.id,
|
||||
toolName: event.name,
|
||||
source: context.source,
|
||||
})
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -21,6 +21,11 @@ export interface ToolResult<T = unknown> {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Execution source - whether the request came from UI or headless API.
|
||||
*/
|
||||
export type ExecutionSource = 'ui' | 'headless'
|
||||
|
||||
/**
|
||||
* Context passed to tool executors.
|
||||
*
|
||||
@@ -33,6 +38,8 @@ export interface ExecutionContext {
|
||||
workflowId?: string
|
||||
workspaceId?: string
|
||||
chatId?: string
|
||||
/** Whether request came from UI (with diff review) or headless API (direct save) */
|
||||
source?: ExecutionSource
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { Grid2x2, Grid2x2Check, Grid2x2X, Loader2, MinusCircle, XCircle } from 'lucide-react'
|
||||
import {
|
||||
BaseClientTool,
|
||||
@@ -5,15 +6,108 @@ import {
|
||||
ClientToolCallState,
|
||||
} from '@/lib/copilot/tools/client/base-tool'
|
||||
import { registerToolUIConfig } from '@/lib/copilot/tools/client/ui-config'
|
||||
import { ExecuteResponseSuccessSchema } from '@/lib/copilot/tools/shared/schemas'
|
||||
import { stripWorkflowDiffMarkers } from '@/lib/workflows/diff'
|
||||
import { sanitizeForCopilot } from '@/lib/workflows/sanitization/json-sanitizer'
|
||||
import { useWorkflowDiffStore } from '@/stores/workflow-diff/store'
|
||||
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
|
||||
import { mergeSubblockState } from '@/stores/workflows/utils'
|
||||
import { useWorkflowStore } from '@/stores/workflows/workflow/store'
|
||||
import type { WorkflowState } from '@/stores/workflows/workflow/types'
|
||||
|
||||
interface EditWorkflowOperation {
|
||||
operation_type: 'add' | 'edit' | 'delete'
|
||||
block_id: string
|
||||
params?: Record<string, any>
|
||||
}
|
||||
|
||||
interface EditWorkflowArgs {
|
||||
operations: EditWorkflowOperation[]
|
||||
workflowId: string
|
||||
currentUserWorkflow?: string
|
||||
}
|
||||
|
||||
export class EditWorkflowClientTool extends BaseClientTool {
|
||||
static readonly id = 'edit_workflow'
|
||||
private lastResult: any | undefined
|
||||
private hasExecuted = false
|
||||
private hasAppliedDiff = false
|
||||
private workflowId: string | undefined
|
||||
|
||||
constructor(toolCallId: string) {
|
||||
super(toolCallId, EditWorkflowClientTool.id, EditWorkflowClientTool.metadata)
|
||||
}
|
||||
|
||||
async markToolComplete(status: number, message?: any, data?: any): Promise<boolean> {
|
||||
const logger = createLogger('EditWorkflowClientTool')
|
||||
logger.info('markToolComplete payload', {
|
||||
toolCallId: this.toolCallId,
|
||||
toolName: this.name,
|
||||
status,
|
||||
message,
|
||||
data,
|
||||
})
|
||||
return super.markToolComplete(status, message, data)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get sanitized workflow JSON from a workflow state, merge subblocks, and sanitize for copilot
|
||||
*/
|
||||
private getSanitizedWorkflowJson(workflowState: any): string | undefined {
|
||||
const logger = createLogger('EditWorkflowClientTool')
|
||||
|
||||
if (!this.workflowId) {
|
||||
logger.warn('No workflowId available for getting sanitized workflow JSON')
|
||||
return undefined
|
||||
}
|
||||
|
||||
if (!workflowState) {
|
||||
logger.warn('No workflow state provided')
|
||||
return undefined
|
||||
}
|
||||
|
||||
try {
|
||||
// Normalize required properties
|
||||
if (!workflowState.loops) workflowState.loops = {}
|
||||
if (!workflowState.parallels) workflowState.parallels = {}
|
||||
if (!workflowState.edges) workflowState.edges = []
|
||||
if (!workflowState.blocks) workflowState.blocks = {}
|
||||
|
||||
// Merge latest subblock values so edits are reflected
|
||||
let mergedState = workflowState
|
||||
if (workflowState.blocks) {
|
||||
mergedState = {
|
||||
...workflowState,
|
||||
blocks: mergeSubblockState(workflowState.blocks, this.workflowId as any),
|
||||
}
|
||||
}
|
||||
|
||||
// Sanitize workflow state for copilot (remove UI-specific data)
|
||||
const sanitizedState = sanitizeForCopilot(mergedState)
|
||||
|
||||
// Convert to JSON string for transport
|
||||
const workflowJson = JSON.stringify(sanitizedState, null, 2)
|
||||
|
||||
return workflowJson
|
||||
} catch (error) {
|
||||
logger.warn('Failed to get sanitized workflow JSON', {
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
})
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
|
||||
private getCurrentWorkflowJsonSafe(logger: ReturnType<typeof createLogger>): string | undefined {
|
||||
try {
|
||||
const workflowStore = useWorkflowStore.getState()
|
||||
const currentState = workflowStore.getWorkflowState()
|
||||
return this.getSanitizedWorkflowJson(currentState)
|
||||
} catch {
|
||||
logger.warn('Failed to get current workflow JSON for error response')
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
|
||||
static readonly metadata: BaseClientToolMetadata = {
|
||||
displayNames: {
|
||||
[ClientToolCallState.generating]: { text: 'Editing your workflow', icon: Loader2 },
|
||||
@@ -56,9 +150,163 @@ export class EditWorkflowClientTool extends BaseClientTool {
|
||||
},
|
||||
}
|
||||
|
||||
// Executed server-side via handleToolCallEvent in stream-handler.ts
|
||||
// Client tool provides UI metadata only for rendering tool call cards
|
||||
// The server applies workflow changes directly in headless mode
|
||||
handleAccept(): void {
|
||||
const logger = createLogger('EditWorkflowClientTool')
|
||||
logger.info('handleAccept called', { toolCallId: this.toolCallId, state: this.getState() })
|
||||
// The actual accept is handled by useWorkflowDiffStore.acceptChanges()
|
||||
// This just updates the tool state
|
||||
this.setState(ClientToolCallState.success)
|
||||
}
|
||||
|
||||
handleReject(): void {
|
||||
const logger = createLogger('EditWorkflowClientTool')
|
||||
logger.info('handleReject called', { toolCallId: this.toolCallId, state: this.getState() })
|
||||
// Tool was already marked complete in execute() - this is just for UI state
|
||||
this.setState(ClientToolCallState.rejected)
|
||||
}
|
||||
|
||||
async execute(args?: EditWorkflowArgs): Promise<void> {
|
||||
const logger = createLogger('EditWorkflowClientTool')
|
||||
|
||||
if (this.hasExecuted) {
|
||||
logger.info('execute skipped (already executed)', { toolCallId: this.toolCallId })
|
||||
return
|
||||
}
|
||||
|
||||
this.hasExecuted = true
|
||||
this.setState(ClientToolCallState.executing)
|
||||
|
||||
// Get workflow ID from args or active workflow
|
||||
const workflowId = args?.workflowId || useWorkflowRegistry.getState().activeWorkflowId
|
||||
if (!workflowId) {
|
||||
this.setState(ClientToolCallState.error)
|
||||
await this.markToolComplete(400, 'No workflow ID provided or active')
|
||||
return
|
||||
}
|
||||
this.workflowId = workflowId
|
||||
|
||||
logger.info('execute starting', {
|
||||
toolCallId: this.toolCallId,
|
||||
workflowId,
|
||||
operationCount: args?.operations?.length,
|
||||
})
|
||||
|
||||
try {
|
||||
// Get current workflow state to send to server
|
||||
const workflowStore = useWorkflowStore.getState()
|
||||
const fullState = workflowStore.getWorkflowState()
|
||||
const mergedBlocks = mergeSubblockState(fullState.blocks, workflowId as any)
|
||||
const payloadState = stripWorkflowDiffMarkers({
|
||||
...fullState,
|
||||
blocks: mergedBlocks,
|
||||
edges: fullState.edges || [],
|
||||
loops: fullState.loops || {},
|
||||
parallels: fullState.parallels || {},
|
||||
})
|
||||
|
||||
// Call server to execute the tool (without saving to DB in UI mode)
|
||||
const response = await fetch('/api/copilot/execute-copilot-server-tool', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
toolName: 'edit_workflow',
|
||||
toolCallId: this.toolCallId,
|
||||
args: {
|
||||
...args,
|
||||
workflowId,
|
||||
currentUserWorkflow: JSON.stringify(payloadState),
|
||||
},
|
||||
}),
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
logger.error('Server execution failed', { status: response.status, error: errorText })
|
||||
this.setState(ClientToolCallState.error)
|
||||
const currentWorkflowJson = this.getCurrentWorkflowJsonSafe(logger)
|
||||
await this.markToolComplete(
|
||||
response.status,
|
||||
errorText || 'Server execution failed',
|
||||
currentWorkflowJson ? { userWorkflow: currentWorkflowJson } : undefined
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
const result = await response.json()
|
||||
logger.info('Server execution result', {
|
||||
success: result.success,
|
||||
hasWorkflowState: !!result.data?.workflowState,
|
||||
})
|
||||
|
||||
// Validate result
|
||||
const parseResult = ExecuteResponseSuccessSchema.safeParse(result)
|
||||
if (!parseResult.success || !result.data?.workflowState) {
|
||||
logger.error('Invalid response from server', { errors: parseResult.error?.errors })
|
||||
this.setState(ClientToolCallState.error)
|
||||
const currentWorkflowJson = this.getCurrentWorkflowJsonSafe(logger)
|
||||
await this.markToolComplete(
|
||||
500,
|
||||
'Invalid response from server',
|
||||
currentWorkflowJson ? { userWorkflow: currentWorkflowJson } : undefined
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
this.lastResult = result.data
|
||||
|
||||
// Apply the proposed state to the diff store for review
|
||||
if (!this.hasAppliedDiff) {
|
||||
const diffStore = useWorkflowDiffStore.getState()
|
||||
// setProposedChanges applies the state optimistically to the workflow store
|
||||
// and sets up diff markers for visual feedback
|
||||
await diffStore.setProposedChanges(result.data.workflowState)
|
||||
logger.info('Diff proposed changes set for edit_workflow with direct workflow state')
|
||||
this.hasAppliedDiff = true
|
||||
}
|
||||
|
||||
// Read back the applied state from the workflow store
|
||||
const actualDiffWorkflow = workflowStore.getWorkflowState()
|
||||
|
||||
if (!actualDiffWorkflow) {
|
||||
this.setState(ClientToolCallState.error)
|
||||
const currentWorkflowJson = this.getCurrentWorkflowJsonSafe(logger)
|
||||
await this.markToolComplete(
|
||||
500,
|
||||
'Failed to retrieve workflow state after applying changes',
|
||||
currentWorkflowJson ? { userWorkflow: currentWorkflowJson } : undefined
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
// Set state to review so user can accept/reject
|
||||
this.setState(ClientToolCallState.review)
|
||||
|
||||
// Mark tool complete with success - the workflow state is ready for review
|
||||
const sanitizedJson = this.getSanitizedWorkflowJson(actualDiffWorkflow)
|
||||
await this.markToolComplete(
|
||||
200,
|
||||
result.data.inputValidationMessage || result.data.skippedItemsMessage || undefined,
|
||||
sanitizedJson ? { userWorkflow: sanitizedJson } : undefined
|
||||
)
|
||||
|
||||
logger.info('execute completed successfully - awaiting user review', {
|
||||
toolCallId: this.toolCallId,
|
||||
state: this.getState(),
|
||||
})
|
||||
} catch (error) {
|
||||
logger.error('execute failed with exception', {
|
||||
toolCallId: this.toolCallId,
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
})
|
||||
this.setState(ClientToolCallState.error)
|
||||
const currentWorkflowJson = this.getCurrentWorkflowJsonSafe(logger)
|
||||
await this.markToolComplete(
|
||||
500,
|
||||
error instanceof Error ? error.message : 'Unknown error',
|
||||
currentWorkflowJson ? { userWorkflow: currentWorkflowJson } : undefined
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Register UI config at module load
|
||||
|
||||
@@ -2631,7 +2631,7 @@ export const editWorkflowServerTool: BaseServerTool<EditWorkflowParams, any> = {
|
||||
name: 'edit_workflow',
|
||||
async execute(
|
||||
params: EditWorkflowParams,
|
||||
context?: { userId: string; workflowId?: string }
|
||||
context?: { userId: string; workflowId?: string; workspaceId?: string; source?: 'ui' | 'headless' }
|
||||
): Promise<any> {
|
||||
const logger = createLogger('EditWorkflowServerTool')
|
||||
const { operations, currentUserWorkflow } = params
|
||||
@@ -2753,68 +2753,83 @@ export const editWorkflowServerTool: BaseServerTool<EditWorkflowParams, any> = {
|
||||
const finalWorkflowState = validation.sanitizedState || modifiedWorkflowState
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
// PERSIST THE CHANGES TO THE DATABASE
|
||||
// This is critical for headless mode and ensures changes are saved
|
||||
// PERSIST THE CHANGES TO THE DATABASE (headless mode only)
|
||||
// In UI mode, we return the proposed state for client-side diff review.
|
||||
// The client will persist after user accepts the changes.
|
||||
// In headless mode, we save directly since there's no UI to review.
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
const workflowStateForPersistence = {
|
||||
blocks: finalWorkflowState.blocks,
|
||||
edges: finalWorkflowState.edges,
|
||||
loops: finalWorkflowState.loops || {},
|
||||
parallels: finalWorkflowState.parallels || {},
|
||||
lastSaved: Date.now(),
|
||||
}
|
||||
const isHeadlessMode = context?.source !== 'ui'
|
||||
|
||||
const saveResult = await saveWorkflowToNormalizedTables(workflowId, workflowStateForPersistence)
|
||||
if (isHeadlessMode) {
|
||||
const workflowStateForPersistence = {
|
||||
blocks: finalWorkflowState.blocks,
|
||||
edges: finalWorkflowState.edges,
|
||||
loops: finalWorkflowState.loops || {},
|
||||
parallels: finalWorkflowState.parallels || {},
|
||||
lastSaved: Date.now(),
|
||||
}
|
||||
|
||||
if (!saveResult.success) {
|
||||
logger.error('Failed to persist workflow changes to database', {
|
||||
workflowId,
|
||||
error: saveResult.error,
|
||||
const saveResult = await saveWorkflowToNormalizedTables(workflowId, workflowStateForPersistence)
|
||||
|
||||
if (!saveResult.success) {
|
||||
logger.error('Failed to persist workflow changes 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))
|
||||
|
||||
// Notify socket server so connected clients can refresh
|
||||
// This uses the copilot-specific endpoint to trigger UI refresh
|
||||
try {
|
||||
const socketUrl = process.env.SOCKET_SERVER_URL || 'http://localhost:3002'
|
||||
const operationsSummary = operations
|
||||
.map((op: any) => `${op.operation_type} ${op.block_id || 'block'}`)
|
||||
.slice(0, 3)
|
||||
.join(', ')
|
||||
const description = `Applied ${operations.length} operation(s): ${operationsSummary}${operations.length > 3 ? '...' : ''}`
|
||||
|
||||
await fetch(`${socketUrl}/api/copilot-workflow-edit`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ workflowId, description }),
|
||||
}).catch((err) => {
|
||||
logger.warn('Failed to notify socket server about copilot edit', { error: err.message })
|
||||
})
|
||||
} catch (notifyError) {
|
||||
// Non-fatal - log and continue
|
||||
logger.warn('Error notifying socket server', { error: notifyError })
|
||||
}
|
||||
|
||||
logger.info('edit_workflow successfully applied and persisted operations (headless mode)', {
|
||||
operationCount: operations.length,
|
||||
blocksCount: Object.keys(finalWorkflowState.blocks).length,
|
||||
edgesCount: finalWorkflowState.edges.length,
|
||||
inputValidationErrors: validationErrors.length,
|
||||
skippedItemsCount: skippedItems.length,
|
||||
schemaValidationErrors: validation.errors.length,
|
||||
validationWarnings: validation.warnings.length,
|
||||
})
|
||||
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(),
|
||||
} else {
|
||||
// UI mode - don't persist, let client handle after user accepts
|
||||
logger.info('edit_workflow returning proposed state for UI review (not persisted)', {
|
||||
operationCount: operations.length,
|
||||
blocksCount: Object.keys(finalWorkflowState.blocks).length,
|
||||
edgesCount: finalWorkflowState.edges.length,
|
||||
inputValidationErrors: validationErrors.length,
|
||||
skippedItemsCount: skippedItems.length,
|
||||
})
|
||||
.where(eq(workflowTable.id, workflowId))
|
||||
|
||||
// Notify socket server so connected clients can refresh
|
||||
// This uses the copilot-specific endpoint to trigger UI refresh
|
||||
try {
|
||||
const socketUrl = process.env.SOCKET_SERVER_URL || 'http://localhost:3002'
|
||||
const operationsSummary = operations
|
||||
.map((op: any) => `${op.operation_type} ${op.block_id || 'block'}`)
|
||||
.slice(0, 3)
|
||||
.join(', ')
|
||||
const description = `Applied ${operations.length} operation(s): ${operationsSummary}${operations.length > 3 ? '...' : ''}`
|
||||
|
||||
await fetch(`${socketUrl}/api/copilot-workflow-edit`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ workflowId, description }),
|
||||
}).catch((err) => {
|
||||
logger.warn('Failed to notify socket server about copilot edit', { error: err.message })
|
||||
})
|
||||
} catch (notifyError) {
|
||||
// Non-fatal - log and continue
|
||||
logger.warn('Error notifying socket server', { error: notifyError })
|
||||
}
|
||||
|
||||
logger.info('edit_workflow successfully applied and persisted operations', {
|
||||
operationCount: operations.length,
|
||||
blocksCount: Object.keys(finalWorkflowState.blocks).length,
|
||||
edgesCount: finalWorkflowState.edges.length,
|
||||
inputValidationErrors: validationErrors.length,
|
||||
skippedItemsCount: skippedItems.length,
|
||||
schemaValidationErrors: validation.errors.length,
|
||||
validationWarnings: validation.warnings.length,
|
||||
})
|
||||
|
||||
// Format validation errors for LLM feedback
|
||||
const inputErrors =
|
||||
validationErrors.length > 0
|
||||
|
||||
Reference in New Issue
Block a user