Class: Ask::Tools::Repl

Inherits:
Ask::Tool
  • Object
show all
Defined in:
lib/ask/tools/shell/repl.rb

Overview

Note:

The kernel runs with the caller's permissions (like Code). It is a durable control environment, not a security sandbox.

Evaluate Ruby code in a persistent, long-lived subprocess.

Unlike Code (which spawns a fresh ruby -e per call), Repl keeps a kernel process alive across calls and evaluates every snippet into the same binding — so state (locals, requires, defined methods) survives between calls. This is the RLM (recursive language model) pattern: the model composes capabilities as code against a persistent environment.

Sessions are named and shared process-wide: calling with the same session name from any tool instance reaches the same kernel. A session is closed by reset: true, Repl.close_session, or after Repl.idle_timeout seconds without use.

Examples:

repl = Ask::Tools::Repl.new
repl.call(code: 'require "json"; data = JSON.parse(%q({"a": 1}))')
repl.call(code: "data['a'] + 1")       # => 2 — `data` still exists
repl.call(code: "def double(x); x * 2; end")
repl.call(code: "double(21)")          # => 42

Defined Under Namespace

Classes: Kernel

Constant Summary collapse

DEFAULT_EVAL_TIMEOUT =

Timeout for a single evaluation, in seconds.

30
DEFAULT_IDLE_TIMEOUT =

Sessions idle longer than this are closed on next access.

300
BUNDLER_ENV =

Env vars that would drag the parent's bundler context into the kernel subprocess (RUBYOPT=-rbundler/setup restricts $LOAD_PATH to the parent's Gemfile). Nil overrides remove them at spawn so the session is plain ruby, like the one-shot Code tool's sandbox.

%w[
  RUBYOPT RUBYLIB BASH_ENV GEM_PATH GEM_HOME
  BUNDLE_GEMFILE BUNDLE_PATH BUNDLE_BIN_PATH BUNDLER_SETUP
  BUNDLER_VERSION BUNDLE_WITHOUT BUNDLE_FROZEN BUNDLE_DEPLOYMENT
  BUNDLE_LOCKFILE BUNDLE_APP_CONFIG
].freeze

Class Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Class Attribute Details

.eval_timeoutInteger

Returns seconds a single evaluation may take.

Returns:

  • (Integer)

    seconds a single evaluation may take



64
65
66
# File 'lib/ask/tools/shell/repl.rb', line 64

def eval_timeout
  @eval_timeout
end

.idle_timeoutInteger

Returns seconds a session may sit unused before it is closed on next access.

Returns:

  • (Integer)

    seconds a session may sit unused before it is closed on next access



61
62
63
# File 'lib/ask/tools/shell/repl.rb', line 61

def idle_timeout
  @idle_timeout
end

Class Method Details

.close_allvoid

This method returns an undefined value.

Close every session kernel. Called at exit; call it explicitly to free subprocesses early.



111
112
113
114
115
116
117
# File 'lib/ask/tools/shell/repl.rb', line 111

def close_all
  @registry_mutex.synchronize do
    @registry.each_value(&:close)
    @registry.clear
  end
  nil
end

.close_session(session) ⇒ void

This method returns an undefined value.

Close and forget a session's kernel.

Parameters:

  • session (String)


99
100
101
102
103
104
105
# File 'lib/ask/tools/shell/repl.rb', line 99

def close_session(session)
  @registry_mutex.synchronize do
    kernel = @registry.delete(session)
    kernel&.close
  end
  nil
end

.kernel_for(session, reset: false) ⇒ Kernel

The kernel for session, spawning one if needed (or after reset).

Parameters:

  • session (String)
  • reset (Boolean) (defaults to: false)

    discard existing session state

Returns:



79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
# File 'lib/ask/tools/shell/repl.rb', line 79

def kernel_for(session, reset: false)
  @registry_mutex.synchronize do
    close_session(session) if reset
    kernel = @registry[session]
    if kernel.nil? || kernel.dead?
      kernel = Kernel.new(session: session)
      @registry[session] = kernel
    elsif kernel.idle_seconds > idle_timeout
      kernel.close
      kernel = Kernel.new(session: session)
      @registry[session] = kernel
    end
    kernel
  end
end

.sessionsArray<String>

Returns active session names.

Returns:

  • (Array<String>)

    active session names



120
121
122
# File 'lib/ask/tools/shell/repl.rb', line 120

def sessions
  @registry_mutex.synchronize { @registry.keys }
end

Instance Method Details

#execute(code:, session: "default", reset: false) ⇒ Object



127
128
129
130
131
132
133
134
135
136
137
138
139
# File 'lib/ask/tools/shell/repl.rb', line 127

def execute(code:, session: "default", reset: false)
  build_result(eval_code(session, code, reset: reset), session)
rescue Kernel::TimeoutError => e
  Ask::Result.error(message: "REPL session '#{session}' timed out (#{e.timeout}s); session state was lost")
rescue Kernel::DeadError => e
  # The session died (crash, external kill, closed stdin). Respawn a
  # fresh kernel and retry once; only give up if it dies again.
  begin
    build_result(eval_code(session, code), session)
  rescue Kernel::DeadError
    Ask::Result.error(message: "REPL session '#{session}' died repeatedly: #{e.message}")
  end
end