Class: Daytona::CodeInterpreter

Inherits:
Object
  • Object
show all
Includes:
Instrumentation
Defined in:
lib/daytona/code_interpreter.rb

Overview

Handles code interpretation and execution within a Sandbox. Currently supports only Python.

This class provides methods to execute code in isolated interpreter contexts, manage contexts, and stream execution output via callbacks. If subsequent code executions are performed in the same context, the variables, imports, and functions defined in the previous execution will be available.

For other languages, use the ‘code_run` method from the `Process` interface, or execute the appropriate command directly in the sandbox terminal.

Constant Summary collapse

WEBSOCKET_TIMEOUT_CODE =
4008

Instance Method Summary collapse

Methods included from Instrumentation

included

Constructor Details

#initialize(sandbox_id:, toolbox_api:, get_preview_link:, otel_state: nil) ⇒ CodeInterpreter

Returns a new instance of CodeInterpreter.

Parameters:

  • sandbox_id (String)
  • toolbox_api (DaytonaToolboxApiClient::InterpreterApi)
  • get_preview_link (Proc)
  • otel_state (Daytona::OtelState, nil) (defaults to: nil)


28
29
30
31
32
33
# File 'lib/daytona/code_interpreter.rb', line 28

def initialize(sandbox_id:, toolbox_api:, get_preview_link:, otel_state: nil)
  @sandbox_id = sandbox_id
  @toolbox_api = toolbox_api
  @get_preview_link = get_preview_link
  @otel_state = otel_state
end

Instance Method Details

#create_context(cwd: nil) ⇒ DaytonaToolboxApiClient::InterpreterContext

Create a new isolated interpreter context.

Contexts provide isolated execution environments with their own global namespace. Variables, imports, and functions defined in one context don’t affect others.

Examples:

# Create isolated context
ctx = sandbox.code_interpreter.create_context

# Execute code in this context
sandbox.code_interpreter.run_code("x = 100", context: ctx)

# Variable only exists in this context
result = sandbox.code_interpreter.run_code("print(x)", context: ctx)  # OK

# Won't see the variable in default context
result = sandbox.code_interpreter.run_code("print(x)")  # NameError

# Clean up
sandbox.code_interpreter.delete_context(ctx)

Parameters:

  • cwd (String, nil) (defaults to: nil)

    Working directory for the context

Returns:

  • (DaytonaToolboxApiClient::InterpreterContext)

Raises:



248
249
250
251
252
253
# File 'lib/daytona/code_interpreter.rb', line 248

def create_context(cwd: nil)
  request = DaytonaToolboxApiClient::CreateContextRequest.new(cwd:)
  @toolbox_api.create_interpreter_context(request)
rescue StandardError => e
  raise Sdk::Error, "Failed to create interpreter context: #{e.message}"
end

#delete_context(context) ⇒ void

This method returns an undefined value.

Delete an interpreter context and shut down all associated processes.

This permanently removes the context and all its state (variables, imports, etc.). The default context cannot be deleted.

Examples:

ctx = sandbox.code_interpreter.create_context
# ... use context ...
sandbox.code_interpreter.delete_context(ctx)

Parameters:

  • context (DaytonaToolboxApiClient::InterpreterContext)

Raises:



288
289
290
291
292
293
# File 'lib/daytona/code_interpreter.rb', line 288

def delete_context(context)
  @toolbox_api.delete_interpreter_context(context.id)
  nil
rescue StandardError => e
  raise Sdk::Error, "Failed to delete interpreter context: #{e.message}"
end

#list_contextsArray<DaytonaToolboxApiClient::InterpreterContext>

List all user-created interpreter contexts.

The default context is not included in this list. Only contexts created via ‘create_context` are returned.

Examples:

contexts = sandbox.code_interpreter.list_contexts
contexts.each do |ctx|
  puts "Context #{ctx.id}: #{ctx.language} at #{ctx.cwd}"
end

Returns:

  • (Array<DaytonaToolboxApiClient::InterpreterContext>)

Raises:



268
269
270
271
272
273
# File 'lib/daytona/code_interpreter.rb', line 268

def list_contexts
  response = @toolbox_api.list_interpreter_contexts
  response.contexts || []
rescue StandardError => e
  raise Sdk::Error, "Failed to list interpreter contexts: #{e.message}"
end

#run_code(code, context: nil, on_stdout: nil, on_stderr: nil, on_error: nil, envs: nil, timeout: nil) ⇒ Daytona::ExecutionResult

Execute Python code in the sandbox.

