ruby-mana 0.4.0 → 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +48 -1
- data/README.md +68 -0
- data/data/lang-rules.yml +196 -0
- data/lib/mana/compiler.rb +1 -1
- data/lib/mana/engine.rb +38 -432
- data/lib/mana/engines/base.rb +79 -0
- data/lib/mana/engines/detect.rb +93 -0
- data/lib/mana/engines/javascript.rb +314 -0
- data/lib/mana/engines/llm.rb +467 -0
- data/lib/mana/engines/python.rb +314 -0
- data/lib/mana/engines/ruby_eval.rb +11 -0
- data/lib/mana/object_registry.rb +89 -0
- data/lib/mana/remote_ref.rb +85 -0
- data/lib/mana/version.rb +1 -1
- data/lib/mana.rb +11 -0
- metadata +25 -2
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
begin
|
|
4
|
+
require "pycall"
|
|
5
|
+
rescue LoadError
|
|
6
|
+
raise LoadError, "pycall gem is required for Python support. Add `gem 'pycall'` to your Gemfile."
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
require "json"
|
|
10
|
+
|
|
11
|
+
module Mana
|
|
12
|
+
module Engines
|
|
13
|
+
class Python < Base
|
|
14
|
+
# Python helper code injected once per namespace.
|
|
15
|
+
# Sets up weak-ref tracking so when Python drops a reference to a
|
|
16
|
+
# complex Ruby object, the ObjectRegistry is notified.
|
|
17
|
+
PY_GC_HELPER = <<~PYTHON
|
|
18
|
+
import weakref as _mana_wr
|
|
19
|
+
import builtins as _mana_bi
|
|
20
|
+
_mana_bi._mana_weakref = _mana_wr
|
|
21
|
+
|
|
22
|
+
class __ManaRef:
|
|
23
|
+
"""Weak-ref release notifier for Ruby objects passed to Python."""
|
|
24
|
+
_release_fn = None
|
|
25
|
+
_instances = {}
|
|
26
|
+
|
|
27
|
+
@classmethod
|
|
28
|
+
def set_release_fn(cls, fn):
|
|
29
|
+
cls._release_fn = fn
|
|
30
|
+
|
|
31
|
+
@classmethod
|
|
32
|
+
def track(cls, ref_id, obj):
|
|
33
|
+
"""Track a Ruby object. When Python GC collects it, notify Ruby."""
|
|
34
|
+
import builtins
|
|
35
|
+
_wr = builtins._mana_weakref
|
|
36
|
+
try:
|
|
37
|
+
ref = _wr.ref(obj, lambda r, rid=ref_id: cls._release(rid))
|
|
38
|
+
cls._instances[ref_id] = ref
|
|
39
|
+
except TypeError:
|
|
40
|
+
# Some objects can't be weakly referenced; skip them
|
|
41
|
+
pass
|
|
42
|
+
|
|
43
|
+
@classmethod
|
|
44
|
+
def _release(cls, ref_id):
|
|
45
|
+
cls._instances.pop(ref_id, None)
|
|
46
|
+
if cls._release_fn:
|
|
47
|
+
try:
|
|
48
|
+
cls._release_fn(ref_id)
|
|
49
|
+
except Exception:
|
|
50
|
+
pass
|
|
51
|
+
|
|
52
|
+
@classmethod
|
|
53
|
+
def release_all(cls):
|
|
54
|
+
for ref_id in list(cls._instances.keys()):
|
|
55
|
+
cls._release(ref_id)
|
|
56
|
+
PYTHON
|
|
57
|
+
|
|
58
|
+
# Thread-local persistent Python state
|
|
59
|
+
# PyCall shares a single Python interpreter per process,
|
|
60
|
+
# but we track our own variable namespace
|
|
61
|
+
def self.namespace
|
|
62
|
+
Thread.current[:mana_py_namespace] ||= create_namespace
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def self.create_namespace
|
|
66
|
+
PyCall.eval("dict()")
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def self.reset!
|
|
70
|
+
ns = Thread.current[:mana_py_namespace]
|
|
71
|
+
if ns
|
|
72
|
+
begin
|
|
73
|
+
mana_ref = ns["__ManaRef"]
|
|
74
|
+
mana_ref.release_all if mana_ref
|
|
75
|
+
rescue => e
|
|
76
|
+
# ignore
|
|
77
|
+
end
|
|
78
|
+
PyCall.exec("pass") # ensure interpreter is alive
|
|
79
|
+
Thread.current[:mana_py_namespace] = nil
|
|
80
|
+
end
|
|
81
|
+
Thread.current[:mana_py_gc_injected] = nil
|
|
82
|
+
ObjectRegistry.reset!
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def execute(code)
|
|
86
|
+
ns = self.class.namespace
|
|
87
|
+
|
|
88
|
+
# 0. Inject GC helper (once per namespace)
|
|
89
|
+
inject_gc_helper(ns)
|
|
90
|
+
|
|
91
|
+
# 1. Inject Ruby variables into Python namespace
|
|
92
|
+
inject_ruby_vars(ns, code)
|
|
93
|
+
|
|
94
|
+
# 2. Inject the Ruby bridge for Python->Ruby callbacks
|
|
95
|
+
inject_ruby_bridge(ns)
|
|
96
|
+
|
|
97
|
+
# 3. Execute Python code in the namespace
|
|
98
|
+
PyCall.exec(code, locals: ns)
|
|
99
|
+
|
|
100
|
+
# 4. Extract declared variables back to Ruby
|
|
101
|
+
extract_py_vars(ns, code)
|
|
102
|
+
|
|
103
|
+
# Return the last expression value if possible
|
|
104
|
+
begin
|
|
105
|
+
ns["result"]
|
|
106
|
+
rescue
|
|
107
|
+
nil
|
|
108
|
+
end
|
|
109
|
+
rescue PyCall::PyError => e
|
|
110
|
+
raise Mana::Error, "Python execution error: #{e.message}"
|
|
111
|
+
rescue ArgumentError => e
|
|
112
|
+
raise Mana::Error, "Python execution error: #{e.message}"
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
private
|
|
116
|
+
|
|
117
|
+
def inject_gc_helper(ns)
|
|
118
|
+
return if Thread.current[:mana_py_gc_injected]
|
|
119
|
+
|
|
120
|
+
PyCall.exec(PY_GC_HELPER, locals: ns)
|
|
121
|
+
|
|
122
|
+
# Wire up the release callback: when Python GC collects a tracked object,
|
|
123
|
+
# __ManaRef calls this proc to release it from the Ruby ObjectRegistry.
|
|
124
|
+
registry = ObjectRegistry.current
|
|
125
|
+
release_fn = proc { |ref_id| registry.release(ref_id.to_i) }
|
|
126
|
+
mana_ref = ns["__ManaRef"]
|
|
127
|
+
mana_ref.set_release_fn(release_fn)
|
|
128
|
+
|
|
129
|
+
Thread.current[:mana_py_gc_injected] = true
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def inject_ruby_vars(ns, code)
|
|
133
|
+
@binding.local_variables.each do |var_name|
|
|
134
|
+
value = @binding.local_variable_get(var_name)
|
|
135
|
+
serialized = serialize_for_py(value)
|
|
136
|
+
begin
|
|
137
|
+
ns[var_name.to_s] = serialized
|
|
138
|
+
rescue => e
|
|
139
|
+
next
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
# Inject a `ruby` bridge into the Python namespace.
|
|
145
|
+
#
|
|
146
|
+
# The bridge is a Ruby object with method_missing that proxies calls
|
|
147
|
+
# back to the Ruby binding. PyCall automatically wraps it so Python
|
|
148
|
+
# can call methods directly:
|
|
149
|
+
#
|
|
150
|
+
# ruby.method_name(arg1, arg2) -- call a Ruby method on the receiver
|
|
151
|
+
# ruby.read("var") -- read a Ruby local variable
|
|
152
|
+
# ruby.write("var", value) -- write a Ruby local variable
|
|
153
|
+
# ruby.call_proc("name", args) -- call a local proc/lambda by name
|
|
154
|
+
#
|
|
155
|
+
# Ruby objects (including the binding receiver) can also be injected
|
|
156
|
+
# directly and called from Python -- PyCall handles the wrapping.
|
|
157
|
+
def inject_ruby_bridge(ns)
|
|
158
|
+
ns["ruby"] = RubyBridge.new(@binding)
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def extract_py_vars(ns, code)
|
|
162
|
+
declared_vars = extract_declared_vars(code)
|
|
163
|
+
declared_vars.each do |var_name|
|
|
164
|
+
next if var_name == "ruby" # skip the bridge object
|
|
165
|
+
begin
|
|
166
|
+
value = ns[var_name]
|
|
167
|
+
deserialized = deserialize_py(value)
|
|
168
|
+
write_var(var_name, deserialized)
|
|
169
|
+
rescue => e
|
|
170
|
+
next
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
def extract_declared_vars(code)
|
|
176
|
+
vars = []
|
|
177
|
+
# Match Python assignments: x = ..., but not x == ... or x += ...
|
|
178
|
+
code.scan(/^(\w+)\s*=[^=]/).each { |m| vars << m[0] }
|
|
179
|
+
# Also match augmented assignments: x += ..., x -= ...
|
|
180
|
+
code.scan(/^(\w+)\s*[+\-*\/]?=/).each { |m| vars << m[0] }
|
|
181
|
+
vars.uniq
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
# Serialize Ruby values for Python injection.
|
|
185
|
+
# Simple types are copied. Complex objects are passed directly via PyCall
|
|
186
|
+
# (which wraps them so Python can call their methods) AND registered in
|
|
187
|
+
# the ObjectRegistry for lifecycle tracking + GC notification.
|
|
188
|
+
def serialize_for_py(value)
|
|
189
|
+
case value
|
|
190
|
+
when Numeric, String, TrueClass, FalseClass, NilClass
|
|
191
|
+
value
|
|
192
|
+
when Symbol
|
|
193
|
+
value.to_s
|
|
194
|
+
when Array
|
|
195
|
+
value.map { |v| serialize_for_py(v) }
|
|
196
|
+
when Hash
|
|
197
|
+
value.each_with_object({}) do |(k, v), h|
|
|
198
|
+
key = k.is_a?(Symbol) ? k.to_s : k
|
|
199
|
+
h[key] = serialize_for_py(v)
|
|
200
|
+
end
|
|
201
|
+
when Proc, Method
|
|
202
|
+
ref_id = ObjectRegistry.current.register(value)
|
|
203
|
+
track_in_python(ref_id, value)
|
|
204
|
+
value
|
|
205
|
+
else
|
|
206
|
+
ref_id = ObjectRegistry.current.register(value)
|
|
207
|
+
track_in_python(ref_id, value)
|
|
208
|
+
value
|
|
209
|
+
end
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
# Tell the Python __ManaRef tracker to watch this object via weakref.
|
|
213
|
+
def track_in_python(ref_id, value)
|
|
214
|
+
ns = self.class.namespace
|
|
215
|
+
begin
|
|
216
|
+
mana_ref = ns["__ManaRef"]
|
|
217
|
+
mana_ref.track(ref_id, value) if mana_ref
|
|
218
|
+
rescue => e
|
|
219
|
+
# Non-fatal: some objects can't be weakly referenced in Python
|
|
220
|
+
end
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
def deserialize_py(value)
|
|
224
|
+
if defined?(PyCall::PyObjectWrapper) && value.is_a?(PyCall::PyObjectWrapper)
|
|
225
|
+
begin
|
|
226
|
+
value.to_a rescue value.to_s
|
|
227
|
+
rescue
|
|
228
|
+
value.to_s
|
|
229
|
+
end
|
|
230
|
+
else
|
|
231
|
+
value
|
|
232
|
+
end
|
|
233
|
+
end
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
# Bridge object injected as `ruby` in the Python namespace.
|
|
237
|
+
# Enables Python->Ruby callbacks via method_missing.
|
|
238
|
+
#
|
|
239
|
+
# Python usage:
|
|
240
|
+
# ruby.some_method(arg1, arg2) -- calls method on binding receiver
|
|
241
|
+
# ruby.read("var_name") -- reads a Ruby local variable
|
|
242
|
+
# ruby.write("var_name", val) -- writes a Ruby local variable
|
|
243
|
+
# ruby.call_proc("name", args) -- calls a local proc/lambda by name
|
|
244
|
+
class RubyBridge
|
|
245
|
+
def initialize(caller_binding)
|
|
246
|
+
@binding = caller_binding
|
|
247
|
+
@receiver = caller_binding.receiver
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
# Read a Ruby local variable
|
|
251
|
+
def read(name)
|
|
252
|
+
name = name.to_s.to_sym
|
|
253
|
+
if @binding.local_variables.include?(name)
|
|
254
|
+
@binding.local_variable_get(name)
|
|
255
|
+
else
|
|
256
|
+
raise NameError, "undefined Ruby variable '#{name}'"
|
|
257
|
+
end
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
# Write a Ruby local variable
|
|
261
|
+
def write(name, value)
|
|
262
|
+
@binding.local_variable_set(name.to_s.to_sym, value)
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
# Explicitly call a local proc/lambda by name
|
|
266
|
+
def call_proc(name, *args)
|
|
267
|
+
name = name.to_s.to_sym
|
|
268
|
+
if @binding.local_variables.include?(name)
|
|
269
|
+
val = @binding.local_variable_get(name)
|
|
270
|
+
if val.respond_to?(:call)
|
|
271
|
+
return val.call(*args)
|
|
272
|
+
end
|
|
273
|
+
end
|
|
274
|
+
raise NoMethodError, "no callable '#{name}' in Ruby scope"
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
# Proxy unknown method calls to the binding receiver.
|
|
278
|
+
# This lets Python do: ruby.some_method(args)
|
|
279
|
+
def method_missing(name, *args)
|
|
280
|
+
name_s = name.to_s
|
|
281
|
+
|
|
282
|
+
# First check local procs/lambdas
|
|
283
|
+
name_sym = name_s.to_sym
|
|
284
|
+
if @binding.local_variables.include?(name_sym)
|
|
285
|
+
val = @binding.local_variable_get(name_sym)
|
|
286
|
+
return val.call(*args) if val.respond_to?(:call)
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
# Then try the receiver
|
|
290
|
+
if @receiver.respond_to?(name_s)
|
|
291
|
+
return @receiver.public_send(name_s, *args)
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
super
|
|
295
|
+
end
|
|
296
|
+
|
|
297
|
+
def respond_to_missing?(name, include_private = false)
|
|
298
|
+
name_s = name.to_s
|
|
299
|
+
name_sym = name_s.to_sym
|
|
300
|
+
|
|
301
|
+
# Check local callables
|
|
302
|
+
if @binding.local_variables.include?(name_sym)
|
|
303
|
+
val = @binding.local_variable_get(name_sym)
|
|
304
|
+
return true if val.respond_to?(:call)
|
|
305
|
+
end
|
|
306
|
+
|
|
307
|
+
# Check receiver (public methods only)
|
|
308
|
+
return true if @receiver.respond_to?(name_s)
|
|
309
|
+
|
|
310
|
+
super
|
|
311
|
+
end
|
|
312
|
+
end
|
|
313
|
+
end
|
|
314
|
+
end
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Mana
|
|
4
|
+
# Thread-local registry for cross-engine object references.
|
|
5
|
+
#
|
|
6
|
+
# When a complex Ruby object is passed to JS/Python, it gets registered here
|
|
7
|
+
# with a unique integer ID. The foreign engine holds a proxy with that ID and
|
|
8
|
+
# routes method calls back through the bidirectional channel.
|
|
9
|
+
#
|
|
10
|
+
# Thread-local so each engine context (which is also thread-local) has its own
|
|
11
|
+
# isolated set of references.
|
|
12
|
+
class ObjectRegistry
|
|
13
|
+
attr_reader :objects
|
|
14
|
+
|
|
15
|
+
def initialize
|
|
16
|
+
@objects = {}
|
|
17
|
+
@next_id = 1
|
|
18
|
+
@release_callbacks = []
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Store an object and return its reference ID.
|
|
22
|
+
# If the same object is already registered, return the existing ID.
|
|
23
|
+
def register(obj)
|
|
24
|
+
# Check if already registered (by object_id for identity, not equality)
|
|
25
|
+
@objects.each do |id, entry|
|
|
26
|
+
return id if entry[:object].equal?(obj)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
id = @next_id
|
|
30
|
+
@next_id += 1
|
|
31
|
+
@objects[id] = { object: obj, type: obj.class.name }
|
|
32
|
+
id
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Retrieve an object by its reference ID.
|
|
36
|
+
def get(id)
|
|
37
|
+
entry = @objects[id]
|
|
38
|
+
entry ? entry[:object] : nil
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Register a callback invoked when any reference is released.
|
|
42
|
+
# The callback receives (id, entry) where entry is { object:, type: }.
|
|
43
|
+
def on_release(&block)
|
|
44
|
+
@release_callbacks << block
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Release a reference. Returns true if it existed.
|
|
48
|
+
# Fires registered on_release callbacks with the id and entry.
|
|
49
|
+
def release(id)
|
|
50
|
+
entry = @objects.delete(id)
|
|
51
|
+
return false unless entry
|
|
52
|
+
|
|
53
|
+
@release_callbacks.each do |cb|
|
|
54
|
+
cb.call(id, entry)
|
|
55
|
+
rescue => e
|
|
56
|
+
# Don't let callback errors break the release
|
|
57
|
+
end
|
|
58
|
+
true
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Number of live references.
|
|
62
|
+
def size
|
|
63
|
+
@objects.size
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Remove all references. Fires on_release for each.
|
|
67
|
+
def clear!
|
|
68
|
+
ids = @objects.keys.dup
|
|
69
|
+
ids.each { |id| release(id) }
|
|
70
|
+
@next_id = 1
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Check if an ID is registered.
|
|
74
|
+
def registered?(id)
|
|
75
|
+
@objects.key?(id)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Thread-local singleton access
|
|
79
|
+
def self.current
|
|
80
|
+
Thread.current[:mana_object_registry] ||= new
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def self.reset!
|
|
84
|
+
registry = Thread.current[:mana_object_registry]
|
|
85
|
+
registry&.clear!
|
|
86
|
+
Thread.current[:mana_object_registry] = nil
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Mana
|
|
4
|
+
# A proxy object representing a remote reference to an object in another engine.
|
|
5
|
+
#
|
|
6
|
+
# When JS or Python receives a complex Ruby object, they get a RemoteRef handle.
|
|
7
|
+
# Method calls on the proxy are routed back to the original object via the
|
|
8
|
+
# ObjectRegistry and the bidirectional calling channel.
|
|
9
|
+
#
|
|
10
|
+
# On the Ruby side, RemoteRef can also wrap foreign objects (JS/Python) that
|
|
11
|
+
# were passed to Ruby — method calls go through the engine's eval mechanism.
|
|
12
|
+
#
|
|
13
|
+
# GC behavior:
|
|
14
|
+
# - When a RemoteRef is garbage collected, its finalizer releases the entry
|
|
15
|
+
# from the ObjectRegistry.
|
|
16
|
+
# - If an `on_release` callback is registered on the registry, it fires,
|
|
17
|
+
# allowing the source engine to be notified (e.g., to free the foreign object).
|
|
18
|
+
class RemoteRef
|
|
19
|
+
attr_reader :ref_id, :source_engine, :type_name
|
|
20
|
+
|
|
21
|
+
def initialize(ref_id, source_engine:, type_name: nil, registry: nil)
|
|
22
|
+
@ref_id = ref_id
|
|
23
|
+
@source_engine = source_engine
|
|
24
|
+
@type_name = type_name
|
|
25
|
+
@registry = registry || ObjectRegistry.current
|
|
26
|
+
setup_release_callback
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Call a method on the remote object
|
|
30
|
+
def method_missing(name, *args, &block)
|
|
31
|
+
name_s = name.to_s
|
|
32
|
+
|
|
33
|
+
# Don't proxy Ruby internal methods
|
|
34
|
+
return super if %w[__id__ __send__ class is_a? kind_of? instance_of? respond_to? respond_to_missing? equal? nil? frozen? inspect to_s hash].include?(name_s)
|
|
35
|
+
|
|
36
|
+
obj = @registry.get(@ref_id)
|
|
37
|
+
raise Mana::Error, "Remote reference #{@ref_id} has been released" unless obj
|
|
38
|
+
|
|
39
|
+
obj.public_send(name, *args, &block)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def respond_to_missing?(name, include_private = false)
|
|
43
|
+
obj = @registry.get(@ref_id)
|
|
44
|
+
return false unless obj
|
|
45
|
+
obj.respond_to?(name, false)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Explicitly release this reference
|
|
49
|
+
def release!
|
|
50
|
+
@registry.release(@ref_id)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Check if the referenced object is still alive
|
|
54
|
+
def alive?
|
|
55
|
+
@registry.registered?(@ref_id)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def inspect
|
|
59
|
+
"#<Mana::RemoteRef id=#{@ref_id} engine=#{@source_engine} type=#{@type_name}>"
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def to_s
|
|
63
|
+
obj = @registry.get(@ref_id)
|
|
64
|
+
obj ? obj.to_s : inspect
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
private
|
|
68
|
+
|
|
69
|
+
# Set up a release callback via Ruby finalizer.
|
|
70
|
+
# When this RemoteRef is garbage collected, the registry entry is released,
|
|
71
|
+
# which in turn fires any on_release callbacks (notifying the source engine).
|
|
72
|
+
# Uses a class method to avoid capturing `self` in the closure.
|
|
73
|
+
def setup_release_callback
|
|
74
|
+
release_proc = self.class.send(:release_callback, @ref_id, @registry)
|
|
75
|
+
ObjectSpace.define_finalizer(self, release_proc)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Build a release proc that doesn't reference the RemoteRef instance.
|
|
79
|
+
# This avoids the "finalizer references object to be finalized" warning.
|
|
80
|
+
def self.release_callback(ref_id, registry)
|
|
81
|
+
proc { |_| registry.release(ref_id) }
|
|
82
|
+
end
|
|
83
|
+
private_class_method :release_callback
|
|
84
|
+
end
|
|
85
|
+
end
|
data/lib/mana/version.rb
CHANGED
data/lib/mana.rb
CHANGED
|
@@ -11,6 +11,12 @@ require_relative "mana/namespace"
|
|
|
11
11
|
require_relative "mana/memory_store"
|
|
12
12
|
require_relative "mana/context_window"
|
|
13
13
|
require_relative "mana/memory"
|
|
14
|
+
require_relative "mana/object_registry"
|
|
15
|
+
require_relative "mana/remote_ref"
|
|
16
|
+
require_relative "mana/engines/base"
|
|
17
|
+
require_relative "mana/engines/llm"
|
|
18
|
+
require_relative "mana/engines/ruby_eval"
|
|
19
|
+
require_relative "mana/engines/detect"
|
|
14
20
|
require_relative "mana/engine"
|
|
15
21
|
require_relative "mana/introspect"
|
|
16
22
|
require_relative "mana/compiler"
|
|
@@ -44,8 +50,13 @@ module Mana
|
|
|
44
50
|
def reset!
|
|
45
51
|
@config = Config.new
|
|
46
52
|
EffectRegistry.clear!
|
|
53
|
+
Engines.reset_detector!
|
|
54
|
+
ObjectRegistry.reset!
|
|
55
|
+
Engines::JavaScript.reset! if defined?(Engines::JavaScript)
|
|
56
|
+
Engines::Python.reset! if defined?(Engines::Python)
|
|
47
57
|
Thread.current[:mana_memory] = nil
|
|
48
58
|
Thread.current[:mana_mock] = nil
|
|
59
|
+
Thread.current[:mana_last_engine] = nil
|
|
49
60
|
end
|
|
50
61
|
|
|
51
62
|
# Define a custom effect that becomes an LLM tool
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: ruby-mana
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.5.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Carl
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-02-
|
|
11
|
+
date: 2026-02-24 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: binding_of_caller
|
|
@@ -24,6 +24,20 @@ dependencies:
|
|
|
24
24
|
- - "~>"
|
|
25
25
|
- !ruby/object:Gem::Version
|
|
26
26
|
version: '1.0'
|
|
27
|
+
- !ruby/object:Gem::Dependency
|
|
28
|
+
name: mini_racer
|
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
|
30
|
+
requirements:
|
|
31
|
+
- - "~>"
|
|
32
|
+
- !ruby/object:Gem::Version
|
|
33
|
+
version: '0.16'
|
|
34
|
+
type: :runtime
|
|
35
|
+
prerelease: false
|
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
37
|
+
requirements:
|
|
38
|
+
- - "~>"
|
|
39
|
+
- !ruby/object:Gem::Version
|
|
40
|
+
version: '0.16'
|
|
27
41
|
description: |
|
|
28
42
|
Mana lets you write natural language strings in Ruby that execute via LLM
|
|
29
43
|
with full access to your program's live state. Read/write variables, call
|
|
@@ -36,6 +50,7 @@ files:
|
|
|
36
50
|
- CHANGELOG.md
|
|
37
51
|
- LICENSE
|
|
38
52
|
- README.md
|
|
53
|
+
- data/lang-rules.yml
|
|
39
54
|
- lib/mana.rb
|
|
40
55
|
- lib/mana/backends/anthropic.rb
|
|
41
56
|
- lib/mana/backends/base.rb
|
|
@@ -46,12 +61,20 @@ files:
|
|
|
46
61
|
- lib/mana/context_window.rb
|
|
47
62
|
- lib/mana/effect_registry.rb
|
|
48
63
|
- lib/mana/engine.rb
|
|
64
|
+
- lib/mana/engines/base.rb
|
|
65
|
+
- lib/mana/engines/detect.rb
|
|
66
|
+
- lib/mana/engines/javascript.rb
|
|
67
|
+
- lib/mana/engines/llm.rb
|
|
68
|
+
- lib/mana/engines/python.rb
|
|
69
|
+
- lib/mana/engines/ruby_eval.rb
|
|
49
70
|
- lib/mana/introspect.rb
|
|
50
71
|
- lib/mana/memory.rb
|
|
51
72
|
- lib/mana/memory_store.rb
|
|
52
73
|
- lib/mana/mixin.rb
|
|
53
74
|
- lib/mana/mock.rb
|
|
54
75
|
- lib/mana/namespace.rb
|
|
76
|
+
- lib/mana/object_registry.rb
|
|
77
|
+
- lib/mana/remote_ref.rb
|
|
55
78
|
- lib/mana/string_ext.rb
|
|
56
79
|
- lib/mana/test.rb
|
|
57
80
|
- lib/mana/version.rb
|