feat(code): added complex code execution for agents' custom tools, added envvar resolution/styling/dropdown for agent custom tool code modal

This commit is contained in:
Waleed Latif
2025-03-01 17:03:03 -08:00
parent 9d55ec4e5c
commit a345b2ffab
8 changed files with 526 additions and 132 deletions
+39 -8
View File
@@ -5,6 +5,40 @@ import { Script, createContext } from 'vm'
export const dynamic = 'force-dynamic' // Disable static optimization
export const runtime = 'nodejs' // Use Node.js runtime
/**
* Resolves environment variables and tags in code
* @param code - Code with variables
* @param params - Parameters that may contain variable values
* @param envVars - Environment variables from the workflow
* @returns Resolved code
*/
function resolveCodeVariables(
code: string,
params: Record<string, any>,
envVars: Record<string, string> = {}
): string {
let resolvedCode = code
// Resolve environment variables with {{var_name}} syntax
const envVarMatches = resolvedCode.match(/\{\{([^}]+)\}\}/g) || []
for (const match of envVarMatches) {
const varName = match.slice(2, -2).trim()
// Priority: 1. Environment variables from workflow, 2. Params, 3. process.env
const varValue = envVars[varName] || params[varName] || process.env[varName] || ''
resolvedCode = resolvedCode.replace(match, varValue)
}
// Resolve tags with <tag_name> syntax
const tagMatches = resolvedCode.match(/<([^>]+)>/g) || []
for (const match of tagMatches) {
const tagName = match.slice(1, -1).trim()
const tagValue = params[tagName] || ''
resolvedCode = resolvedCode.replace(match, tagValue)
}
return resolvedCode
}
export async function POST(req: NextRequest) {
const startTime = Date.now()
let stdout = ''
@@ -12,18 +46,15 @@ export async function POST(req: NextRequest) {
try {
const body = await req.json()
const { code, params = {}, timeout = 3000 } = body
const { code, params = {}, timeout = 3000, envVars = {} } = body
// Check if code contains unresolved template variables
if (code.includes('<') && code.includes('>')) {
throw new Error(
'Code contains unresolved template variables. Please ensure all variables are resolved before execution.'
)
}
// Resolve variables in the code with workflow environment variables
const resolvedCode = resolveCodeVariables(code, params, envVars)
// Create a secure context with console logging
const context = createContext({
params,
environmentVariables: envVars, // Make environment variables available in the context
console: {
log: (...args: any[]) => {
const logMessage =
@@ -46,7 +77,7 @@ export async function POST(req: NextRequest) {
const script = new Script(`
(async () => {
try {
${code}
${resolvedCode}
} catch (error) {
console.error(error);
throw error;
@@ -14,6 +14,8 @@ interface CodeEditorProps {
placeholder?: string
className?: string
minHeight?: string
highlightVariables?: boolean
onKeyDown?: (e: React.KeyboardEvent) => void
}
export function CodeEditor({
@@ -23,6 +25,8 @@ export function CodeEditor({
placeholder = '',
className = '',
minHeight = '360px',
highlightVariables = true,
onKeyDown,
}: CodeEditorProps) {
const [code, setCode] = useState(value)
const [visualLineHeights, setVisualLineHeights] = useState<number[]>([])
@@ -103,6 +107,38 @@ export function CodeEditor({
return numbers
}
// Custom highlighter that highlights environment variables and tags
const customHighlight = (code: string) => {
if (!highlightVariables || language !== 'javascript') {
// Use default Prism highlighting for non-JS or when variable highlighting is off
return highlight(code, languages[language], language)
}
// First, get the default Prism highlighting
let highlighted = highlight(code, languages[language], language)
// Then, highlight environment variables with {{var_name}} syntax in blue
if (highlighted.includes('{{')) {
highlighted = highlighted.replace(
/\{\{([^}]+)\}\}/g,
'<span class="text-blue-500">{{$1}}</span>'
)
}
// Also highlight tags with <tag_name> syntax in blue
if (highlighted.includes('<') && !language.includes('html')) {
highlighted = highlighted.replace(/<([^>\s/]+)>/g, (match, group) => {
// Avoid replacing HTML tags in comments
if (match.startsWith('<!--') || match.includes('</')) {
return match
}
return `<span class="text-blue-500">&lt;${group}&gt;</span>`
})
}
return highlighted
}
return (
<div
className={cn(
@@ -159,7 +195,8 @@ export function CodeEditor({
onChange(newCode)
}
}}
highlight={(code) => highlight(code, languages[language], language)}
onKeyDown={onKeyDown}
highlight={(code) => customHighlight(code)}
padding={12}
style={{
fontFamily: 'inherit',
@@ -1,4 +1,4 @@
import { useEffect, useMemo, useState } from 'react'
import { useEffect, useMemo, useRef, useState } from 'react'
import { Code, FileJson, X } from 'lucide-react'
import { Button } from '@/components/ui/button'
import {
@@ -9,7 +9,9 @@ import {
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog'
import { EnvVarDropdown, checkEnvVarTrigger } from '@/components/ui/env-var-dropdown'
import { Label } from '@/components/ui/label'
import { TagDropdown, checkTagTrigger } from '@/components/ui/tag-dropdown'
import { cn } from '@/lib/utils'
import { useCustomToolsStore } from '@/stores/custom-tools/store'
import { CodeEditor } from './code-editor'
@@ -52,6 +54,16 @@ export function CustomToolModal({
const [isEditing, setIsEditing] = useState(false)
const [toolId, setToolId] = useState<string | undefined>(undefined)
// Environment variables and tags dropdown state
const [showEnvVars, setShowEnvVars] = useState(false)
const [showTags, setShowTags] = useState(false)
const [searchTerm, setSearchTerm] = useState('')
const [cursorPosition, setCursorPosition] = useState(0)
const codeEditorRef = useRef<HTMLDivElement>(null)
const [activeSourceBlockId, setActiveSourceBlockId] = useState<string | null>(null)
// Add state for dropdown positioning
const [dropdownPosition, setDropdownPosition] = useState({ top: 0, left: 0 })
const addTool = useCustomToolsStore((state) => state.addTool)
const updateTool = useCustomToolsStore((state) => state.updateTool)
@@ -240,6 +252,78 @@ export function CustomToolModal({
if (codeError) {
setCodeError(null)
}
// Check for environment variables and tags
const textarea = codeEditorRef.current?.querySelector('textarea')
if (textarea) {
const pos = textarea.selectionStart
setCursorPosition(pos)
// Calculate cursor position for dropdowns
const textBeforeCursor = value.substring(0, pos)
const lines = textBeforeCursor.split('\n')
const currentLine = lines.length
const currentCol = lines[lines.length - 1].length
// Find position of cursor in the editor
try {
if (codeEditorRef.current) {
const editorRect = codeEditorRef.current.getBoundingClientRect()
const lineHeight = 21 // Same as in CodeEditor
// Calculate approximate position
const top = currentLine * lineHeight + 5
const left = Math.min(currentCol * 8, editorRect.width - 260) // Prevent dropdown from going off-screen
setDropdownPosition({ top, left })
}
} catch (error) {
console.error('Error calculating cursor position:', error)
}
// Check if we should show the environment variables dropdown
const envVarTrigger = checkEnvVarTrigger(value, pos)
setShowEnvVars(envVarTrigger.show)
setSearchTerm(envVarTrigger.show ? envVarTrigger.searchTerm : '')
// Check if we should show the tags dropdown
const tagTrigger = checkTagTrigger(value, pos)
setShowTags(tagTrigger.show)
if (!tagTrigger.show) {
setActiveSourceBlockId(null)
}
}
}
// Handle environment variable selection
const handleEnvVarSelect = (newValue: string) => {
setFunctionCode(newValue)
setShowEnvVars(false)
}
// Handle tag selection
const handleTagSelect = (newValue: string) => {
setFunctionCode(newValue)
setShowTags(false)
setActiveSourceBlockId(null)
}
// Handle key press events
const handleKeyDown = (e: React.KeyboardEvent) => {
// Only handle Escape directly if dropdowns aren't visible
// Otherwise, let the dropdowns handle their own keyboard events
if (e.key === 'Escape' && !showEnvVars && !showTags) {
setShowEnvVars(false)
setShowTags(false)
}
// Don't handle other keys when dropdowns are visible
if (showEnvVars || showTags) {
if (['ArrowDown', 'ArrowUp', 'Enter'].includes(e.key)) {
e.preventDefault()
e.stopPropagation()
}
}
}
const navigationItems = [
@@ -358,14 +442,61 @@ export function CustomToolModal({
<span className="text-sm text-red-600 ml-4 flex-shrink-0">{codeError}</span>
)}
</div>
<CodeEditor
value={functionCode}
onChange={handleFunctionCodeChange}
language="javascript"
placeholder={`// This code will be executed when the tool is called`}
minHeight="340px"
className={cn(codeError ? 'border-red-500' : '')}
/>
<div ref={codeEditorRef} className="relative">
<CodeEditor
value={functionCode}
onChange={handleFunctionCodeChange}
language="javascript"
placeholder={`// This code will be executed when the tool is called. You can use environment variables with {{VARIABLE_NAME}}.`}
minHeight="340px"
className={cn(codeError ? 'border-red-500' : '')}
highlightVariables={true}
onKeyDown={handleKeyDown}
/>
{/* Environment variables dropdown */}
{showEnvVars && (
<EnvVarDropdown
visible={showEnvVars}
onSelect={handleEnvVarSelect}
searchTerm={searchTerm}
inputValue={functionCode}
cursorPosition={cursorPosition}
onClose={() => {
setShowEnvVars(false)
setSearchTerm('')
}}
className="w-64"
style={{
position: 'absolute',
top: `${dropdownPosition.top}px`,
left: `${dropdownPosition.left}px`,
}}
/>
)}
{/* Tags dropdown */}
{showTags && (
<TagDropdown
visible={showTags}
onSelect={handleTagSelect}
blockId=""
activeSourceBlockId={activeSourceBlockId}
inputValue={functionCode}
cursorPosition={cursorPosition}
onClose={() => {
setShowTags(false)
setActiveSourceBlockId(null)
}}
className="w-64"
style={{
position: 'absolute',
top: `${dropdownPosition.top}px`,
left: `${dropdownPosition.left}px`,
}}
/>
)}
</div>
<div className="h-6"></div>
</div>
</div>
+32 -26
View File
@@ -10,6 +10,7 @@ interface EnvVarDropdownProps {
inputValue: string
cursorPosition: number
onClose?: () => void
style?: React.CSSProperties
}
export const EnvVarDropdown: React.FC<EnvVarDropdownProps> = ({
@@ -20,6 +21,7 @@ export const EnvVarDropdown: React.FC<EnvVarDropdownProps> = ({
inputValue,
cursorPosition,
onClose,
style,
}) => {
const envVars = useEnvironmentStore((state) => Object.keys(state.variables))
const [selectedIndex, setSelectedIndex] = useState(0)
@@ -57,35 +59,38 @@ export const EnvVarDropdown: React.FC<EnvVarDropdownProps> = ({
onClose?.()
}
// Handle keyboard navigation
const handleKeyDown = (e: KeyboardEvent) => {
if (!visible || filteredEnvVars.length === 0) return
switch (e.key) {
case 'ArrowDown':
e.preventDefault()
setSelectedIndex((prev) => (prev < filteredEnvVars.length - 1 ? prev + 1 : prev))
break
case 'ArrowUp':
e.preventDefault()
setSelectedIndex((prev) => (prev > 0 ? prev - 1 : prev))
break
case 'Enter':
e.preventDefault()
handleEnvVarSelect(filteredEnvVars[selectedIndex])
break
case 'Escape':
e.preventDefault()
onClose?.()
break
}
}
// Add and remove keyboard event listener
useEffect(() => {
if (visible) {
window.addEventListener('keydown', handleKeyDown)
return () => window.removeEventListener('keydown', handleKeyDown)
const handleKeyboardEvent = (e: KeyboardEvent) => {
if (!filteredEnvVars.length) return
switch (e.key) {
case 'ArrowDown':
e.preventDefault()
e.stopPropagation()
setSelectedIndex((prev) => (prev < filteredEnvVars.length - 1 ? prev + 1 : prev))
break
case 'ArrowUp':
e.preventDefault()
e.stopPropagation()
setSelectedIndex((prev) => (prev > 0 ? prev - 1 : prev))
break
case 'Enter':
e.preventDefault()
e.stopPropagation()
handleEnvVarSelect(filteredEnvVars[selectedIndex])
break
case 'Escape':
e.preventDefault()
e.stopPropagation()
onClose?.()
break
}
}
window.addEventListener('keydown', handleKeyboardEvent, true)
return () => window.removeEventListener('keydown', handleKeyboardEvent, true)
}
}, [visible, selectedIndex, filteredEnvVars])
@@ -97,6 +102,7 @@ export const EnvVarDropdown: React.FC<EnvVarDropdownProps> = ({
'absolute z-[9999] w-full mt-1 overflow-hidden bg-popover rounded-md border shadow-md',
className
)}
style={style}
>
{filteredEnvVars.length === 0 ? (
<div className="px-3 py-2 text-sm text-muted-foreground">
+32 -26
View File
@@ -27,6 +27,7 @@ interface TagDropdownProps {
inputValue: string
cursorPosition: number
onClose?: () => void
style?: React.CSSProperties
}
export const TagDropdown: React.FC<TagDropdownProps> = ({
@@ -38,6 +39,7 @@ export const TagDropdown: React.FC<TagDropdownProps> = ({
inputValue,
cursorPosition,
onClose,
style,
}) => {
const [selectedIndex, setSelectedIndex] = useState(0)
@@ -198,35 +200,38 @@ export const TagDropdown: React.FC<TagDropdownProps> = ({
onClose?.()
}
// Handle keyboard navigation
const handleKeyDown = (e: KeyboardEvent) => {
if (!visible || filteredTags.length === 0) return
switch (e.key) {
case 'ArrowDown':
e.preventDefault()
setSelectedIndex((prev) => (prev < filteredTags.length - 1 ? prev + 1 : prev))
break
case 'ArrowUp':
e.preventDefault()
setSelectedIndex((prev) => (prev > 0 ? prev - 1 : prev))
break
case 'Enter':
e.preventDefault()
handleTagSelect(filteredTags[selectedIndex])
break
case 'Escape':
e.preventDefault()
onClose?.()
break
}
}
// Add and remove keyboard event listener
useEffect(() => {
if (visible) {
window.addEventListener('keydown', handleKeyDown)
return () => window.removeEventListener('keydown', handleKeyDown)
const handleKeyboardEvent = (e: KeyboardEvent) => {
if (!filteredTags.length) return
switch (e.key) {
case 'ArrowDown':
e.preventDefault()
e.stopPropagation()
setSelectedIndex((prev) => (prev < filteredTags.length - 1 ? prev + 1 : prev))
break
case 'ArrowUp':
e.preventDefault()
e.stopPropagation()
setSelectedIndex((prev) => (prev > 0 ? prev - 1 : prev))
break
case 'Enter':
e.preventDefault()
e.stopPropagation()
handleTagSelect(filteredTags[selectedIndex])
break
case 'Escape':
e.preventDefault()
e.stopPropagation()
onClose?.()
break
}
}
window.addEventListener('keydown', handleKeyboardEvent, true)
return () => window.removeEventListener('keydown', handleKeyboardEvent, true)
}
}, [visible, selectedIndex, filteredTags])
@@ -239,6 +244,7 @@ export const TagDropdown: React.FC<TagDropdownProps> = ({
'absolute z-[9999] w-full mt-1 overflow-hidden bg-popover rounded-md border shadow-md',
className
)}
style={style}
>
<div className="py-1">
{filteredTags.length === 0 ? (
+129 -62
View File
@@ -37,6 +37,62 @@ export interface BlockHandler {
): Promise<BlockOutput>
}
/**
* Shared helper for executing code with WebContainer and VM fallback
* @param code - The code to execute
* @param params - Parameters to pass to the code
* @param timeout - Execution timeout in milliseconds
* @returns Execution result
*/
async function executeCodeWithFallback(
code: string,
params: Record<string, any> = {},
timeout: number = 5000
): Promise<{ success: boolean; output: any; error?: string }> {
// Only try WebContainer in browser environment with direct execution
const isBrowser = typeof window !== 'undefined'
if (isBrowser && window.crossOriginIsolated) {
try {
// Dynamically import WebContainer to prevent server-side import
const { executeCode } = await import('@/lib/webcontainer')
// Execute directly in the browser
const result = await executeCode(code, params, timeout)
if (!result.success) {
console.warn(`WebContainer API execution failed: ${result.error}`)
throw new Error(result.error || `WebContainer execution failed with no error message`)
}
return { success: true, output: result.output }
} catch (error: any) {
console.warn('WebContainer execution failed, falling back to VM:', error)
console.error('WebContainer error details:', {
name: error.name,
message: error.message,
stack: error.stack,
})
}
}
// Fall back to VM execution if WebContainer fails or not available
try {
const vmResult = await executeTool('function_execute', { code, ...params }, true)
if (!vmResult.success) {
throw new Error(vmResult.error || `Function execution failed with no error message`)
}
return { success: true, output: vmResult.output }
} catch (vmError: any) {
return {
success: false,
output: null,
error: `Function execution failed: ${vmError.message}`,
}
}
}
/**
* Handler for Agent blocks that process LLM requests with optional tools.
*/
@@ -68,31 +124,72 @@ export class AgentBlockHandler implements BlockHandler {
// Format tools for provider API
const formattedTools = Array.isArray(inputs.tools)
? inputs.tools
.map((tool: any) => {
// Handle custom tools
if (tool.type === 'custom-tool' && tool.schema) {
return {
id: `custom_${tool.title}`,
name: tool.schema.function.name,
description: tool.schema.function.description || '',
params: tool.params || {},
parameters: {
type: tool.schema.function.parameters.type,
properties: tool.schema.function.parameters.properties,
required: tool.schema.function.parameters.required || [],
},
}
}
? (
await Promise.all(
inputs.tools.map(async (tool: any) => {
// Handle custom tools
if (tool.type === 'custom-tool' && tool.schema) {
// Add function execution capability to custom tools with code
if (tool.code) {
// Store the tool's code and make it available for execution
const toolName = tool.schema.function.name
const params = tool.params || {}
// Handle regular block tools with operation selection
return transformBlockTool(tool, {
selectedOperation: tool.operation,
getAllBlocks,
getTool,
// Create a tool that can execute the code
return {
id: `custom_${tool.title}`,
name: toolName,
description: tool.schema.function.description || '',
params: params,
parameters: {
type: tool.schema.function.parameters.type,
properties: tool.schema.function.parameters.properties,
required: tool.schema.function.parameters.required || [],
},
executeFunction: async (callParams: Record<string, any>) => {
try {
// Execute the code with WebContainer fallback
const result = await executeCodeWithFallback(
tool.code,
{ ...params, ...callParams },
tool.timeout || 5000
)
if (!result.success) {
throw new Error(result.error || 'Function execution failed')
}
return result.output
} catch (error: any) {
console.error(`Error executing custom tool ${toolName}:`, error)
throw new Error(`Error in ${toolName}: ${error.message}`)
}
},
}
}
return {
id: `custom_${tool.title}`,
name: tool.schema.function.name,
description: tool.schema.function.description || '',
params: tool.params || {},
parameters: {
type: tool.schema.function.parameters.type,
properties: tool.schema.function.parameters.properties,
required: tool.schema.function.parameters.required || [],
},
}
}
// Handle regular block tools with operation selection
return transformBlockTool(tool, {
selectedOperation: tool.operation,
getAllBlocks,
getTool,
})
})
})
.filter((t): t is NonNullable<typeof t> => t !== null)
)
).filter((t: any): t is NonNullable<typeof t> => t !== null)
: []
// Ensure context is properly formatted for the provider
@@ -460,49 +557,19 @@ export class FunctionBlockHandler implements BlockHandler {
inputs: Record<string, any>,
context: ExecutionContext
): Promise<BlockOutput> {
// Only try WebContainer in browser environment with direct execution
const isBrowser = typeof window !== 'undefined'
if (isBrowser && window.crossOriginIsolated) {
try {
// Dynamically import WebContainer to prevent server-side import
const { executeCode } = await import('@/lib/webcontainer')
// Prepare code for execution
const codeContent = Array.isArray(inputs.code)
? inputs.code.map((c: { content: string }) => c.content).join('\n')
: inputs.code
// Prepare code for execution
const codeContent = Array.isArray(inputs.code)
? inputs.code.map((c: { content: string }) => c.content).join('\n')
: inputs.code
// Use the shared helper function
const result = await executeCodeWithFallback(codeContent, inputs, inputs.timeout || 5000)
// Execute directly in the browser
const result = await executeCode(codeContent, inputs, inputs.timeout || 5000)
if (!result.success) {
console.warn(`WebContainer API execution failed: ${result.error}`)
throw new Error(result.error || `WebContainer execution failed with no error message`)
}
return { response: result.output }
} catch (error: any) {
console.warn('WebContainer execution failed, falling back to VM:', error)
console.error('WebContainer error details:', {
name: error.name,
message: error.message,
stack: error.stack,
})
}
if (!result.success) {
throw new Error(result.error || 'Function execution failed')
}
// Fall back to VM execution if WebContainer fails or not available
try {
const vmResult = await executeTool('function_execute', inputs, true)
if (!vmResult.success) {
throw new Error(vmResult.error || `Function execution failed with no error message`)
}
return { response: vmResult.output }
} catch (vmError: any) {
throw new Error(`Function execution failed: ${vmError.message}`)
}
return { response: result.output }
}
}
+113
View File
@@ -1,4 +1,5 @@
import { useCustomToolsStore } from '@/stores/custom-tools/store'
import { useEnvironmentStore } from '@/stores/settings/environment/store'
import { visionTool as crewAIVision } from './crewai/vision'
import { scrapeTool } from './firecrawl/scrape'
import { functionExecuteTool as functionExecute } from './function/execute'
@@ -82,6 +83,16 @@ export function getTool(toolId: string): ToolConfig | undefined {
return undefined
}
// Check if we're running in the browser
function isBrowser(): boolean {
return typeof window !== 'undefined'
}
// Check if WebContainer is available
function isWebContainerAvailable(): boolean {
return isBrowser() && !!window.crossOriginIsolated
}
// Create a tool config from a custom tool definition
function getCustomTool(customToolId: string): ToolConfig | undefined {
// Extract the identifier part (could be UUID or title)
@@ -131,16 +142,109 @@ function getCustomTool(customToolId: string): ToolConfig | undefined {
method: 'POST',
headers: () => ({ 'Content-Type': 'application/json' }),
body: (params: Record<string, any>) => {
// Get environment variables from the store
const envStore = useEnvironmentStore.getState()
const allEnvVars = envStore.getAllVariables()
// Convert environment variables to a simple key-value object
const envVars = Object.entries(allEnvVars).reduce(
(acc, [key, variable]) => {
acc[key] = variable.value
return acc
},
{} as Record<string, string>
)
// Include everything needed for execution
return {
code: customTool.code,
params: params, // These will be available in the VM context
schema: customTool.schema.function.parameters, // For validation on the client side
envVars: envVars, // Pass environment variables for server-side resolution
}
},
isInternalRoute: true,
},
// Direct execution support for browser environment with WebContainer
directExecution: async (params: Record<string, any>) => {
// If there's no code, we can't execute directly
if (!customTool.code) {
return {
success: false,
output: {},
error: 'No code provided for tool execution',
}
}
// If we're in a browser with WebContainer available, use it
if (isWebContainerAvailable()) {
try {
// Get environment variables from the store
const envStore = useEnvironmentStore.getState()
const envVars = envStore.getAllVariables()
// Create a merged params object that includes environment variables
const mergedParams = { ...params }
// Add environment variables to the params
Object.entries(envVars).forEach(([key, variable]) => {
if (variable.value && !mergedParams[key]) {
mergedParams[key] = variable.value
}
})
// Resolve environment variables and tags in the code
let resolvedCode = customTool.code
// Resolve environment variables with {{var_name}} syntax
const envVarMatches = resolvedCode.match(/\{\{([^}]+)\}\}/g) || []
for (const match of envVarMatches) {
const varName = match.slice(2, -2).trim()
// Look for the variable in our environment store first, then in params
const envVar = envVars[varName]
const varValue = envVar ? envVar.value : mergedParams[varName] || ''
resolvedCode = resolvedCode.replace(match, varValue)
}
// Resolve tags with <tag_name> syntax
const tagMatches = resolvedCode.match(/<([^>]+)>/g) || []
for (const match of tagMatches) {
const tagName = match.slice(1, -1).trim()
const tagValue = mergedParams[tagName] || ''
resolvedCode = resolvedCode.replace(match, tagValue)
}
// Dynamically import the executeCode function
const { executeCode } = await import('@/lib/webcontainer')
// Execute the code with resolved variables
const result = await executeCode(
resolvedCode,
mergedParams, // Use the merged params that include env vars
5000 // Default timeout
)
if (!result.success) {
throw new Error(result.error || 'WebContainer execution failed')
}
return {
success: true,
output: result.output.result || result.output,
error: undefined,
}
} catch (error: any) {
console.warn('WebContainer execution failed, falling back to API:', error.message)
// Fall back to API route if WebContainer fails
return undefined
}
}
// No WebContainer or not in browser, return undefined to use regular API route
return undefined
},
// Response handling
transformResponse: async (response: Response) => {
const data = await response.json()
@@ -177,6 +281,15 @@ export async function executeTool(
throw new Error(`Tool not found: ${toolId}`)
}
// For custom tools, try direct execution in browser first if available
if (toolId.startsWith('custom_') && tool.directExecution) {
const directResult = await tool.directExecution(params)
if (directResult) {
return directResult
}
// If directExecution returns undefined, fall back to API route
}
// For internal routes or when skipProxy is true, call the API directly
if (tool.request.isInternalRoute || skipProxy) {
const result = await handleInternalRequest(toolId, tool, params)
+3
View File
@@ -34,6 +34,9 @@ export interface ToolConfig<P = any, R extends ToolResponse = ToolResponse> {
isInternalRoute?: boolean // Whether this is an internal API route
}
// Direct execution in browser (optional) - bypasses HTTP request
directExecution?: (params: P) => Promise<R | undefined>
// Response handling
transformResponse: (response: Response) => Promise<R>
transformError: (error: any) => string