Vercel AI SDK: Streaming Code Execution with HopX
The Vercel AI SDK makes building AI applications delightful. Streaming responses, tool calling, and React hooks that "just work." But when your AI needs to execute code, you hit a wall: where do you run it safely?
This tutorial shows how to integrate HopX sandboxes with the Vercel AI SDK for real-time, streaming code execution. Your users see output as it happens, character by character.
What We're Building
| 1 | ┌─────────────────────────────────────────────────────────────────┐ |
| 2 | │ User Chat Interface │ |
| 3 | │ │ |
| 4 | │ User: "Calculate the factorial of 100" │ |
| 5 | │ │ |
| 6 | │ Assistant: I'll calculate that for you... │ |
| 7 | │ │ |
| 8 | │ ┌────────────────────────────────────────────────────────┐ │ |
| 9 | │ │ >>> Executing Python... │ │ |
| 10 | │ │ factorial(100) = 933262154439441526816992388... │ │ ← Streaming |
| 11 | │ │ ✓ Completed in 0.3s │ │ |
| 12 | │ └────────────────────────────────────────────────────────┘ │ |
| 13 | │ │ |
| 14 | │ The factorial of 100 is a 158-digit number: 9.33×10^157 │ |
| 15 | └─────────────────────────────────────────────────────────────────┘ |
| 16 | |
Prerequisites
| 1 | npm install ai @ai-sdk/openai @hopx-ai/sdk |
| 2 | |
Set environment variables:
| 1 | OPENAI_API_KEY=sk-... |
| 2 | HOPX_API_KEY=... |
| 3 | |
Project Structure
| 1 | app/ |
| 2 | ├── api/ |
| 3 | │ └── chat/ |
| 4 | │ └── route.ts # AI chat endpoint with tool calling |
| 5 | ├── components/ |
| 6 | │ ├── chat.tsx # Chat UI component |
| 7 | │ └── code-output.tsx # Streaming code output display |
| 8 | └── page.tsx # Main page |
| 9 | |
Step 1: Create the Chat API Route
The API route handles chat messages and tool execution:
| 1 | // app/api/chat/route.ts |
| 2 | import { openai } from '@ai-sdk/openai'; |
| 3 | import { streamText, tool } from 'ai'; |
| 4 | import { z } from 'zod'; |
| 5 | import { Sandbox } from '@hopx-ai/sdk'; |
| 6 | |
| 7 | // Allow streaming responses up to 60 seconds |
| 8 | export const maxDuration = 60; |
| 9 | |
| 10 | export async function POST(req: Request) { |
| 11 | const { messages } = await req.json(); |
| 12 | |
| 13 | const result = streamText({ |
| 14 | model: openai('gpt-4o'), |
| 15 | system: `You are a helpful AI assistant that can execute Python code. |
| 16 | |
| 17 | When users ask you to calculate, analyze data, or do anything that requires computation: |
| 18 | 1. Write Python code to accomplish the task |
| 19 | 2. Use the execute_python tool to run it |
| 20 | 3. Explain the results clearly |
| 21 | |
| 22 | The sandbox has pandas, numpy, matplotlib, and standard libraries. |
| 23 | For charts, save to /app/output.png.`, |
| 24 | |
| 25 | messages, |
| 26 | |
| 27 | tools: { |
| 28 | execute_python: tool({ |
| 29 | description: 'Execute Python code in a secure sandbox. Use for calculations, data analysis, and any computational task.', |
| 30 | parameters: z.object({ |
| 31 | code: z.string().describe('Python code to execute'), |
| 32 | description: z.string().describe('Brief description of what this code does'), |
| 33 | }), |
| 34 | execute: async ({ code, description }) => { |
| 35 | const sandbox = await Sandbox.create({ |
| 36 | template: 'code-interpreter', |
| 37 | apiKey: process.env.HOPX_API_KEY, |
| 38 | }); |
| 39 | |
| 40 | try { |
| 41 | const result = await sandbox.runCode(code, { |
| 42 | language: 'python', |
| 43 | timeout: 30, |
| 44 | }); |
| 45 | |
| 46 | return { |
| 47 | success: result.exitCode === 0, |
| 48 | output: result.stdout || '', |
| 49 | error: result.stderr || '', |
| 50 | exitCode: result.exitCode, |
| 51 | description, |
| 52 | }; |
| 53 | } finally { |
| 54 | await sandbox.kill(); |
| 55 | } |
| 56 | }, |
| 57 | }), |
| 58 | }, |
| 59 | |
| 60 | // Maximum tool invocations per message |
| 61 | maxSteps: 5, |
| 62 | }); |
| 63 | |
| 64 | return result.toDataStreamResponse(); |
| 65 | } |
| 66 | |
Step 2: Create the Chat Component
A React component using the useChat hook:
| 1 | // app/components/chat.tsx |
| 2 | 'use client'; |
| 3 | |
| 4 | import { useChat } from 'ai/react'; |
| 5 | import { CodeOutput } from './code-output'; |
| 6 | |
| 7 | export function Chat() { |
| 8 | const { messages, input, handleInputChange, handleSubmit, isLoading } = useChat({ |
| 9 | api: '/api/chat', |
| 10 | }); |
| 11 | |
| 12 | return ( |
| 13 | <div className="flex flex-col h-screen max-w-3xl mx-auto p-4"> |
| 14 | {/* Messages */} |
| 15 | <div className="flex-1 overflow-y-auto space-y-4 mb-4"> |
| 16 | {messages.map((message) => ( |
| 17 | <div |
| 18 | key={message.id} |
| 19 | className={`flex ${ |
| 20 | message.role === 'user' ? 'justify-end' : 'justify-start' |
| 21 | }`} |
| 22 | > |
| 23 | <div |
| 24 | className={`max-w-[80%] rounded-lg px-4 py-2 ${ |
| 25 | message.role === 'user' |
| 26 | ? 'bg-blue-600 text-white' |
| 27 | : 'bg-gray-100 text-gray-900' |
| 28 | }`} |
| 29 | > |
| 30 | {/* Render text content */} |
| 31 | <p className="whitespace-pre-wrap">{message.content}</p> |
| 32 | |
| 33 | {/* Render tool invocations */} |
| 34 | {message.toolInvocations?.map((invocation) => ( |
| 35 | <CodeOutput |
| 36 | key={invocation.toolCallId} |
| 37 | invocation={invocation} |
| 38 | /> |
| 39 | ))} |
| 40 | </div> |
| 41 | </div> |
| 42 | ))} |
| 43 | |
| 44 | {isLoading && ( |
| 45 | <div className="flex justify-start"> |
| 46 | <div className="bg-gray-100 rounded-lg px-4 py-2"> |
| 47 | <span className="animate-pulse">Thinking...</span> |
| 48 | </div> |
| 49 | </div> |
| 50 | )} |
| 51 | </div> |
| 52 | |
| 53 | {/* Input form */} |
| 54 | <form onSubmit={handleSubmit} className="flex gap-2"> |
| 55 | <input |
| 56 | value={input} |
| 57 | onChange={handleInputChange} |
| 58 | placeholder="Ask me to calculate something..." |
| 59 | className="flex-1 px-4 py-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500" |
| 60 | disabled={isLoading} |
| 61 | /> |
| 62 | <button |
| 63 | type="submit" |
| 64 | disabled={isLoading || !input.trim()} |
| 65 | className="px-6 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed" |
| 66 | > |
| 67 | Send |
| 68 | </button> |
| 69 | </form> |
| 70 | </div> |
| 71 | ); |
| 72 | } |
| 73 | |
Step 3: Create the Code Output Component
Display code execution results with syntax highlighting:
| 1 | // app/components/code-output.tsx |
| 2 | 'use client'; |
| 3 | |
| 4 | import { ToolInvocation } from 'ai'; |
| 5 | |
| 6 | interface CodeOutputProps { |
| 7 | invocation: ToolInvocation; |
| 8 | } |
| 9 | |
| 10 | export function CodeOutput({ invocation }: CodeOutputProps) { |
| 11 | // Extract tool result if available |
| 12 | const result = 'result' in invocation ? invocation.result : null; |
| 13 | const args = invocation.args as { code: string; description: string }; |
| 14 | |
| 15 | const isComplete = 'result' in invocation; |
| 16 | const success = result?.success; |
| 17 | |
| 18 | return ( |
| 19 | <div className="mt-3 rounded-lg border border-gray-200 overflow-hidden"> |
| 20 | {/* Header */} |
| 21 | <div className="flex items-center justify-between px-3 py-2 bg-gray-50 border-b border-gray-200"> |
| 22 | <div className="flex items-center gap-2"> |
| 23 | <span className="text-xs font-medium text-gray-500">Python</span> |
| 24 | <span className="text-xs text-gray-400">{args.description}</span> |
| 25 | </div> |
| 26 | <div className="flex items-center gap-2"> |
| 27 | {!isComplete && ( |
| 28 | <span className="flex items-center gap-1 text-xs text-amber-600"> |
| 29 | <span className="w-2 h-2 bg-amber-500 rounded-full animate-pulse" /> |
| 30 | Executing... |
| 31 | </span> |
| 32 | )} |
| 33 | {isComplete && success && ( |
| 34 | <span className="flex items-center gap-1 text-xs text-green-600"> |
| 35 | <span className="w-2 h-2 bg-green-500 rounded-full" /> |
| 36 | Success |
| 37 | </span> |
| 38 | )} |
| 39 | {isComplete && !success && ( |
| 40 | <span className="flex items-center gap-1 text-xs text-red-600"> |
| 41 | <span className="w-2 h-2 bg-red-500 rounded-full" /> |
| 42 | Error |
| 43 | </span> |
| 44 | )} |
| 45 | </div> |
| 46 | </div> |
| 47 | |
| 48 | {/* Code */} |
| 49 | <div className="bg-gray-900 p-3"> |
| 50 | <pre className="text-sm text-gray-100 overflow-x-auto"> |
| 51 | <code>{args.code}</code> |
| 52 | </pre> |
| 53 | </div> |
| 54 | |
| 55 | {/* Output */} |
| 56 | {isComplete && ( |
| 57 | <div className="border-t border-gray-200"> |
| 58 | <div className="px-3 py-1 bg-gray-50 border-b border-gray-200"> |
| 59 | <span className="text-xs font-medium text-gray-500">Output</span> |
| 60 | </div> |
| 61 | <div className="p-3 bg-black"> |
| 62 | {result.output && ( |
| 63 | <pre className="text-sm text-green-400 whitespace-pre-wrap"> |
| 64 | {result.output} |
| 65 | </pre> |
| 66 | )} |
| 67 | {result.error && ( |
| 68 | <pre className="text-sm text-red-400 whitespace-pre-wrap"> |
| 69 | {result.error} |
| 70 | </pre> |
| 71 | )} |
| 72 | {!result.output && !result.error && ( |
| 73 | <span className="text-sm text-gray-500 italic">No output</span> |
| 74 | )} |
| 75 | </div> |
| 76 | </div> |
| 77 | )} |
| 78 | </div> |
| 79 | ); |
| 80 | } |
| 81 | |
Step 4: Wire It Up
Create the main page:
| 1 | // app/page.tsx |
| 2 | import { Chat } from './components/chat'; |
| 3 | |
| 4 | export default function Home() { |
| 5 | return ( |
| 6 | <main className="min-h-screen bg-white"> |
| 7 | <Chat /> |
| 8 | </main> |
| 9 | ); |
| 10 | } |
| 11 | |
Advanced: True Streaming Output
The basic implementation waits for code to finish before showing output. For real-time streaming, we need a custom approach:
Streaming API Route
| 1 | // app/api/execute/route.ts |
| 2 | import { NextRequest } from 'next/server'; |
| 3 | import { Sandbox } from '@hopx-ai/sdk'; |
| 4 | |
| 5 | export async function POST(req: NextRequest) { |
| 6 | const { code, language = 'python' } = await req.json(); |
| 7 | |
| 8 | const encoder = new TextEncoder(); |
| 9 | |
| 10 | const stream = new ReadableStream({ |
| 11 | async start(controller) { |
| 12 | const sandbox = await Sandbox.create({ |
| 13 | template: 'code-interpreter', |
| 14 | apiKey: process.env.HOPX_API_KEY, |
| 15 | }); |
| 16 | |
| 17 | try { |
| 18 | // Send status |
| 19 | controller.enqueue( |
| 20 | encoder.encode(`data: ${JSON.stringify({ type: 'status', message: 'Executing...' })}\n\n`) |
| 21 | ); |
| 22 | |
| 23 | const result = await sandbox.runCode(code, { |
| 24 | language, |
| 25 | timeout: 60, |
| 26 | }); |
| 27 | |
| 28 | // Send output |
| 29 | if (result.stdout) { |
| 30 | controller.enqueue( |
| 31 | encoder.encode(`data: ${JSON.stringify({ type: 'stdout', text: result.stdout })}\n\n`) |
| 32 | ); |
| 33 | } |
| 34 | |
| 35 | if (result.stderr) { |
| 36 | controller.enqueue( |
| 37 | encoder.encode(`data: ${JSON.stringify({ type: 'stderr', text: result.stderr })}\n\n`) |
| 38 | ); |
| 39 | } |
| 40 | |
| 41 | // Send completion |
| 42 | controller.enqueue( |
| 43 | encoder.encode(`data: ${JSON.stringify({ |
| 44 | type: 'done', |
| 45 | exitCode: result.exitCode, |
| 46 | success: result.exitCode === 0 |
| 47 | })}\n\n`) |
| 48 | ); |
| 49 | |
| 50 | } catch (error) { |
| 51 | controller.enqueue( |
| 52 | encoder.encode(`data: ${JSON.stringify({ |
| 53 | type: 'error', |
| 54 | message: error instanceof Error ? error.message : 'Execution failed' |
| 55 | })}\n\n`) |
| 56 | ); |
| 57 | } finally { |
| 58 | await sandbox.kill(); |
| 59 | controller.close(); |
| 60 | } |
| 61 | }, |
| 62 | }); |
| 63 | |
| 64 | return new Response(stream, { |
| 65 | headers: { |
| 66 | 'Content-Type': 'text/event-stream', |
| 67 | 'Cache-Control': 'no-cache', |
| 68 | 'Connection': 'keep-alive', |
| 69 | }, |
| 70 | }); |
| 71 | } |
| 72 | |
Streaming Hook
| 1 | // hooks/use-code-execution.ts |
| 2 | 'use client'; |
| 3 | |
| 4 | import { useState, useCallback } from 'react'; |
| 5 | |
| 6 | interface ExecutionState { |
| 7 | status: 'idle' | 'running' | 'done' | 'error'; |
| 8 | stdout: string; |
| 9 | stderr: string; |
| 10 | exitCode: number | null; |
| 11 | error: string | null; |
| 12 | } |
| 13 | |
| 14 | export function useCodeExecution() { |
| 15 | const [state, setState] = useState<ExecutionState>({ |
| 16 | status: 'idle', |
| 17 | stdout: '', |
| 18 | stderr: '', |
| 19 | exitCode: null, |
| 20 | error: null, |
| 21 | }); |
| 22 | |
| 23 | const execute = useCallback(async (code: string, language = 'python') => { |
| 24 | setState({ |
| 25 | status: 'running', |
| 26 | stdout: '', |
| 27 | stderr: '', |
| 28 | exitCode: null, |
| 29 | error: null, |
| 30 | }); |
| 31 | |
| 32 | try { |
| 33 | const response = await fetch('/api/execute', { |
| 34 | method: 'POST', |
| 35 | headers: { 'Content-Type': 'application/json' }, |
| 36 | body: JSON.stringify({ code, language }), |
| 37 | }); |
| 38 | |
| 39 | const reader = response.body?.getReader(); |
| 40 | const decoder = new TextDecoder(); |
| 41 | |
| 42 | if (!reader) { |
| 43 | throw new Error('No response body'); |
| 44 | } |
| 45 | |
| 46 | let buffer = ''; |
| 47 | |
| 48 | while (true) { |
| 49 | const { done, value } = await reader.read(); |
| 50 | if (done) break; |
| 51 | |
| 52 | buffer += decoder.decode(value, { stream: true }); |
| 53 | const lines = buffer.split('\n'); |
| 54 | buffer = lines.pop() || ''; |
| 55 | |
| 56 | for (const line of lines) { |
| 57 | if (line.startsWith('data: ')) { |
| 58 | try { |
| 59 | const data = JSON.parse(line.slice(6)); |
| 60 | |
| 61 | switch (data.type) { |
| 62 | case 'status': |
| 63 | // Update status message if needed |
| 64 | break; |
| 65 | |
| 66 | case 'stdout': |
| 67 | setState((prev) => ({ |
| 68 | ...prev, |
| 69 | stdout: prev.stdout + data.text, |
| 70 | })); |
| 71 | break; |
| 72 | |
| 73 | case 'stderr': |
| 74 | setState((prev) => ({ |
| 75 | ...prev, |
| 76 | stderr: prev.stderr + data.text, |
| 77 | })); |
| 78 | break; |
| 79 | |
| 80 | case 'done': |
| 81 | setState((prev) => ({ |
| 82 | ...prev, |
| 83 | status: 'done', |
| 84 | exitCode: data.exitCode, |
| 85 | })); |
| 86 | break; |
| 87 | |
| 88 | case 'error': |
| 89 | setState((prev) => ({ |
| 90 | ...prev, |
| 91 | status: 'error', |
| 92 | error: data.message, |
| 93 | })); |
| 94 | break; |
| 95 | } |
| 96 | } catch { |
| 97 | // Ignore parse errors |
| 98 | } |
| 99 | } |
| 100 | } |
| 101 | } |
| 102 | } catch (error) { |
| 103 | setState((prev) => ({ |
| 104 | ...prev, |
| 105 | status: 'error', |
| 106 | error: error instanceof Error ? error.message : 'Unknown error', |
| 107 | })); |
| 108 | } |
| 109 | }, []); |
| 110 | |
| 111 | const reset = useCallback(() => { |
| 112 | setState({ |
| 113 | status: 'idle', |
| 114 | stdout: '', |
| 115 | stderr: '', |
| 116 | exitCode: null, |
| 117 | error: null, |
| 118 | }); |
| 119 | }, []); |
| 120 | |
| 121 | return { ...state, execute, reset }; |
| 122 | } |
| 123 | |
Streaming Code Block Component
| 1 | // components/streaming-code-block.tsx |
| 2 | 'use client'; |
| 3 | |
| 4 | import { useState } from 'react'; |
| 5 | import { useCodeExecution } from '@/hooks/use-code-execution'; |
| 6 | import { Play, Loader2, RotateCcw } from 'lucide-react'; |
| 7 | |
| 8 | interface StreamingCodeBlockProps { |
| 9 | code: string; |
| 10 | language?: string; |
| 11 | } |
| 12 | |
| 13 | export function StreamingCodeBlock({ code, language = 'python' }: StreamingCodeBlockProps) { |
| 14 | const { status, stdout, stderr, exitCode, error, execute, reset } = useCodeExecution(); |
| 15 | const [showOutput, setShowOutput] = useState(false); |
| 16 | |
| 17 | const handleRun = async () => { |
| 18 | setShowOutput(true); |
| 19 | await execute(code, language); |
| 20 | }; |
| 21 | |
| 22 | return ( |
| 23 | <div className="rounded-lg border border-gray-200 overflow-hidden"> |
| 24 | {/* Code header */} |
| 25 | <div className="flex items-center justify-between px-4 py-2 bg-gray-50 border-b"> |
| 26 | <span className="text-sm font-mono text-gray-600">{language}</span> |
| 27 | <div className="flex items-center gap-2"> |
| 28 | {status === 'idle' && ( |
| 29 | <button |
| 30 | onClick={handleRun} |
| 31 | className="flex items-center gap-1 px-3 py-1 text-sm bg-green-600 text-white rounded hover:bg-green-700" |
| 32 | > |
| 33 | <Play className="w-4 h-4" /> |
| 34 | Run |
| 35 | </button> |
| 36 | )} |
| 37 | {status === 'running' && ( |
| 38 | <span className="flex items-center gap-1 text-sm text-amber-600"> |
| 39 | <Loader2 className="w-4 h-4 animate-spin" /> |
| 40 | Running... |
| 41 | </span> |
| 42 | )} |
| 43 | {(status === 'done' || status === 'error') && ( |
| 44 | <button |
| 45 | onClick={() => { |
| 46 | reset(); |
| 47 | setShowOutput(false); |
| 48 | }} |
| 49 | className="flex items-center gap-1 px-3 py-1 text-sm bg-gray-600 text-white rounded hover:bg-gray-700" |
| 50 | > |
| 51 | <RotateCcw className="w-4 h-4" /> |
| 52 | Reset |
| 53 | </button> |
| 54 | )} |
| 55 | </div> |
| 56 | </div> |
| 57 | |
| 58 | {/* Code content */} |
| 59 | <pre className="p-4 bg-gray-900 text-gray-100 overflow-x-auto"> |
| 60 | <code>{code}</code> |
| 61 | </pre> |
| 62 | |
| 63 | {/* Output panel */} |
| 64 | {showOutput && ( |
| 65 | <div className="border-t border-gray-200"> |
| 66 | <div className="px-4 py-2 bg-gray-50 border-b flex items-center justify-between"> |
| 67 | <span className="text-sm font-medium text-gray-600">Output</span> |
| 68 | {status === 'done' && exitCode !== null && ( |
| 69 | <span className={`text-xs ${exitCode === 0 ? 'text-green-600' : 'text-red-600'}`}> |
| 70 | Exit code: {exitCode} |
| 71 | </span> |
| 72 | )} |
| 73 | </div> |
| 74 | <div className="p-4 bg-black min-h-[100px] max-h-[300px] overflow-auto font-mono text-sm"> |
| 75 | {stdout && <pre className="text-green-400 whitespace-pre-wrap">{stdout}</pre>} |
| 76 | {stderr && <pre className="text-red-400 whitespace-pre-wrap">{stderr}</pre>} |
| 77 | {error && <pre className="text-red-400">{error}</pre>} |
| 78 | {status === 'running' && !stdout && !stderr && ( |
| 79 | <span className="text-gray-500 animate-pulse">Waiting for output...</span> |
| 80 | )} |
| 81 | </div> |
| 82 | </div> |
| 83 | )} |
| 84 | </div> |
| 85 | ); |
| 86 | } |
| 87 | |
Integration with AI SDK Tools
Combine the streaming execution with AI SDK tool calling:
| 1 | // app/api/chat/route.ts |
| 2 | import { openai } from '@ai-sdk/openai'; |
| 3 | import { streamText, tool } from 'ai'; |
| 4 | import { z } from 'zod'; |
| 5 | import { Sandbox } from '@hopx-ai/sdk'; |
| 6 | |
| 7 | // Persistent sandbox for conversation |
| 8 | let sandbox: Sandbox | null = null; |
| 9 | |
| 10 | async function getOrCreateSandbox() { |
| 11 | if (!sandbox) { |
| 12 | sandbox = await Sandbox.create({ |
| 13 | template: 'code-interpreter', |
| 14 | apiKey: process.env.HOPX_API_KEY, |
| 15 | ttl: 300, // 5 minutes |
| 16 | }); |
| 17 | } |
| 18 | return sandbox; |
| 19 | } |
| 20 | |
| 21 | export async function POST(req: Request) { |
| 22 | const { messages } = await req.json(); |
| 23 | |
| 24 | const result = streamText({ |
| 25 | model: openai('gpt-4o'), |
| 26 | messages, |
| 27 | |
| 28 | tools: { |
| 29 | execute_python: tool({ |
| 30 | description: 'Execute Python code. State persists between calls.', |
| 31 | parameters: z.object({ |
| 32 | code: z.string(), |
| 33 | }), |
| 34 | execute: async ({ code }) => { |
| 35 | const sb = await getOrCreateSandbox(); |
| 36 | |
| 37 | const result = await sb.runCode(code, { |
| 38 | language: 'python', |
| 39 | timeout: 30, |
| 40 | }); |
| 41 | |
| 42 | return { |
| 43 | output: result.stdout, |
| 44 | error: result.stderr, |
| 45 | success: result.exitCode === 0, |
| 46 | }; |
| 47 | }, |
| 48 | }), |
| 49 | |
| 50 | install_package: tool({ |
| 51 | description: 'Install a Python package using pip', |
| 52 | parameters: z.object({ |
| 53 | package: z.string().describe('Package name to install'), |
| 54 | }), |
| 55 | execute: async ({ package: pkg }) => { |
| 56 | const sb = await getOrCreateSandbox(); |
| 57 | |
| 58 | const result = await sb.runCode(`pip install ${pkg}`, { |
| 59 | language: 'bash', |
| 60 | timeout: 60, |
| 61 | }); |
| 62 | |
| 63 | return { |
| 64 | success: result.exitCode === 0, |
| 65 | output: result.stdout, |
| 66 | error: result.stderr, |
| 67 | }; |
| 68 | }, |
| 69 | }), |
| 70 | |
| 71 | read_file: tool({ |
| 72 | description: 'Read a file from the sandbox', |
| 73 | parameters: z.object({ |
| 74 | path: z.string().describe('File path to read'), |
| 75 | }), |
| 76 | execute: async ({ path }) => { |
| 77 | const sb = await getOrCreateSandbox(); |
| 78 | |
| 79 | try { |
| 80 | const content = await sb.files.read(path); |
| 81 | return { success: true, content }; |
| 82 | } catch (e) { |
| 83 | return { |
| 84 | success: false, |
| 85 | error: e instanceof Error ? e.message : 'File not found' |
| 86 | }; |
| 87 | } |
| 88 | }, |
| 89 | }), |
| 90 | |
| 91 | write_file: tool({ |
| 92 | description: 'Write content to a file in the sandbox', |
| 93 | parameters: z.object({ |
| 94 | path: z.string().describe('File path to write'), |
| 95 | content: z.string().describe('Content to write'), |
| 96 | }), |
| 97 | execute: async ({ path, content }) => { |
| 98 | const sb = await getOrCreateSandbox(); |
| 99 | |
| 100 | try { |
| 101 | await sb.files.write(path, content); |
| 102 | return { success: true, path }; |
| 103 | } catch (e) { |
| 104 | return { |
| 105 | success: false, |
| 106 | error: e instanceof Error ? e.message : 'Write failed' |
| 107 | }; |
| 108 | } |
| 109 | }, |
| 110 | }), |
| 111 | }, |
| 112 | |
| 113 | maxSteps: 10, |
| 114 | }); |
| 115 | |
| 116 | return result.toDataStreamResponse(); |
| 117 | } |
| 118 | |
Multi-Language Support
Extend the tools to support multiple languages:
| 1 | import { z } from 'zod'; |
| 2 | |
| 3 | const executeCode = tool({ |
| 4 | description: 'Execute code in Python, JavaScript, TypeScript, or Bash', |
| 5 | parameters: z.object({ |
| 6 | code: z.string().describe('Code to execute'), |
| 7 | language: z.enum(['python', 'javascript', 'typescript', 'bash']) |
| 8 | .describe('Programming language'), |
| 9 | }), |
| 10 | execute: async ({ code, language }) => { |
| 11 | const sandbox = await Sandbox.create({ |
| 12 | template: 'code-interpreter', |
| 13 | apiKey: process.env.HOPX_API_KEY, |
| 14 | }); |
| 15 | |
| 16 | try { |
| 17 | const result = await sandbox.runCode(code, { |
| 18 | language, |
| 19 | timeout: 30, |
| 20 | }); |
| 21 | |
| 22 | return { |
| 23 | language, |
| 24 | output: result.stdout, |
| 25 | error: result.stderr, |
| 26 | success: result.exitCode === 0, |
| 27 | }; |
| 28 | } finally { |
| 29 | await sandbox.kill(); |
| 30 | } |
| 31 | }, |
| 32 | }); |
| 33 | |
Error Handling Best Practices
| 1 | // Wrap tool execution with error handling |
| 2 | const safeExecute = async (fn: () => Promise<any>) => { |
| 3 | try { |
| 4 | return await fn(); |
| 5 | } catch (error) { |
| 6 | if (error instanceof Error) { |
| 7 | // Check for specific error types |
| 8 | if (error.message.includes('timeout')) { |
| 9 | return { |
| 10 | success: false, |
| 11 | error: 'Code execution timed out. Try simplifying your code.', |
| 12 | }; |
| 13 | } |
| 14 | if (error.message.includes('memory')) { |
| 15 | return { |
| 16 | success: false, |
| 17 | error: 'Out of memory. Try processing smaller data chunks.', |
| 18 | }; |
| 19 | } |
| 20 | } |
| 21 | return { |
| 22 | success: false, |
| 23 | error: 'Execution failed. Please try again.', |
| 24 | }; |
| 25 | } |
| 26 | }; |
| 27 | |
| 28 | // Use in tool |
| 29 | execute: async ({ code }) => { |
| 30 | return safeExecute(async () => { |
| 31 | const sandbox = await Sandbox.create({ ... }); |
| 32 | // ... execution code |
| 33 | }); |
| 34 | }, |
| 35 | |
Production Considerations
1. Rate Limiting
| 1 | import { Ratelimit } from '@upstash/ratelimit'; |
| 2 | import { Redis } from '@upstash/redis'; |
| 3 | |
| 4 | const ratelimit = new Ratelimit({ |
| 5 | redis: Redis.fromEnv(), |
| 6 | limiter: Ratelimit.slidingWindow(10, '1 m'), // 10 requests per minute |
| 7 | }); |
| 8 | |
| 9 | export async function POST(req: Request) { |
| 10 | const ip = req.headers.get('x-forwarded-for') ?? 'anonymous'; |
| 11 | const { success } = await ratelimit.limit(ip); |
| 12 | |
| 13 | if (!success) { |
| 14 | return new Response('Rate limit exceeded', { status: 429 }); |
| 15 | } |
| 16 | |
| 17 | // ... rest of handler |
| 18 | } |
| 19 | |
2. Input Validation
| 1 | const MAX_CODE_LENGTH = 10000; |
| 2 | |
| 3 | execute: async ({ code }) => { |
| 4 | if (code.length > MAX_CODE_LENGTH) { |
| 5 | return { |
| 6 | success: false, |
| 7 | error: `Code exceeds maximum length of ${MAX_CODE_LENGTH} characters`, |
| 8 | }; |
| 9 | } |
| 10 | // ... execution |
| 11 | }, |
| 12 | |
3. Sandbox Pooling
| 1 | // For high-traffic applications, maintain a pool of warm sandboxes |
| 2 | class SandboxPool { |
| 3 | private pool: Sandbox[] = []; |
| 4 | private maxSize = 5; |
| 5 | |
| 6 | async acquire(): Promise<Sandbox> { |
| 7 | if (this.pool.length > 0) { |
| 8 | return this.pool.pop()!; |
| 9 | } |
| 10 | return Sandbox.create({ template: 'code-interpreter' }); |
| 11 | } |
| 12 | |
| 13 | release(sandbox: Sandbox) { |
| 14 | if (this.pool.length < this.maxSize) { |
| 15 | this.pool.push(sandbox); |
| 16 | } else { |
| 17 | sandbox.kill(); |
| 18 | } |
| 19 | } |
| 20 | } |
| 21 | |
Complete Example
Here's a full working Next.js app:
| 1 | // app/api/chat/route.ts |
| 2 | import { openai } from '@ai-sdk/openai'; |
| 3 | import { streamText, tool } from 'ai'; |
| 4 | import { z } from 'zod'; |
| 5 | import { Sandbox } from '@hopx-ai/sdk'; |
| 6 | |
| 7 | export const maxDuration = 60; |
| 8 | |
| 9 | export async function POST(req: Request) { |
| 10 | const { messages } = await req.json(); |
| 11 | |
| 12 | const result = streamText({ |
| 13 | model: openai('gpt-4o'), |
| 14 | system: `You are a helpful coding assistant. Execute Python code to answer questions.`, |
| 15 | messages, |
| 16 | tools: { |
| 17 | python: tool({ |
| 18 | description: 'Execute Python code', |
| 19 | parameters: z.object({ code: z.string() }), |
| 20 | execute: async ({ code }) => { |
| 21 | const sandbox = await Sandbox.create({ |
| 22 | template: 'code-interpreter', |
| 23 | apiKey: process.env.HOPX_API_KEY, |
| 24 | }); |
| 25 | try { |
| 26 | const result = await sandbox.runCode(code, { |
| 27 | language: 'python', |
| 28 | timeout: 30, |
| 29 | }); |
| 30 | return { |
| 31 | output: result.stdout || 'No output', |
| 32 | error: result.stderr, |
| 33 | success: result.exitCode === 0, |
| 34 | }; |
| 35 | } finally { |
| 36 | await sandbox.kill(); |
| 37 | } |
| 38 | }, |
| 39 | }), |
| 40 | }, |
| 41 | maxSteps: 5, |
| 42 | }); |
| 43 | |
| 44 | return result.toDataStreamResponse(); |
| 45 | } |
| 46 | |
| 1 | // app/page.tsx |
| 2 | 'use client'; |
| 3 | |
| 4 | import { useChat } from 'ai/react'; |
| 5 | |
| 6 | export default function Home() { |
| 7 | const { messages, input, handleInputChange, handleSubmit, isLoading } = useChat(); |
| 8 | |
| 9 | return ( |
| 10 | <div className="max-w-2xl mx-auto p-4"> |
| 11 | <h1 className="text-2xl font-bold mb-4">AI Code Assistant</h1> |
| 12 | |
| 13 | <div className="space-y-4 mb-4"> |
| 14 | {messages.map((m) => ( |
| 15 | <div key={m.id} className={m.role === 'user' ? 'text-right' : ''}> |
| 16 | <div className={`inline-block p-3 rounded-lg ${ |
| 17 | m.role === 'user' ? 'bg-blue-600 text-white' : 'bg-gray-100' |
| 18 | }`}> |
| 19 | <p>{m.content}</p> |
| 20 | {m.toolInvocations?.map((t) => ( |
| 21 | <pre key={t.toolCallId} className="mt-2 p-2 bg-black text-green-400 rounded text-sm"> |
| 22 | {'result' in t ? t.result.output : 'Executing...'} |
| 23 | </pre> |
| 24 | ))} |
| 25 | </div> |
| 26 | </div> |
| 27 | ))} |
| 28 | </div> |
| 29 | |
| 30 | <form onSubmit={handleSubmit} className="flex gap-2"> |
| 31 | <input |
| 32 | value={input} |
| 33 | onChange={handleInputChange} |
| 34 | placeholder="Ask me to calculate something..." |
| 35 | className="flex-1 p-2 border rounded" |
| 36 | /> |
| 37 | <button |
| 38 | type="submit" |
| 39 | disabled={isLoading} |
| 40 | className="px-4 py-2 bg-blue-600 text-white rounded disabled:opacity-50" |
| 41 | > |
| 42 | Send |
| 43 | </button> |
| 44 | </form> |
| 45 | </div> |
| 46 | ); |
| 47 | } |
| 48 | |
Conclusion
The Vercel AI SDK + HopX combination gives you:
- Streaming responses: Users see AI thinking in real-time
- Secure execution: Code runs in isolated sandboxes
- Tool calling: Clean integration with function calling
- React-first: Hooks that work seamlessly with Next.js
This pattern works for any AI application that needs to execute untrusted code—from coding assistants to data analysis tools.
Ready to add code execution to your AI app? Get started with HopX — sandboxes that spin up in 100ms.
Further Reading
- Vercel AI SDK Documentation — Official docs
- LangChain Tools with Secure Execution — LangChain integration
- Build a Code Interpreter — Full agent tutorial
- Streaming Code Execution — Deep dive into streaming