feat(e2b-execution): add remote code execution to support Python + Imports (#1226)

* feat(e2b-execution): add remote code execution via e2b

* ux improvements

* fix streaming

* progress

* fix tooltip text

* make supported languages an enum

* fix error handling

* fix tests
This commit is contained in:
Vikhyath Mondreti
2025-09-02 18:15:29 -07:00
committed by GitHub
parent 9de0d91f9a
commit 1a5d5ddffa
17 changed files with 658 additions and 70 deletions

View File

@@ -32,6 +32,14 @@ describe('Function Execute API Route', () => {
createLogger: vi.fn().mockReturnValue(mockLogger),
}))
vi.doMock('@/lib/execution/e2b', () => ({
executeInE2B: vi.fn().mockResolvedValue({
result: 'e2b success',
stdout: 'e2b output',
sandboxId: 'test-sandbox-id',
}),
}))
mockRunInContext.mockResolvedValue('vm success')
mockCreateContext.mockReturnValue({})
})
@@ -45,6 +53,7 @@ describe('Function Execute API Route', () => {
const req = createMockRequest('POST', {
code: 'return "Hello World"',
timeout: 5000,
useLocalVM: true,
})
const { POST } = await import('@/app/api/function/execute/route')
@@ -74,6 +83,7 @@ describe('Function Execute API Route', () => {
it('should use default timeout when not provided', async () => {
const req = createMockRequest('POST', {
code: 'return "test"',
useLocalVM: true,
})
const { POST } = await import('@/app/api/function/execute/route')
@@ -93,6 +103,7 @@ describe('Function Execute API Route', () => {
it('should resolve environment variables with {{var_name}} syntax', async () => {
const req = createMockRequest('POST', {
code: 'return {{API_KEY}}',
useLocalVM: true,
envVars: {
API_KEY: 'secret-key-123',
},
@@ -108,6 +119,7 @@ describe('Function Execute API Route', () => {
it('should resolve tag variables with <tag_name> syntax', async () => {
const req = createMockRequest('POST', {
code: 'return <email>',
useLocalVM: true,
params: {
email: { id: '123', subject: 'Test Email' },
},
@@ -123,6 +135,7 @@ describe('Function Execute API Route', () => {
it('should NOT treat email addresses as template variables', async () => {
const req = createMockRequest('POST', {
code: 'return "Email sent to user"',
useLocalVM: true,
params: {
email: {
from: 'Waleed Latif <waleed@sim.ai>',
@@ -141,6 +154,7 @@ describe('Function Execute API Route', () => {
it('should only match valid variable names in angle brackets', async () => {
const req = createMockRequest('POST', {
code: 'return <validVar> + "<invalid@email.com>" + <another_valid>',
useLocalVM: true,
params: {
validVar: 'hello',
another_valid: 'world',
@@ -178,6 +192,7 @@ describe('Function Execute API Route', () => {
const req = createMockRequest('POST', {
code: 'return <email>',
useLocalVM: true,
params: gmailData,
})
@@ -200,6 +215,7 @@ describe('Function Execute API Route', () => {
const req = createMockRequest('POST', {
code: 'return <email>',
useLocalVM: true,
params: complexEmailData,
})
@@ -214,6 +230,7 @@ describe('Function Execute API Route', () => {
it('should handle custom tool execution with direct parameter access', async () => {
const req = createMockRequest('POST', {
code: 'return location + " weather is sunny"',
useLocalVM: true,
params: {
location: 'San Francisco',
},
@@ -245,6 +262,7 @@ describe('Function Execute API Route', () => {
it('should handle timeout parameter', async () => {
const req = createMockRequest('POST', {
code: 'return "test"',
useLocalVM: true,
timeout: 10000,
})
@@ -262,6 +280,7 @@ describe('Function Execute API Route', () => {
it('should handle empty parameters object', async () => {
const req = createMockRequest('POST', {
code: 'return "no params"',
useLocalVM: true,
params: {},
})
@@ -295,6 +314,7 @@ SyntaxError: Invalid or unexpected token
const req = createMockRequest('POST', {
code: 'const obj = {\n name: "test",\n description: "This has a missing closing quote\n};\nreturn obj;',
useLocalVM: true,
timeout: 5000,
})
@@ -338,6 +358,7 @@ SyntaxError: Invalid or unexpected token
const req = createMockRequest('POST', {
code: 'const obj = null;\nreturn obj.someMethod();',
useLocalVM: true,
timeout: 5000,
})
@@ -379,6 +400,7 @@ SyntaxError: Invalid or unexpected token
const req = createMockRequest('POST', {
code: 'const x = 42;\nreturn undefinedVariable + x;',
useLocalVM: true,
timeout: 5000,
})
@@ -409,6 +431,7 @@ SyntaxError: Invalid or unexpected token
const req = createMockRequest('POST', {
code: 'return "test";',
useLocalVM: true,
timeout: 5000,
})
@@ -445,6 +468,7 @@ SyntaxError: Invalid or unexpected token
const req = createMockRequest('POST', {
code: 'const a = 1;\nconst b = 2;\nconst c = 3;\nconst d = 4;\nreturn a + b + c + d;',
useLocalVM: true,
timeout: 5000,
})
@@ -476,6 +500,7 @@ SyntaxError: Invalid or unexpected token
const req = createMockRequest('POST', {
code: 'const obj = {\n name: "test"\n// Missing closing brace',
useLocalVM: true,
timeout: 5000,
})
@@ -496,6 +521,7 @@ SyntaxError: Invalid or unexpected token
// This tests the escapeRegExp function indirectly
const req = createMockRequest('POST', {
code: 'return {{special.chars+*?}}',
useLocalVM: true,
envVars: {
'special.chars+*?': 'escaped-value',
},
@@ -512,6 +538,7 @@ SyntaxError: Invalid or unexpected token
// Test with complex but not circular data first
const req = createMockRequest('POST', {
code: 'return <complexData>',
useLocalVM: true,
params: {
complexData: {
special: 'chars"with\'quotes',

View File

@@ -1,5 +1,7 @@
import { createContext, Script } from 'vm'
import { type NextRequest, NextResponse } from 'next/server'
import { executeInE2B } from '@/lib/execution/e2b'
import { CodeLanguage, DEFAULT_CODE_LANGUAGE, isValidCodeLanguage } from '@/lib/execution/languages'
import { createLogger } from '@/lib/logs/console/logger'
export const dynamic = 'force-dynamic'
@@ -8,6 +10,10 @@ export const maxDuration = 60
const logger = createLogger('FunctionExecuteAPI')
// Constants for E2B code wrapping line counts
const E2B_JS_WRAPPER_LINES = 3 // Lines before user code: ';(async () => {', ' try {', ' const __sim_result = await (async () => {'
const E2B_PYTHON_WRAPPER_LINES = 1 // Lines before user code: 'def __sim_main__():'
/**
* Enhanced error information interface
*/
@@ -124,6 +130,103 @@ function extractEnhancedError(
return enhanced
}
/**
* Parse and format E2B error message
* Removes E2B-specific line references and adds correct user line numbers
*/
function formatE2BError(
errorMessage: string,
errorOutput: string,
language: CodeLanguage,
userCode: string,
prologueLineCount: number
): { formattedError: string; cleanedOutput: string } {
// Calculate line offset based on language and prologue
const wrapperLines =
language === CodeLanguage.Python ? E2B_PYTHON_WRAPPER_LINES : E2B_JS_WRAPPER_LINES
const totalOffset = prologueLineCount + wrapperLines
let userLine: number | undefined
let cleanErrorType = ''
let cleanErrorMsg = ''
if (language === CodeLanguage.Python) {
// Python error format: "Cell In[X], line Y" followed by error details
// Extract line number from the Cell reference
const cellMatch = errorOutput.match(/Cell In\[\d+\], line (\d+)/)
if (cellMatch) {
const originalLine = Number.parseInt(cellMatch[1], 10)
userLine = originalLine - totalOffset
}
// Extract clean error message from the error string
// Remove file references like "(detected at line X) (file.py, line Y)"
cleanErrorMsg = errorMessage
.replace(/\s*\(detected at line \d+\)/g, '')
.replace(/\s*\([^)]+\.py, line \d+\)/g, '')
.trim()
} else if (language === CodeLanguage.JavaScript) {
// JavaScript error format from E2B: "SyntaxError: /path/file.ts: Message. (line:col)\n\n 9 | ..."
// First, extract the error type and message from the first line
const firstLineEnd = errorMessage.indexOf('\n')
const firstLine = firstLineEnd > 0 ? errorMessage.substring(0, firstLineEnd) : errorMessage
// Parse: "SyntaxError: /home/user/index.ts: Missing semicolon. (11:9)"
const jsErrorMatch = firstLine.match(/^(\w+Error):\s*[^:]+:\s*([^(]+)\.\s*\((\d+):(\d+)\)/)
if (jsErrorMatch) {
cleanErrorType = jsErrorMatch[1]
cleanErrorMsg = jsErrorMatch[2].trim()
const originalLine = Number.parseInt(jsErrorMatch[3], 10)
userLine = originalLine - totalOffset
} else {
// Fallback: look for line number in the arrow pointer line (> 11 |)
const arrowMatch = errorMessage.match(/^>\s*(\d+)\s*\|/m)
if (arrowMatch) {
const originalLine = Number.parseInt(arrowMatch[1], 10)
userLine = originalLine - totalOffset
}
// Try to extract error type and message
const errorMatch = firstLine.match(/^(\w+Error):\s*(.+)/)
if (errorMatch) {
cleanErrorType = errorMatch[1]
cleanErrorMsg = errorMatch[2]
.replace(/^[^:]+:\s*/, '') // Remove file path
.replace(/\s*\(\d+:\d+\)\s*$/, '') // Remove line:col at end
.trim()
} else {
cleanErrorMsg = firstLine
}
}
}
// Build the final clean error message
const finalErrorMsg =
cleanErrorType && cleanErrorMsg
? `${cleanErrorType}: ${cleanErrorMsg}`
: cleanErrorMsg || errorMessage
// Format with line number if available
let formattedError = finalErrorMsg
if (userLine && userLine > 0) {
const codeLines = userCode.split('\n')
// Clamp userLine to the actual user code range
const actualUserLine = Math.min(userLine, codeLines.length)
if (actualUserLine > 0 && actualUserLine <= codeLines.length) {
const lineContent = codeLines[actualUserLine - 1]?.trim()
if (lineContent) {
formattedError = `Line ${actualUserLine}: \`${lineContent}\` - ${finalErrorMsg}`
} else {
formattedError = `Line ${actualUserLine} - ${finalErrorMsg}`
}
}
}
// For stdout, just return the clean error message without the full traceback
const cleanedOutput = finalErrorMsg
return { formattedError, cleanedOutput }
}
/**
* Create a detailed error message for users
*/
@@ -442,6 +545,8 @@ export async function POST(req: NextRequest) {
code,
params = {},
timeout = 5000,
language = DEFAULT_CODE_LANGUAGE,
useLocalVM = false,
envVars = {},
blockData = {},
blockNameMapping = {},
@@ -474,19 +579,163 @@ export async function POST(req: NextRequest) {
resolvedCode = codeResolution.resolvedCode
const contextVariables = codeResolution.contextVariables
const executionMethod = 'vm' // Default execution method
const e2bEnabled = process.env.E2B_ENABLED === 'true'
const lang = isValidCodeLanguage(language) ? language : DEFAULT_CODE_LANGUAGE
const useE2B =
e2bEnabled &&
!useLocalVM &&
(lang === CodeLanguage.JavaScript || lang === CodeLanguage.Python)
logger.info(`[${requestId}] Using VM for code execution`, {
hasEnvVars: Object.keys(envVars).length > 0,
hasWorkflowVariables: Object.keys(workflowVariables).length > 0,
})
if (useE2B) {
logger.info(`[${requestId}] E2B status`, {
enabled: e2bEnabled,
hasApiKey: Boolean(process.env.E2B_API_KEY),
language: lang,
})
let prologue = ''
const epilogue = ''
// Create a secure context with console logging
if (lang === CodeLanguage.JavaScript) {
// Track prologue lines for error adjustment
let prologueLineCount = 0
prologue += `const params = JSON.parse(${JSON.stringify(JSON.stringify(executionParams))});\n`
prologueLineCount++
prologue += `const environmentVariables = JSON.parse(${JSON.stringify(JSON.stringify(envVars))});\n`
prologueLineCount++
for (const [k, v] of Object.entries(contextVariables)) {
prologue += `const ${k} = JSON.parse(${JSON.stringify(JSON.stringify(v))});\n`
prologueLineCount++
}
const wrapped = [
';(async () => {',
' try {',
' const __sim_result = await (async () => {',
` ${resolvedCode.split('\n').join('\n ')}`,
' })();',
" console.log('__SIM_RESULT__=' + JSON.stringify(__sim_result));",
' } catch (error) {',
' console.log(String((error && (error.stack || error.message)) || error));',
' throw error;',
' }',
'})();',
].join('\n')
const codeForE2B = prologue + wrapped + epilogue
const execStart = Date.now()
const {
result: e2bResult,
stdout: e2bStdout,
sandboxId,
error: e2bError,
} = await executeInE2B({
code: codeForE2B,
language: CodeLanguage.JavaScript,
timeoutMs: timeout,
})
const executionTime = Date.now() - execStart
stdout += e2bStdout
logger.info(`[${requestId}] E2B JS sandbox`, {
sandboxId,
stdoutPreview: e2bStdout?.slice(0, 200),
error: e2bError,
})
// If there was an execution error, format it properly
if (e2bError) {
const { formattedError, cleanedOutput } = formatE2BError(
e2bError,
e2bStdout,
lang,
resolvedCode,
prologueLineCount
)
return NextResponse.json(
{
success: false,
error: formattedError,
output: { result: null, stdout: cleanedOutput, executionTime },
},
{ status: 500 }
)
}
return NextResponse.json({
success: true,
output: { result: e2bResult ?? null, stdout, executionTime },
})
}
// Track prologue lines for error adjustment
let prologueLineCount = 0
prologue += 'import json\n'
prologueLineCount++
prologue += `params = json.loads(${JSON.stringify(JSON.stringify(executionParams))})\n`
prologueLineCount++
prologue += `environmentVariables = json.loads(${JSON.stringify(JSON.stringify(envVars))})\n`
prologueLineCount++
for (const [k, v] of Object.entries(contextVariables)) {
prologue += `${k} = json.loads(${JSON.stringify(JSON.stringify(v))})\n`
prologueLineCount++
}
const wrapped = [
'def __sim_main__():',
...resolvedCode.split('\n').map((l) => ` ${l}`),
'__sim_result__ = __sim_main__()',
"print('__SIM_RESULT__=' + json.dumps(__sim_result__))",
].join('\n')
const codeForE2B = prologue + wrapped + epilogue
const execStart = Date.now()
const {
result: e2bResult,
stdout: e2bStdout,
sandboxId,
error: e2bError,
} = await executeInE2B({
code: codeForE2B,
language: CodeLanguage.Python,
timeoutMs: timeout,
})
const executionTime = Date.now() - execStart
stdout += e2bStdout
logger.info(`[${requestId}] E2B Py sandbox`, {
sandboxId,
stdoutPreview: e2bStdout?.slice(0, 200),
error: e2bError,
})
// If there was an execution error, format it properly
if (e2bError) {
const { formattedError, cleanedOutput } = formatE2BError(
e2bError,
e2bStdout,
lang,
resolvedCode,
prologueLineCount
)
return NextResponse.json(
{
success: false,
error: formattedError,
output: { result: null, stdout: cleanedOutput, executionTime },
},
{ status: 500 }
)
}
return NextResponse.json({
success: true,
output: { result: e2bResult ?? null, stdout, executionTime },
})
}
const executionMethod = 'vm'
const context = createContext({
params: executionParams,
environmentVariables: envVars,
...contextVariables, // Add resolved variables directly to context
fetch: globalThis.fetch || require('node-fetch').default,
...contextVariables,
fetch: (globalThis as any).fetch || require('node-fetch').default,
console: {
log: (...args: any[]) => {
const logMessage = `${args
@@ -504,23 +753,17 @@ export async function POST(req: NextRequest) {
},
})
// Calculate line offset for user code to provide accurate error reporting
const wrapperLines = ['(async () => {', ' try {']
// Add custom tool parameter declarations if needed
if (isCustomTool) {
wrapperLines.push(' // For custom tools, make parameters directly accessible')
Object.keys(executionParams).forEach((key) => {
wrapperLines.push(` const ${key} = params.${key};`)
})
}
userCodeStartLine = wrapperLines.length + 1 // +1 because user code starts on next line
// Build the complete script with proper formatting for line numbers
userCodeStartLine = wrapperLines.length + 1
const fullScript = [
...wrapperLines,
` ${resolvedCode.split('\n').join('\n ')}`, // Indent user code
` ${resolvedCode.split('\n').join('\n ')}`,
' } catch (error) {',
' console.error(error);',
' throw error;',
@@ -529,33 +772,26 @@ export async function POST(req: NextRequest) {
].join('\n')
const script = new Script(fullScript, {
filename: 'user-function.js', // This filename will appear in stack traces
lineOffset: 0, // Start line numbering from 0
columnOffset: 0, // Start column numbering from 0
filename: 'user-function.js',
lineOffset: 0,
columnOffset: 0,
})
const result = await script.runInContext(context, {
timeout,
displayErrors: true,
breakOnSigint: true, // Allow breaking on SIGINT for better debugging
breakOnSigint: true,
})
// }
const executionTime = Date.now() - startTime
logger.info(`[${requestId}] Function executed successfully using ${executionMethod}`, {
executionTime,
})
const response = {
return NextResponse.json({
success: true,
output: {
result,
stdout,
executionTime,
},
}
return NextResponse.json(response)
output: { result, stdout, executionTime },
})
} catch (error: any) {
const executionTime = Date.now() - startTime
logger.error(`[${requestId}] Function execution failed`, {

View File

@@ -5,10 +5,12 @@ import { useParams } from 'next/navigation'
import { highlight, languages } from 'prismjs'
import 'prismjs/components/prism-javascript'
import 'prismjs/themes/prism.css'
import 'prismjs/components/prism-python'
import Editor from 'react-simple-code-editor'
import { Button } from '@/components/ui/button'
import { checkEnvVarTrigger, EnvVarDropdown } from '@/components/ui/env-var-dropdown'
import { checkTagTrigger, TagDropdown } from '@/components/ui/tag-dropdown'
import { CodeLanguage } from '@/lib/execution/languages'
import { createLogger } from '@/lib/logs/console/logger'
import { cn } from '@/lib/utils'
import { WandPromptBar } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/wand-prompt-bar/wand-prompt-bar'
@@ -26,7 +28,7 @@ interface CodeProps {
subBlockId: string
isConnecting: boolean
placeholder?: string
language?: 'javascript' | 'json'
language?: 'javascript' | 'json' | 'python'
generationType?: GenerationType
value?: string
isPreview?: boolean
@@ -125,35 +127,73 @@ export function Code({
if (onValidationChange && subBlockId === 'responseFormat') {
const timeoutId = setTimeout(() => {
onValidationChange(isValidJson)
}, 150) // Match debounce time from setStoreValue
}, 150)
return () => clearTimeout(timeoutId)
}
}, [isValidJson, onValidationChange, subBlockId])
const editorRef = useRef<HTMLDivElement>(null)
// Function to toggle collapsed state
const toggleCollapsed = () => {
setCollapsedValue(blockId, collapsedStateKey, !isCollapsed)
}
// Create refs to hold the handlers
const handleStreamStartRef = useRef<() => void>(() => {})
const handleGeneratedContentRef = useRef<(generatedCode: string) => void>(() => {})
const handleStreamChunkRef = useRef<(chunk: string) => void>(() => {})
// AI Code Generation Hook - use new wand system
const wandHook = wandConfig?.enabled
const [languageValue] = useSubBlockValue<string>(blockId, 'language')
const [remoteExecution] = useSubBlockValue<boolean>(blockId, 'remoteExecution')
const effectiveLanguage = (languageValue as 'javascript' | 'python' | 'json') || language
const dynamicPlaceholder = useMemo(() => {
if (remoteExecution && languageValue === CodeLanguage.Python) {
return 'Write Python...'
}
return placeholder
}, [remoteExecution, languageValue, placeholder])
const dynamicWandConfig = useMemo(() => {
if (remoteExecution && languageValue === CodeLanguage.Python) {
return {
...wandConfig,
prompt: `You are an expert Python programmer.
Generate ONLY the raw body of a Python function based on the user's request.
The code should be executable within a Python function body context.
- 'params' (object): Contains input parameters derived from the JSON schema. Access these directly using the parameter name wrapped in angle brackets, e.g., '<paramName>'. Do NOT use 'params.paramName'.
- 'environmentVariables' (object): Contains environment variables. Reference these using the double curly brace syntax: '{{ENV_VAR_NAME}}'. Do NOT use os.environ or env.
Current code context: {context}
IMPORTANT FORMATTING RULES:
1. Reference Environment Variables: Use the exact syntax {{VARIABLE_NAME}}. Do NOT wrap it in quotes.
2. Reference Input Parameters/Workflow Variables: Use the exact syntax <variable_name>. Do NOT wrap it in quotes.
3. Function Body ONLY: Do NOT include the function signature (e.g., 'def my_func(...)') or surrounding braces. Return the final value with 'return'.
4. Imports: You may add imports as needed (standard library or pip-installed packages) without comments.
5. No Markdown: Do NOT include backticks, code fences, or any markdown.
6. Clarity: Write clean, readable Python code.`,
placeholder: 'Describe the Python function you want to create...',
}
}
return wandConfig
}, [wandConfig, remoteExecution, languageValue])
const wandHook = dynamicWandConfig?.enabled
? useWand({
wandConfig,
wandConfig: dynamicWandConfig,
currentValue: code,
onStreamStart: () => handleStreamStartRef.current?.(),
onStreamChunk: (chunk: string) => handleStreamChunkRef.current?.(chunk),
onGeneratedContent: (content: string) => handleGeneratedContentRef.current?.(content),
onStreamChunk: (chunk: string) => {
setCode((prev) => prev + chunk)
handleStreamChunkRef.current?.(chunk)
},
onGeneratedContent: (content: string) => {
handleGeneratedContentRef.current?.(content)
},
})
: null
// Extract values from wand hook
const isAiLoading = wandHook?.isLoading || false
const isAiStreaming = wandHook?.isStreaming || false
const generateCodeStream = wandHook?.generateStream || (() => {})
@@ -164,7 +204,6 @@ export function Code({
const updatePromptValue = wandHook?.updatePromptValue || (() => {})
const cancelGeneration = wandHook?.cancelGeneration || (() => {})
// State management - useSubBlockValue with explicit streaming control
const [storeValue, setStoreValue] = useSubBlockValue(blockId, subBlockId, false, {
isStreaming: isAiStreaming,
onStreamingEnd: () => {
@@ -174,43 +213,28 @@ export function Code({
const emitTagSelection = useTagSelection(blockId, subBlockId)
// Use preview value when in preview mode, otherwise use store value or prop value
const value = isPreview ? previewValue : propValue !== undefined ? propValue : storeValue
// Define the handlers in useEffect to avoid setState during render
useEffect(() => {
handleStreamStartRef.current = () => {
setCode('')
// Streaming state is now controlled by isAiStreaming
}
handleGeneratedContentRef.current = (generatedCode: string) => {
setCode(generatedCode)
if (!isPreview && !disabled) {
setStoreValue(generatedCode)
// Final value will be persisted when isAiStreaming becomes false
}
}
handleStreamChunkRef.current = (chunk: string) => {
setCode((currentCode) => {
const newCode = currentCode + chunk
if (!isPreview && !disabled) {
// Update the value - it won't be persisted until streaming ends
setStoreValue(newCode)
}
return newCode
})
}
}, [isPreview, disabled, setStoreValue])
// Effects
useEffect(() => {
if (isAiStreaming) return
const valueString = value?.toString() ?? ''
if (valueString !== code) {
setCode(valueString)
}
}, [value])
}, [value, code, isAiStreaming])
useEffect(() => {
if (!editorRef.current) return
@@ -279,7 +303,6 @@ export function Code({
}
}, [code])
// Handlers
const handleDrop = (e: React.DragEvent) => {
if (isPreview) return
e.preventDefault()
@@ -338,12 +361,11 @@ export function Code({
}, 0)
}
// Render helpers
const renderLineNumbers = () => {
const renderLineNumbers = (): ReactElement[] => {
const numbers: ReactElement[] = []
let lineNumber = 1
visualLineHeights.forEach((height, index) => {
visualLineHeights.forEach((height) => {
numbers.push(
<div key={`${lineNumber}-0`} className={cn('text-muted-foreground text-xs leading-[21px]')}>
{lineNumber}
@@ -364,7 +386,7 @@ export function Code({
if (numbers.length === 0) {
numbers.push(
<div key='1-0' className={cn('text-muted-foreground text-xs leading-[21px]')}>
<div key={'1-0'} className={cn('text-muted-foreground text-xs leading-[21px]')}>
1
</div>
)
@@ -383,7 +405,7 @@ export function Code({
onSubmit={(prompt: string) => generateCodeStream({ prompt })}
onCancel={isAiStreaming ? cancelGeneration : hidePromptInline}
onChange={updatePromptValue}
placeholder={wandConfig?.placeholder || aiPromptPlaceholder}
placeholder={dynamicWandConfig?.placeholder || aiPromptPlaceholder}
/>
<div
@@ -440,7 +462,7 @@ export function Code({
>
{code.length === 0 && !isCollapsed && (
<div className='pointer-events-none absolute top-[12px] left-[42px] select-none text-muted-foreground/50'>
{placeholder}
{dynamicPlaceholder}
</div>
)}
@@ -478,7 +500,11 @@ export function Code({
}
}}
highlight={(codeToHighlight) =>
highlight(codeToHighlight, languages[language], language)
highlight(
codeToHighlight,
languages[effectiveLanguage === 'python' ? 'python' : 'javascript'],
effectiveLanguage === 'python' ? 'python' : 'javascript'
)
}
padding={12}
style={{

View File

@@ -0,0 +1,60 @@
import { Info } from 'lucide-react'
import { Label, Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui'
import { Switch as UISwitch } from '@/components/ui/switch'
import { env } from '@/lib/env'
import { useSubBlockValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/hooks/use-sub-block-value'
interface E2BSwitchProps {
blockId: string
subBlockId: string
title: string
value?: boolean
isPreview?: boolean
previewValue?: boolean | null
disabled?: boolean
}
export function E2BSwitch({
blockId,
subBlockId,
title,
value: propValue,
isPreview = false,
previewValue,
disabled = false,
}: E2BSwitchProps) {
const e2bEnabled = env.NEXT_PUBLIC_E2B_ENABLED === 'true'
if (!e2bEnabled) return null
const [storeValue, setStoreValue] = useSubBlockValue<boolean>(blockId, subBlockId)
const value = isPreview ? previewValue : propValue !== undefined ? propValue : storeValue
const handleChange = (checked: boolean) => {
if (!isPreview && !disabled) setStoreValue(checked)
}
return (
<div className='flex items-center gap-2'>
<UISwitch
id={`${blockId}-${subBlockId}`}
checked={Boolean(value)}
onCheckedChange={handleChange}
disabled={isPreview || disabled}
/>
<Label
htmlFor={`${blockId}-${subBlockId}`}
className='cursor-pointer font-normal text-sm leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70'
>
{title}
</Label>
<Tooltip>
<TooltipTrigger asChild>
<Info className='h-4 w-4 cursor-pointer text-muted-foreground' />
</TooltipTrigger>
<TooltipContent side='top' className='max-w-[320px] select-text whitespace-pre-wrap'>
Python/Javascript code run in a sandbox environment. Can have slower execution times.
</TooltipContent>
</Tooltip>
</div>
)
}

View File

@@ -34,6 +34,7 @@ import {
} from '@/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components'
import type { SubBlockConfig } from '@/blocks/types'
import { DocumentTagEntry } from './components/document-tag-entry/document-tag-entry'
import { E2BSwitch } from './components/e2b-switch'
import { KnowledgeTagFilters } from './components/knowledge-tag-filters/knowledge-tag-filters'
interface SubBlockProps {
@@ -203,6 +204,18 @@ export function SubBlock({
/>
)
case 'switch':
if (config.id === 'remoteExecution') {
return (
<E2BSwitch
blockId={blockId}
subBlockId={config.id}
title={config.title ?? ''}
isPreview={isPreview}
previewValue={previewValue}
disabled={isDisabled}
/>
)
}
return (
<Switch
blockId={blockId}

View File

@@ -6,6 +6,7 @@ import { Badge } from '@/components/ui/badge'
import { Button } from '@/components/ui/button'
import { Card } from '@/components/ui/card'
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'
import { env } from '@/lib/env'
import { createLogger } from '@/lib/logs/console/logger'
import { parseCronToHumanReadable } from '@/lib/schedules/utils'
import { cn, validateName } from '@/lib/utils'
@@ -442,11 +443,17 @@ export function WorkflowBlock({ id, data }: NodeProps<WorkflowBlockProps>) {
const isTriggerMode = useWorkflowStore.getState().blocks[blockId]?.triggerMode ?? false
const effectiveAdvanced = currentWorkflow.isDiffMode ? displayAdvancedMode : isAdvancedMode
const effectiveTrigger = currentWorkflow.isDiffMode ? displayTriggerMode : isTriggerMode
const e2bClientEnabled = env.NEXT_PUBLIC_E2B_ENABLED === 'true'
// Filter visible blocks and those that meet their conditions
const visibleSubBlocks = subBlocks.filter((block) => {
if (block.hidden) return false
// Filter out E2B-related blocks if E2B is not enabled on the client
if (!e2bClientEnabled && (block.id === 'remoteExecution' || block.id === 'language')) {
return false
}
// Special handling for trigger mode
if (block.type === ('trigger-config' as SubBlockType)) {
// Show trigger-config blocks when in trigger mode OR for pure trigger blocks

View File

@@ -1,4 +1,5 @@
import { CodeIcon } from '@/components/icons'
import { CodeLanguage, getLanguageDisplayName } from '@/lib/execution/languages'
import type { BlockConfig } from '@/blocks/types'
import type { CodeExecutionOutput } from '@/tools/function/types'
@@ -7,12 +8,34 @@ export const FunctionBlock: BlockConfig<CodeExecutionOutput> = {
name: 'Function',
description: 'Run custom logic',
longDescription:
'Execute custom JavaScript or TypeScript code within your workflow to transform data or implement complex logic. Create reusable functions to process inputs and generate outputs for other blocks.',
'Execute custom JavaScript or Python code within your workflow. Use E2B for remote execution with imports or enable Fast Mode (bolt) to run JavaScript locally for lowest latency.',
docsLink: 'https://docs.sim.ai/blocks/function',
category: 'blocks',
bgColor: '#FF402F',
icon: CodeIcon,
subBlocks: [
{
id: 'remoteExecution',
type: 'switch',
layout: 'full',
title: 'Remote Code Execution',
description: 'Python/Javascript code run in a sandbox environment. Slower execution times.',
},
{
id: 'language',
type: 'dropdown',
layout: 'full',
options: [
{ label: getLanguageDisplayName(CodeLanguage.JavaScript), id: CodeLanguage.JavaScript },
{ label: getLanguageDisplayName(CodeLanguage.Python), id: CodeLanguage.Python },
],
placeholder: 'Select language',
value: () => CodeLanguage.JavaScript,
condition: {
field: 'remoteExecution',
value: true,
},
},
{
id: 'code',
type: 'code',
@@ -76,7 +99,9 @@ try {
access: ['function_execute'],
},
inputs: {
code: { type: 'string', description: 'JavaScript/TypeScript code to execute' },
code: { type: 'string', description: 'JavaScript or Python code to execute' },
remoteExecution: { type: 'boolean', description: 'Use E2B remote execution' },
language: { type: 'string', description: 'Language (javascript or python)' },
timeout: { type: 'number', description: 'Execution timeout' },
},
outputs: {

View File

@@ -75,6 +75,8 @@ describe('FunctionBlockHandler', () => {
}
const expectedToolParams = {
code: inputs.code,
language: 'javascript',
useLocalVM: true,
timeout: inputs.timeout,
envVars: {},
workflowVariables: {},
@@ -107,6 +109,8 @@ describe('FunctionBlockHandler', () => {
const expectedCode = 'const x = 5;\nreturn x * 2;'
const expectedToolParams = {
code: expectedCode,
language: 'javascript',
useLocalVM: true,
timeout: inputs.timeout,
envVars: {},
workflowVariables: {},
@@ -132,6 +136,8 @@ describe('FunctionBlockHandler', () => {
const inputs = { code: 'return true;' }
const expectedToolParams = {
code: inputs.code,
language: 'javascript',
useLocalVM: true,
timeout: 5000, // Default timeout
envVars: {},
workflowVariables: {},

View File

@@ -1,3 +1,4 @@
import { DEFAULT_CODE_LANGUAGE } from '@/lib/execution/languages'
import { createLogger } from '@/lib/logs/console/logger'
import { BlockType } from '@/executor/consts'
import type { BlockHandler, ExecutionContext } from '@/executor/types'
@@ -58,6 +59,8 @@ export class FunctionBlockHandler implements BlockHandler {
'function_execute',
{
code: codeContent,
language: inputs.language || DEFAULT_CODE_LANGUAGE,
useLocalVM: !inputs.remoteExecution,
timeout: inputs.timeout || 5000,
envVars: context.environmentVariables || {},
workflowVariables: context.workflowVariables || {},

View File

@@ -189,6 +189,10 @@ export const env = createEnv({
SLACK_CLIENT_SECRET: z.string().optional(), // Slack OAuth client secret
REDDIT_CLIENT_ID: z.string().optional(), // Reddit OAuth client ID
REDDIT_CLIENT_SECRET: z.string().optional(), // Reddit OAuth client secret
// E2B Remote Code Execution
E2B_ENABLED: z.string().optional(), // Enable E2B remote code execution
E2B_API_KEY: z.string().optional(), // E2B API key for sandbox creation
},
client: {
@@ -220,6 +224,8 @@ export const env = createEnv({
NEXT_PUBLIC_BRAND_FAVICON_URL: z.string().url().optional(), // Custom favicon URL
NEXT_PUBLIC_CUSTOM_CSS_URL: z.string().url().optional(), // Custom CSS stylesheet URL
NEXT_PUBLIC_SUPPORT_EMAIL: z.string().email().optional(), // Custom support email
NEXT_PUBLIC_E2B_ENABLED: z.string().optional(), // Enable E2B remote code execution (client-side)
NEXT_PUBLIC_DOCUMENTATION_URL: z.string().url().optional(), // Custom documentation URL
NEXT_PUBLIC_TERMS_URL: z.string().url().optional(), // Custom terms of service URL
NEXT_PUBLIC_PRIVACY_URL: z.string().url().optional(), // Custom privacy policy URL
@@ -268,6 +274,7 @@ export const env = createEnv({
NEXT_PUBLIC_BRAND_ACCENT_HOVER_COLOR: process.env.NEXT_PUBLIC_BRAND_ACCENT_HOVER_COLOR,
NEXT_PUBLIC_BRAND_BACKGROUND_COLOR: process.env.NEXT_PUBLIC_BRAND_BACKGROUND_COLOR,
NEXT_PUBLIC_TRIGGER_DEV_ENABLED: process.env.NEXT_PUBLIC_TRIGGER_DEV_ENABLED,
NEXT_PUBLIC_E2B_ENABLED: process.env.NEXT_PUBLIC_E2B_ENABLED,
NODE_ENV: process.env.NODE_ENV,
NEXT_TELEMETRY_DISABLED: process.env.NEXT_TELEMETRY_DISABLED,
},

View File

@@ -0,0 +1,98 @@
import { Sandbox } from '@e2b/code-interpreter'
import { createLogger } from '@/lib/logs/console/logger'
import { CodeLanguage } from './languages'
export interface E2BExecutionRequest {
code: string
language: CodeLanguage
timeoutMs: number
}
export interface E2BExecutionResult {
result: unknown
stdout: string
sandboxId?: string
error?: string
}
const logger = createLogger('E2BExecution')
export async function executeInE2B(req: E2BExecutionRequest): Promise<E2BExecutionResult> {
const { code, language, timeoutMs } = req
logger.info(`Executing code in E2B`, {
code,
language,
timeoutMs,
})
const apiKey = process.env.E2B_API_KEY
if (!apiKey) {
throw new Error('E2B_API_KEY is required when E2B is enabled')
}
const sandbox = await Sandbox.create({ apiKey })
const sandboxId = sandbox.sandboxId
const stdoutChunks = []
try {
const execution = await sandbox.runCode(code, {
language: language === CodeLanguage.Python ? 'python' : 'javascript',
timeoutMs,
})
// Check for execution errors
if (execution.error) {
const errorMessage = `${execution.error.name}: ${execution.error.value}`
logger.error(`E2B execution error`, {
sandboxId,
error: execution.error,
errorMessage,
})
// Include error traceback in stdout if available
const errorOutput = execution.error.traceback || errorMessage
return {
result: null,
stdout: errorOutput,
error: errorMessage,
sandboxId,
}
}
// Get output from execution
if (execution.text) {
stdoutChunks.push(execution.text)
}
if (execution.logs?.stdout) {
stdoutChunks.push(...execution.logs.stdout)
}
if (execution.logs?.stderr) {
stdoutChunks.push(...execution.logs.stderr)
}
const stdout = stdoutChunks.join('\n')
let result: unknown = null
const prefix = '__SIM_RESULT__='
const lines = stdout.split('\n')
const marker = lines.find((l) => l.startsWith(prefix))
let cleanedStdout = stdout
if (marker) {
const jsonPart = marker.slice(prefix.length)
try {
result = JSON.parse(jsonPart)
} catch {
result = jsonPart
}
cleanedStdout = lines.filter((l) => !l.startsWith(prefix)).join('\n')
}
return { result, stdout: cleanedStdout, sandboxId }
} finally {
try {
await sandbox.kill()
} catch {}
}
}

View File

@@ -0,0 +1,33 @@
/**
* Supported code execution languages
*/
export enum CodeLanguage {
JavaScript = 'javascript',
Python = 'python',
}
/**
* Type guard to check if a string is a valid CodeLanguage
*/
export function isValidCodeLanguage(value: string): value is CodeLanguage {
return Object.values(CodeLanguage).includes(value as CodeLanguage)
}
/**
* Get language display name
*/
export function getLanguageDisplayName(language: CodeLanguage): string {
switch (language) {
case CodeLanguage.JavaScript:
return 'JavaScript'
case CodeLanguage.Python:
return 'Python'
default:
return language
}
}
/**
* Default language for code execution
*/
export const DEFAULT_CODE_LANGUAGE = CodeLanguage.JavaScript

View File

@@ -26,6 +26,7 @@
"test:billing:suite": "bun run scripts/test-billing-suite.ts"
},
"dependencies": {
"@e2b/code-interpreter": "^2.0.0",
"@anthropic-ai/sdk": "^0.39.0",
"@aws-sdk/client-s3": "^3.779.0",
"@aws-sdk/s3-request-presigner": "^3.779.0",

View File

@@ -54,6 +54,8 @@ describe('Function Execute Tool', () => {
blockData: {},
blockNameMapping: {},
isCustomTool: false,
language: 'javascript',
useLocalVM: false,
timeout: 5000,
workflowId: undefined,
})
@@ -80,6 +82,8 @@ describe('Function Execute Tool', () => {
blockData: {},
blockNameMapping: {},
isCustomTool: false,
language: 'javascript',
useLocalVM: false,
workflowId: undefined,
})
})
@@ -97,6 +101,8 @@ describe('Function Execute Tool', () => {
blockData: {},
blockNameMapping: {},
isCustomTool: false,
language: 'javascript',
useLocalVM: false,
workflowId: undefined,
})
})

View File

@@ -1,3 +1,4 @@
import { DEFAULT_CODE_LANGUAGE } from '@/lib/execution/languages'
import type { CodeExecutionInput, CodeExecutionOutput } from '@/tools/function/types'
import type { ToolConfig } from '@/tools/types'
@@ -17,6 +18,21 @@ export const functionExecuteTool: ToolConfig<CodeExecutionInput, CodeExecutionOu
visibility: 'user-or-llm',
description: 'The code to execute',
},
language: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Language to execute (javascript or python)',
default: DEFAULT_CODE_LANGUAGE,
},
useLocalVM: {
type: 'boolean',
required: false,
visibility: 'user-only',
description:
'If true, execute JavaScript in local VM for faster execution. If false, use remote E2B execution.',
default: false,
},
timeout: {
type: 'number',
required: false,
@@ -67,6 +83,8 @@ export const functionExecuteTool: ToolConfig<CodeExecutionInput, CodeExecutionOu
return {
code: codeContent,
language: params.language || DEFAULT_CODE_LANGUAGE,
useLocalVM: params.useLocalVM || false,
timeout: params.timeout || DEFAULT_TIMEOUT,
envVars: params.envVars || {},
workflowVariables: params.workflowVariables || {},

View File

@@ -1,12 +1,15 @@
import type { CodeLanguage } from '@/lib/execution/languages'
import type { ToolResponse } from '@/tools/types'
export interface CodeExecutionInput {
code: Array<{ content: string; id: string }> | string
language?: CodeLanguage
useLocalVM?: boolean
timeout?: number
memoryLimit?: number
envVars?: Record<string, string>
workflowVariables?: Record<string, any>
blockData?: Record<string, any>
workflowVariables?: Record<string, unknown>
blockData?: Record<string, unknown>
blockNameMapping?: Record<string, string>
_context?: {
workflowId?: string

View File

@@ -64,6 +64,7 @@
"@cerebras/cerebras_cloud_sdk": "^1.23.0",
"@chatscope/chat-ui-kit-react": "2.1.1",
"@chatscope/chat-ui-kit-styles": "1.4.0",
"@e2b/code-interpreter": "^2.0.0",
"@hookform/resolvers": "^4.1.3",
"@opentelemetry/api": "^1.9.0",
"@opentelemetry/exporter-collector": "^0.25.0",
@@ -463,6 +464,8 @@
"@browserbasehq/stagehand": ["@browserbasehq/stagehand@2.4.4", "", { "dependencies": { "@anthropic-ai/sdk": "0.39.0", "@browserbasehq/sdk": "^2.4.0", "@google/genai": "^0.8.0", "ai": "^4.3.9", "devtools-protocol": "^0.0.1464554", "fetch-cookie": "^3.1.0", "openai": "^4.87.1", "pino": "^9.6.0", "pino-pretty": "^13.0.0", "playwright": "^1.52.0", "ws": "^8.18.0", "zod-to-json-schema": "^3.23.5" }, "optionalDependencies": { "@ai-sdk/anthropic": "^1.2.6", "@ai-sdk/azure": "^1.3.19", "@ai-sdk/cerebras": "^0.2.6", "@ai-sdk/deepseek": "^0.2.13", "@ai-sdk/google": "^1.2.6", "@ai-sdk/groq": "^1.2.4", "@ai-sdk/mistral": "^1.2.7", "@ai-sdk/openai": "^1.0.14", "@ai-sdk/perplexity": "^1.1.7", "@ai-sdk/togetherai": "^0.2.6", "@ai-sdk/xai": "^1.2.15", "ollama-ai-provider": "^1.2.0" }, "peerDependencies": { "deepmerge": "^4.3.1", "dotenv": "^16.4.5", "zod": ">=3.25.0 <4.1.0" } }, "sha512-PSctDxqMrLLCwv+fg3piYfzUvhe9vWZjSkBWiDVfTL2/611+PrTMGPjnikz1rhyDrTo7IU9KGui/ZMntZHEnzQ=="],
"@bufbuild/protobuf": ["@bufbuild/protobuf@2.7.0", "", {}, "sha512-qn6tAIZEw5i/wiESBF4nQxZkl86aY4KoO0IkUa2Lh+rya64oTOdJQFlZuMwI1Qz9VBJQrQC4QlSA2DNek5gCOA=="],
"@bugsnag/cuid": ["@bugsnag/cuid@3.2.1", "", {}, "sha512-zpvN8xQ5rdRWakMd/BcVkdn2F8HKlDSbM3l7duueK590WmI1T0ObTLc1V/1e55r14WNjPd5AJTYX4yPEAFVi+Q=="],
"@cerebras/cerebras_cloud_sdk": ["@cerebras/cerebras_cloud_sdk@1.46.0", "", { "dependencies": { "@types/node": "^18.11.18", "@types/node-fetch": "^2.6.4", "abort-controller": "^3.0.0", "agentkeepalive": "^4.2.1", "form-data-encoder": "1.7.2", "formdata-node": "^4.3.2", "node-fetch": "^2.6.7" } }, "sha512-5IFlD9jvZr5fRsHd4pL4sFkzDPSvHxzM0dmk3mJG2/biRBl+3EV6CdXCabf7J8kr8hyQyK67N0rQLnYy6NSh9w=="],
@@ -471,6 +474,10 @@
"@chatscope/chat-ui-kit-styles": ["@chatscope/chat-ui-kit-styles@1.4.0", "", {}, "sha512-016mBJD3DESw7Nh+lkKcPd22xG92ghA0VpIXIbjQtmXhC7Ve6wRazTy8z1Ahut+Tbv179+JxrftuMngsj/yV8Q=="],
"@connectrpc/connect": ["@connectrpc/connect@2.0.0-rc.3", "", { "peerDependencies": { "@bufbuild/protobuf": "^2.2.0" } }, "sha512-ARBt64yEyKbanyRETTjcjJuHr2YXorzQo0etyS5+P6oSeW8xEuzajA9g+zDnMcj1hlX2dQE93foIWQGfpru7gQ=="],
"@connectrpc/connect-web": ["@connectrpc/connect-web@2.0.0-rc.3", "", { "peerDependencies": { "@bufbuild/protobuf": "^2.2.0", "@connectrpc/connect": "2.0.0-rc.3" } }, "sha512-w88P8Lsn5CCsA7MFRl2e6oLY4J/5toiNtJns/YJrlyQaWOy3RO8pDgkz+iIkG98RPMhj2thuBvsd3Cn4DKKCkw=="],
"@csstools/color-helpers": ["@csstools/color-helpers@5.1.0", "", {}, "sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA=="],
"@csstools/css-calc": ["@csstools/css-calc@2.1.4", "", { "peerDependencies": { "@csstools/css-parser-algorithms": "^3.0.5", "@csstools/css-tokenizer": "^3.0.4" } }, "sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ=="],
@@ -485,6 +492,8 @@
"@drizzle-team/brocli": ["@drizzle-team/brocli@0.10.2", "", {}, "sha512-z33Il7l5dKjUgGULTqBsQBQwckHh5AbIuxhdsIxDDiZAzBOrZO6q9ogcWC65kU382AfynTfgNumVcNIjuIua6w=="],
"@e2b/code-interpreter": ["@e2b/code-interpreter@2.0.0", "", { "dependencies": { "e2b": "^2.0.1" } }, "sha512-rCIW4dV544sUx2YQB/hhwDrK6sQzIb5lr5h/1CIVoOIRgU9q3NUxsFj+2OsgWd4rMG8l6b/oA7FVZJwP4sGX/A=="],
"@electric-sql/client": ["@electric-sql/client@1.0.0-beta.1", "", { "optionalDependencies": { "@rollup/rollup-darwin-arm64": "^4.18.1" } }, "sha512-Ei9jN3pDoGzc+a/bGqnB5ajb52IvSv7/n2btuyzUlcOHIR2kM9fqtYTJXPwZYKLkGZlHWlpHgWyRtrinkP2nHg=="],
"@emnapi/runtime": ["@emnapi/runtime@1.5.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-97/BJ3iXHww3djw6hYIfErCZFee7qCtrneuLa20UXFCOTCfBM2cvQHjWJ2EG0s0MtdNwInarqCTz35i4wWXHsQ=="],
@@ -1741,6 +1750,8 @@
"commondir": ["commondir@1.0.1", "", {}, "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg=="],
"compare-versions": ["compare-versions@6.1.1", "", {}, "sha512-4hm4VPpIecmlg59CHXnRDnqGplJFrbLG4aFEl5vl6cK1u76ws3LLvX7ikFnTDl5vo39sjWD6AaDPYodJp/NNHg=="],
"compute-scroll-into-view": ["compute-scroll-into-view@3.1.1", "", {}, "sha512-VRhuHOLoKYOy4UbilLbUzbYg93XLjv2PncJC50EuTWPA3gaja1UjBsUP/D/9/juV3vQFr6XBEzn9KCAHdUvOHw=="],
"concat-stream": ["concat-stream@2.0.0", "", { "dependencies": { "buffer-from": "^1.0.0", "inherits": "^2.0.3", "readable-stream": "^3.0.2", "typedarray": "^0.0.6" } }, "sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A=="],
@@ -1905,6 +1916,8 @@
"dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="],
"e2b": ["e2b@2.0.1", "", { "dependencies": { "@bufbuild/protobuf": "^2.6.2", "@connectrpc/connect": "2.0.0-rc.3", "@connectrpc/connect-web": "2.0.0-rc.3", "compare-versions": "^6.1.0", "openapi-fetch": "^0.9.7", "platform": "^1.3.6" } }, "sha512-wJgZTV1QFeh5WKQ23n6hWmMODPmyKiiWaQy+uxvV/5M9NH/zMY/ONzhh7j7ONYgeH8jdRZxOS2e+G345EodGeA=="],
"eastasianwidth": ["eastasianwidth@0.2.0", "", {}, "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA=="],
"ecdsa-sig-formatter": ["ecdsa-sig-formatter@1.0.11", "", { "dependencies": { "safe-buffer": "^5.0.1" } }, "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ=="],
@@ -2601,6 +2614,10 @@
"openai": ["openai@4.104.0", "", { "dependencies": { "@types/node": "^18.11.18", "@types/node-fetch": "^2.6.4", "abort-controller": "^3.0.0", "agentkeepalive": "^4.2.1", "form-data-encoder": "1.7.2", "formdata-node": "^4.3.2", "node-fetch": "^2.6.7" }, "peerDependencies": { "ws": "^8.18.0", "zod": "^3.23.8" }, "optionalPeers": ["ws", "zod"], "bin": { "openai": "bin/cli" } }, "sha512-p99EFNsA/yX6UhVO93f5kJsDRLAg+CTA2RBqdHK4RtK8u5IJw32Hyb2dTGKbnnFmnuoBv5r7Z2CURI9sGZpSuA=="],
"openapi-fetch": ["openapi-fetch@0.9.8", "", { "dependencies": { "openapi-typescript-helpers": "^0.0.8" } }, "sha512-zM6elH0EZStD/gSiNlcPrzXcVQ/pZo3BDvC6CDwRDUt1dDzxlshpmQnpD6cZaJ39THaSmwVCxxRrPKNM1hHrDg=="],
"openapi-typescript-helpers": ["openapi-typescript-helpers@0.0.8", "", {}, "sha512-1eNjQtbfNi5Z/kFhagDIaIRj6qqDzhjNJKz8cmMW0CVdGwT6e1GLbAfgI0d28VTJa1A8jz82jm/4dG8qNoNS8g=="],
"opentracing": ["opentracing@0.14.7", "", {}, "sha512-vz9iS7MJ5+Bp1URw8Khvdyw1H/hGvzHWlKQ7eRrQojSCDL1/SrWfrY9QebLw97n2deyRtzHRC3MkQfVNUCo91Q=="],
"option": ["option@0.2.4", "", {}, "sha512-pkEqbDyl8ou5cpq+VsnQbe/WlEy5qS7xPzMS1U55OCG9KPvwFD46zDbxQIj3egJSFc3D+XhYOPUzz49zQAVy7A=="],
@@ -2681,6 +2698,8 @@
"pkg-types": ["pkg-types@1.3.1", "", { "dependencies": { "confbox": "^0.1.8", "mlly": "^1.7.4", "pathe": "^2.0.1" } }, "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ=="],
"platform": ["platform@1.3.6", "", {}, "sha512-fnWVljUchTro6RiCFvCXBbNhJc2NijN7oIQxbwsyL0buWJPG85v81ehlHI9fXrJsMNgTofEoWIQeClKpgxFLrg=="],
"playwright": ["playwright@1.55.0", "", { "dependencies": { "playwright-core": "1.55.0" }, "optionalDependencies": { "fsevents": "2.3.2" }, "bin": { "playwright": "cli.js" } }, "sha512-sdCWStblvV1YU909Xqx0DhOjPZE4/5lJsIS84IfN9dAZfcl/CIZ5O8l3o0j7hPMjDvqoTF8ZUcc+i/GL5erstA=="],
"playwright-core": ["playwright-core@1.55.0", "", { "bin": { "playwright-core": "cli.js" } }, "sha512-GvZs4vU3U5ro2nZpeiwyb0zuFaqb9sUiAJuyrWpcGouD8y9/HLgGbNRjIph7zU9D3hnPaisMl9zG9CgFi/biIg=="],