By default, code runs in the default shared context which persists variables, imports, and functions across executions. To run in an isolated context, create a new context with ‘create_context` and pass it as the `context` argument.

Examples:

def handle_stdout(msg)
  print "STDOUT: #{msg.output}"
end

def handle_stderr(msg)
  print "STDERR: #{msg.output}"
end

def handle_error(err)
  puts "ERROR: #{err.name}: #{err.value}"
end

code = <<~PYTHON
  import sys
  import time
  for i in range(5):
      print(i)
      time.sleep(1)
  sys.stderr.write("Counting done!")
PYTHON

result = sandbox.code_interpreter.run_code(
  code,
  on_stdout: method(:handle_stdout),
  on_stderr: method(:handle_stderr),
  on_error: method(:handle_error),
  timeout: 10
)

Parameters:

  • code (String)

    Code to execute

  • context (DaytonaToolboxApiClient::InterpreterContext, nil) (defaults to: nil)

    Context to run code in

  • on_stdout (Proc, nil) (defaults to: nil)

    Callback for stdout messages (receives OutputMessage)

  • on_stderr (Proc, nil) (defaults to: nil)

    Callback for stderr messages (receives OutputMessage)

  • on_error (Proc, nil) (defaults to: nil)

    Callback for execution errors (receives ExecutionError)

  • envs (Hash<String, String>, nil) (defaults to: nil)

    Environment variables for this execution

  • timeout (Integer, nil) (defaults to: nil)

    Timeout in seconds. 0 means no timeout. Default is 10 minutes.

Returns:

Raises:



80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
# File 'lib/daytona/code_interpreter.rb', line 80

def run_code(code, context: nil, on_stdout: nil, on_stderr: nil, on_error: nil, envs: nil, timeout: nil) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength, Metrics/ParameterLists
  # Get WebSocket URL via preview link
  preview_link = @get_preview_link.call(WS_PORT)
  url = URI.parse(preview_link.url)
  url.scheme = url.scheme == 'https' ? 'wss' : 'ws'
  url.path = '/process/interpreter/execute'
  ws_url = url.to_s

  result = ExecutionResult.new

  # Create request payload
  request = { code: }
  request[:contextId] = context.id if context
  request[:envs] = envs if envs
  request[:timeout] = timeout if timeout

  # Build headers with preview token
  headers = @toolbox_api.api_client.default_headers.dup.merge(
    'X-Daytona-Preview-Token' => preview_link.token,
    'Content-Type' => 'application/json',
    'Accept' => 'application/json'
  )

  # Use queue for synchronization
  completion_queue = Queue.new
  interpreter = self # Capture self for use in blocks
  last_message_time = Time.now
  message_mutex = Mutex.new

  puts "[DEBUG] Connecting to WebSocket: #{ws_url}" if ENV['DEBUG']

  # Connect to WebSocket and execute
  ws = WebSocket::Client::Simple.connect(ws_url, headers:)

  ws.on :open do
    puts '[DEBUG] WebSocket opened, sending request' if ENV['DEBUG']
    ws.send(JSON.dump(request))
  end

  ws.on :message do |msg|
    message_mutex.synchronize { last_message_time = Time.now }

    puts "[DEBUG] Received message (length=#{msg.data.length}): #{msg.data.inspect[0..200]}" if ENV['DEBUG']

    interpreter.send(:handle_message, msg.data, result, on_stdout, on_stderr, on_error, completion_queue)
  end

  ws.on :error do |e|
    puts "[DEBUG] WebSocket error: #{e.message}" if ENV['DEBUG']
    completion_queue.push({ type: :error, error: e })
  end

  ws.on :close do |e|
    if ENV['DEBUG']
      code = e&.code || 'nil'
      reason = e&.reason || 'nil'
      puts "[DEBUG] WebSocket closed: code=#{code}, reason=#{reason}"
    end
    error_info = interpreter.send(:handle_close, e)
    if error_info
      completion_queue.push({ type: :error_from_close, error: error_info })
    else
      completion_queue.push({ type: :close })
    end
  end

  # Wait for completion signal with idle timeout
  # If timeout is specified, wait longer to detect actual timeout errors
  # Otherwise use short idle timeout for normal completion
  idle_timeout = timeout ? (timeout + 2.0) : 1.0
  max_wait = (timeout || 300) + 3 # Add buffer to configured timeout
  start_time = Time.now
  completion_reason = nil

  # Wait for completion or close event
  loop do
    begin
      completion = completion_queue.pop(true) # non-blocking
      puts "[DEBUG] Got completion signal: #{completion[:type]}" if ENV['DEBUG']

      # Control message (completed/interrupted) = normal completion
      if completion[:type] == :completed
        completion_reason = :completed
        break
      # If it's an error from close event (like timeout), raise it
      elsif completion[:type] == :error_from_close
        error_msg = completion[:error]
        # Raise TimeoutError for timeout cases, regular Error for others
        if error_msg.include?('timed out') || error_msg.include?('Execution timed out')
          raise Sdk::TimeoutError, error_msg
        end

        raise Sdk::Error, error_msg

      # Close event during execution (before control message) = likely timeout or error
      elsif completion[:type] == :close
        elapsed = Time.now - start_time
        # If we got close near the timeout, it's likely a timeout
        if timeout && elapsed >= timeout && elapsed < (timeout + 2)
          raise Sdk::TimeoutError,
                'Execution timed out: operation exceeded the configured `timeout`. Provide a larger value if needed.'
        end
        # Otherwise normal close
        completion_reason = :close
        break
      # WebSocket errors
      elsif completion[:type] == :error && !completion[:error].message.include?('stream closed')
        raise Sdk::Error, "WebSocket error: #{completion[:error].message}"
      end
    rescue ThreadError
      # Queue is empty, check idle timeout
    end

    # Check idle timeout (no messages for N seconds = completion)
    time_since_last_message = message_mutex.synchronize { Time.now - last_message_time }
    if time_since_last_message > idle_timeout
      puts "[DEBUG] Idle timeout reached (#{idle_timeout}s), assuming completion" if ENV['DEBUG']
      completion_reason = :idle_complete
      break
    end

    # Check for absolute timeout (safety net)
    if Time.now - start_time > max_wait
      ws.close
      raise Sdk::TimeoutError,
            'Execution timed out: operation exceeded the configured `timeout`. Provide a larger value if needed.'
    end

    sleep 0.05 # Check every 50ms
  end

  # Close WebSocket if not already closed
  ws.close if completion_reason != :close
  sleep 0.05

  result
rescue Sdk::Error
  # Re-raise SDK errors as-is
  raise
rescue StandardError => e
  # Wrap unexpected errors
  raise Sdk::Error, "Failed to run code: #{e.message}"
end