ruby-mana 0.4.0 → 0.5.0
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 +41 -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 +51 -0
- data/lib/mana/engines/detect.rb +93 -0
- data/lib/mana/engines/javascript.rb +90 -0
- data/lib/mana/engines/llm.rb +459 -0
- data/lib/mana/engines/python.rb +230 -0
- data/lib/mana/engines/ruby_eval.rb +11 -0
- data/lib/mana/version.rb +1 -1
- data/lib/mana.rb +8 -0
- metadata +23 -2
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
begin
|
|
4
|
+
require "mini_racer"
|
|
5
|
+
rescue LoadError
|
|
6
|
+
raise LoadError, "mini_racer gem is required for JavaScript support. Add `gem 'mini_racer'` to your Gemfile."
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
require "json"
|
|
10
|
+
|
|
11
|
+
module Mana
|
|
12
|
+
module Engines
|
|
13
|
+
class JavaScript < Base
|
|
14
|
+
# Thread-local persistent V8 context (lazy-loaded, long-running)
|
|
15
|
+
def self.context
|
|
16
|
+
Thread.current[:mana_js_context] ||= create_context
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def self.create_context
|
|
20
|
+
MiniRacer::Context.new
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def self.reset!
|
|
24
|
+
Thread.current[:mana_js_context]&.dispose
|
|
25
|
+
Thread.current[:mana_js_context] = nil
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def execute(code)
|
|
29
|
+
ctx = self.class.context
|
|
30
|
+
|
|
31
|
+
# 1. Scan code for Ruby variable references
|
|
32
|
+
# Variables from Ruby binding are injected into JS context
|
|
33
|
+
inject_ruby_vars(ctx, code)
|
|
34
|
+
|
|
35
|
+
# 2. Execute the JS code
|
|
36
|
+
result = ctx.eval(code)
|
|
37
|
+
|
|
38
|
+
# 3. Extract any new/modified variables back to Ruby binding
|
|
39
|
+
extract_js_vars(ctx, code)
|
|
40
|
+
|
|
41
|
+
result
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
private
|
|
45
|
+
|
|
46
|
+
def inject_ruby_vars(ctx, code)
|
|
47
|
+
@binding.local_variables.each do |var_name|
|
|
48
|
+
# Only inject variables actually referenced in the code (word-boundary match)
|
|
49
|
+
pattern = /\b#{Regexp.escape(var_name.to_s)}\b/
|
|
50
|
+
next unless code.match?(pattern)
|
|
51
|
+
|
|
52
|
+
value = @binding.local_variable_get(var_name)
|
|
53
|
+
serialized = serialize(value)
|
|
54
|
+
ctx.eval("var #{var_name} = #{JSON.generate(serialized)}")
|
|
55
|
+
rescue => e
|
|
56
|
+
next
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def extract_js_vars(ctx, code)
|
|
61
|
+
declared_vars = extract_declared_vars(code)
|
|
62
|
+
declared_vars.each do |var_name|
|
|
63
|
+
begin
|
|
64
|
+
value = ctx.eval(var_name)
|
|
65
|
+
deserialized = deserialize(value)
|
|
66
|
+
write_var(var_name, deserialized)
|
|
67
|
+
rescue MiniRacer::RuntimeError
|
|
68
|
+
next
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def extract_declared_vars(code)
|
|
74
|
+
vars = []
|
|
75
|
+
# Match: const x = ..., let x = ..., var x = ...
|
|
76
|
+
code.scan(/\b(?:const|let|var)\s+(\w+)\s*=/).each { |m| vars << m[0] }
|
|
77
|
+
# Match: bare assignment at start of line: x = ...
|
|
78
|
+
# But NOT: x === ..., x == ..., x => ...
|
|
79
|
+
code.scan(/^(\w+)\s*=[^=>]/).each { |m| vars << m[0] }
|
|
80
|
+
vars.uniq
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def deserialize(value)
|
|
84
|
+
# JS values come back as Ruby primitives from mini_racer
|
|
85
|
+
# Arrays and Hashes are automatically converted
|
|
86
|
+
value
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
@@ -0,0 +1,459 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module Mana
|
|
6
|
+
module Engines
|
|
7
|
+
class LLM < Base
|
|
8
|
+
TOOLS = [
|
|
9
|
+
{
|
|
10
|
+
name: "read_var",
|
|
11
|
+
description: "Read a variable value from the Ruby scope.",
|
|
12
|
+
input_schema: {
|
|
13
|
+
type: "object",
|
|
14
|
+
properties: { name: { type: "string", description: "Variable name" } },
|
|
15
|
+
required: ["name"]
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
name: "write_var",
|
|
20
|
+
description: "Write a value to a variable in the Ruby scope. Creates the variable if it doesn't exist.",
|
|
21
|
+
input_schema: {
|
|
22
|
+
type: "object",
|
|
23
|
+
properties: {
|
|
24
|
+
name: { type: "string", description: "Variable name" },
|
|
25
|
+
value: { description: "Value to assign (any JSON type)" }
|
|
26
|
+
},
|
|
27
|
+
required: %w[name value]
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
name: "read_attr",
|
|
32
|
+
description: "Read an attribute from a Ruby object.",
|
|
33
|
+
input_schema: {
|
|
34
|
+
type: "object",
|
|
35
|
+
properties: {
|
|
36
|
+
obj: { type: "string", description: "Variable name holding the object" },
|
|
37
|
+
attr: { type: "string", description: "Attribute name to read" }
|
|
38
|
+
},
|
|
39
|
+
required: %w[obj attr]
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
name: "write_attr",
|
|
44
|
+
description: "Set an attribute on a Ruby object.",
|
|
45
|
+
input_schema: {
|
|
46
|
+
type: "object",
|
|
47
|
+
properties: {
|
|
48
|
+
obj: { type: "string", description: "Variable name holding the object" },
|
|
49
|
+
attr: { type: "string", description: "Attribute name to set" },
|
|
50
|
+
value: { description: "Value to assign" }
|
|
51
|
+
},
|
|
52
|
+
required: %w[obj attr value]
|
|
53
|
+
}
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
name: "call_func",
|
|
57
|
+
description: "Call a Ruby method/function available in the current scope.",
|
|
58
|
+
input_schema: {
|
|
59
|
+
type: "object",
|
|
60
|
+
properties: {
|
|
61
|
+
name: { type: "string", description: "Function/method name" },
|
|
62
|
+
args: { type: "array", description: "Arguments to pass", items: {} }
|
|
63
|
+
},
|
|
64
|
+
required: ["name"]
|
|
65
|
+
}
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
name: "done",
|
|
69
|
+
description: "Signal that the task is complete.",
|
|
70
|
+
input_schema: {
|
|
71
|
+
type: "object",
|
|
72
|
+
properties: {
|
|
73
|
+
result: { description: "Optional return value" }
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
].freeze
|
|
78
|
+
|
|
79
|
+
REMEMBER_TOOL = {
|
|
80
|
+
name: "remember",
|
|
81
|
+
description: "Store a fact in long-term memory. This memory persists across script executions. Use when the user explicitly asks to remember something.",
|
|
82
|
+
input_schema: {
|
|
83
|
+
type: "object",
|
|
84
|
+
properties: { content: { type: "string", description: "The fact to remember" } },
|
|
85
|
+
required: ["content"]
|
|
86
|
+
}
|
|
87
|
+
}.freeze
|
|
88
|
+
|
|
89
|
+
class << self
|
|
90
|
+
def handler_stack
|
|
91
|
+
Thread.current[:mana_handlers] ||= []
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def with_handler(handler = nil, **opts, &block)
|
|
95
|
+
handler_stack.push(handler)
|
|
96
|
+
block.call
|
|
97
|
+
ensure
|
|
98
|
+
handler_stack.pop
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# Built-in tools + remember + any registered custom effects
|
|
102
|
+
def all_tools
|
|
103
|
+
tools = TOOLS.dup
|
|
104
|
+
tools << REMEMBER_TOOL unless Memory.incognito?
|
|
105
|
+
tools + Mana::EffectRegistry.tool_definitions
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def initialize(caller_binding, config = Mana.config)
|
|
110
|
+
super
|
|
111
|
+
@caller_path = caller_source_path
|
|
112
|
+
@incognito = Memory.incognito?
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def execute(prompt)
|
|
116
|
+
Thread.current[:mana_depth] ||= 0
|
|
117
|
+
Thread.current[:mana_depth] += 1
|
|
118
|
+
nested = Thread.current[:mana_depth] > 1
|
|
119
|
+
|
|
120
|
+
# Nested calls get fresh short-term memory but share long-term
|
|
121
|
+
if nested && !@incognito
|
|
122
|
+
outer_memory = Thread.current[:mana_memory]
|
|
123
|
+
inner_memory = Mana::Memory.new
|
|
124
|
+
long_term = outer_memory&.long_term || []
|
|
125
|
+
inner_memory.instance_variable_set(:@long_term, long_term)
|
|
126
|
+
inner_memory.instance_variable_set(:@next_id, (long_term.map { |m| m[:id] }.max || 0) + 1)
|
|
127
|
+
Thread.current[:mana_memory] = inner_memory
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
context = build_context(prompt)
|
|
131
|
+
system_prompt = build_system_prompt(context)
|
|
132
|
+
|
|
133
|
+
# Use memory's short_term messages (auto per-thread), or fresh if incognito
|
|
134
|
+
memory = @incognito ? nil : Memory.current
|
|
135
|
+
memory&.wait_for_compaction
|
|
136
|
+
|
|
137
|
+
messages = memory ? memory.short_term : []
|
|
138
|
+
messages << { role: "user", content: prompt }
|
|
139
|
+
|
|
140
|
+
iterations = 0
|
|
141
|
+
done_result = nil
|
|
142
|
+
|
|
143
|
+
loop do
|
|
144
|
+
iterations += 1
|
|
145
|
+
raise MaxIterationsError, "exceeded #{@config.max_iterations} iterations" if iterations > @config.max_iterations
|
|
146
|
+
|
|
147
|
+
response = llm_call(system_prompt, messages)
|
|
148
|
+
tool_uses = extract_tool_uses(response)
|
|
149
|
+
|
|
150
|
+
break if tool_uses.empty?
|
|
151
|
+
|
|
152
|
+
# Append assistant message
|
|
153
|
+
messages << { role: "assistant", content: response }
|
|
154
|
+
|
|
155
|
+
# Process each tool use
|
|
156
|
+
tool_results = tool_uses.map do |tu|
|
|
157
|
+
result = handle_effect(tu, memory)
|
|
158
|
+
done_result = (tu[:input][:result] || tu[:input]["result"]) if tu[:name] == "done"
|
|
159
|
+
{ type: "tool_result", tool_use_id: tu[:id], content: result.to_s }
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
messages << { role: "user", content: tool_results }
|
|
163
|
+
|
|
164
|
+
break if tool_uses.any? { |t| t[:name] == "done" }
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
# Append a final assistant summary so LLM has full context next call
|
|
168
|
+
if memory && done_result
|
|
169
|
+
messages << { role: "assistant", content: [{ type: "text", text: "Done: #{done_result}" }] }
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
# Schedule compaction if needed (runs in background, skip for nested)
|
|
173
|
+
memory&.schedule_compaction unless nested
|
|
174
|
+
|
|
175
|
+
done_result
|
|
176
|
+
ensure
|
|
177
|
+
if nested && !@incognito
|
|
178
|
+
Thread.current[:mana_memory] = outer_memory
|
|
179
|
+
end
|
|
180
|
+
Thread.current[:mana_depth] -= 1 if Thread.current[:mana_depth]
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
# Mock handling — public so Engine dispatcher can call it
|
|
184
|
+
def handle_mock(prompt)
|
|
185
|
+
mock = Mana.current_mock
|
|
186
|
+
stub = mock.match(prompt)
|
|
187
|
+
|
|
188
|
+
unless stub
|
|
189
|
+
truncated = prompt.length > 60 ? "#{prompt[0..57]}..." : prompt
|
|
190
|
+
raise MockError, "No mock matched: \"#{truncated}\"\n Add: mock_prompt \"#{truncated}\", _return: \"...\""
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
values = if stub.block
|
|
194
|
+
stub.block.call(prompt)
|
|
195
|
+
else
|
|
196
|
+
stub.values.dup
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
return_value = values.delete(:_return)
|
|
200
|
+
|
|
201
|
+
values.each do |name, value|
|
|
202
|
+
write_local(name.to_s, value)
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
# Record in short-term memory if not incognito
|
|
206
|
+
if !@incognito
|
|
207
|
+
memory = Memory.current
|
|
208
|
+
if memory
|
|
209
|
+
memory.short_term << { role: "user", content: prompt }
|
|
210
|
+
memory.short_term << { role: "assistant", content: [{ type: "text", text: "Done: #{return_value || values.inspect}" }] }
|
|
211
|
+
end
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
return_value || values.values.first
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
private
|
|
218
|
+
|
|
219
|
+
# --- Context Building ---
|
|
220
|
+
|
|
221
|
+
def build_context(prompt)
|
|
222
|
+
var_names = prompt.scan(/<(\w+)>/).flatten.uniq
|
|
223
|
+
ctx = {}
|
|
224
|
+
var_names.each do |name|
|
|
225
|
+
val = resolve(name)
|
|
226
|
+
ctx[name] = serialize_value(val)
|
|
227
|
+
rescue NameError
|
|
228
|
+
# Variable doesn't exist yet — will be created by LLM
|
|
229
|
+
end
|
|
230
|
+
ctx
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
def build_system_prompt(context)
|
|
234
|
+
parts = [
|
|
235
|
+
"You are embedded inside a Ruby program. You interact with the program's live state using the provided tools.",
|
|
236
|
+
"",
|
|
237
|
+
"Rules:",
|
|
238
|
+
"- Use read_var / read_attr to inspect variables and objects.",
|
|
239
|
+
"- Use write_var to create or update variables in the Ruby scope.",
|
|
240
|
+
"- Use write_attr to set attributes on Ruby objects.",
|
|
241
|
+
"- Use call_func to call Ruby methods available in scope.",
|
|
242
|
+
"- Call done when the task is complete.",
|
|
243
|
+
"- When the user references <var>, that's a variable in scope.",
|
|
244
|
+
"- If a referenced variable doesn't exist yet, the user expects you to create it with write_var.",
|
|
245
|
+
"- Be precise with types: use numbers for numeric values, arrays for lists, strings for text."
|
|
246
|
+
]
|
|
247
|
+
|
|
248
|
+
# Inject long-term memories or incognito notice
|
|
249
|
+
if @incognito
|
|
250
|
+
parts << ""
|
|
251
|
+
parts << "You are in incognito mode. The remember tool is disabled. No memories will be loaded or saved."
|
|
252
|
+
else
|
|
253
|
+
memory = Memory.current
|
|
254
|
+
if memory
|
|
255
|
+
# Inject summaries from compaction
|
|
256
|
+
unless memory.summaries.empty?
|
|
257
|
+
parts << ""
|
|
258
|
+
parts << "Previous conversation summary:"
|
|
259
|
+
memory.summaries.each { |s| parts << " #{s}" }
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
unless memory.long_term.empty?
|
|
263
|
+
parts << ""
|
|
264
|
+
parts << "Long-term memories (persistent across executions):"
|
|
265
|
+
memory.long_term.each { |m| parts << "- #{m[:content]}" }
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
unless memory.long_term.empty?
|
|
269
|
+
parts << ""
|
|
270
|
+
parts << "You have a `remember` tool to store new facts in long-term memory when the user asks."
|
|
271
|
+
end
|
|
272
|
+
end
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
unless context.empty?
|
|
276
|
+
parts << ""
|
|
277
|
+
parts << "Current variable values:"
|
|
278
|
+
context.each { |k, v| parts << " #{k} = #{v}" }
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
# Discover available functions from caller's source
|
|
282
|
+
methods = begin
|
|
283
|
+
Mana::Introspect.methods_from_file(@caller_path)
|
|
284
|
+
rescue => _e
|
|
285
|
+
[]
|
|
286
|
+
end
|
|
287
|
+
unless methods.empty?
|
|
288
|
+
parts << ""
|
|
289
|
+
parts << Mana::Introspect.format_for_prompt(methods)
|
|
290
|
+
end
|
|
291
|
+
|
|
292
|
+
# List custom effects
|
|
293
|
+
custom_effects = Mana::EffectRegistry.tool_definitions
|
|
294
|
+
unless custom_effects.empty?
|
|
295
|
+
parts << ""
|
|
296
|
+
parts << "Custom tools available:"
|
|
297
|
+
custom_effects.each do |t|
|
|
298
|
+
params = (t[:input_schema][:properties] || {}).keys.join(", ")
|
|
299
|
+
parts << " #{t[:name]}(#{params}) — #{t[:description]}"
|
|
300
|
+
end
|
|
301
|
+
end
|
|
302
|
+
|
|
303
|
+
parts.join("\n")
|
|
304
|
+
end
|
|
305
|
+
|
|
306
|
+
# --- Effect Handling ---
|
|
307
|
+
|
|
308
|
+
def handle_effect(tool_use, memory = nil)
|
|
309
|
+
name = tool_use[:name]
|
|
310
|
+
input = tool_use[:input] || {}
|
|
311
|
+
# Normalize keys to strings for consistent access
|
|
312
|
+
input = input.transform_keys(&:to_s) if input.is_a?(Hash)
|
|
313
|
+
|
|
314
|
+
# Check handler stack first (legacy)
|
|
315
|
+
handler = self.class.handler_stack.last
|
|
316
|
+
return handler.call(name, input) if handler && handler.respond_to?(:call)
|
|
317
|
+
|
|
318
|
+
# Check custom effect registry
|
|
319
|
+
handled, result = Mana::EffectRegistry.handle(name, input)
|
|
320
|
+
return serialize_value(result) if handled
|
|
321
|
+
|
|
322
|
+
case name
|
|
323
|
+
when "read_var"
|
|
324
|
+
serialize_value(resolve(input["name"]))
|
|
325
|
+
|
|
326
|
+
when "write_var"
|
|
327
|
+
var_name = input["name"]
|
|
328
|
+
value = input["value"]
|
|
329
|
+
write_local(var_name, value)
|
|
330
|
+
"ok: #{var_name} = #{value.inspect}"
|
|
331
|
+
|
|
332
|
+
when "read_attr"
|
|
333
|
+
obj = resolve(input["obj"])
|
|
334
|
+
validate_name!(input["attr"])
|
|
335
|
+
serialize_value(obj.public_send(input["attr"]))
|
|
336
|
+
|
|
337
|
+
when "write_attr"
|
|
338
|
+
obj = resolve(input["obj"])
|
|
339
|
+
validate_name!(input["attr"])
|
|
340
|
+
obj.public_send("#{input['attr']}=", input["value"])
|
|
341
|
+
"ok: #{input['obj']}.#{input['attr']} = #{input['value'].inspect}"
|
|
342
|
+
|
|
343
|
+
when "call_func"
|
|
344
|
+
func = input["name"]
|
|
345
|
+
validate_name!(func)
|
|
346
|
+
args = input["args"] || []
|
|
347
|
+
# Try method first, then local variable (supports lambdas/procs)
|
|
348
|
+
callable = if @binding.receiver.respond_to?(func.to_sym, true)
|
|
349
|
+
@binding.receiver.method(func.to_sym)
|
|
350
|
+
elsif @binding.local_variables.include?(func.to_sym)
|
|
351
|
+
@binding.local_variable_get(func.to_sym)
|
|
352
|
+
else
|
|
353
|
+
@binding.receiver.method(func.to_sym) # raise NameError
|
|
354
|
+
end
|
|
355
|
+
result = callable.call(*args)
|
|
356
|
+
serialize_value(result)
|
|
357
|
+
|
|
358
|
+
when "remember"
|
|
359
|
+
if @incognito
|
|
360
|
+
"Memory not saved (incognito mode)"
|
|
361
|
+
elsif memory
|
|
362
|
+
entry = memory.remember(input["content"])
|
|
363
|
+
"Remembered (id=#{entry[:id]}): #{input['content']}"
|
|
364
|
+
else
|
|
365
|
+
"Memory not available"
|
|
366
|
+
end
|
|
367
|
+
|
|
368
|
+
when "done"
|
|
369
|
+
input["result"].to_s
|
|
370
|
+
|
|
371
|
+
else
|
|
372
|
+
"error: unknown tool #{name}"
|
|
373
|
+
end
|
|
374
|
+
rescue => e
|
|
375
|
+
"error: #{e.class}: #{e.message}"
|
|
376
|
+
end
|
|
377
|
+
|
|
378
|
+
# --- Binding Helpers ---
|
|
379
|
+
|
|
380
|
+
VALID_IDENTIFIER = /\A[A-Za-z_][A-Za-z0-9_]*\z/
|
|
381
|
+
|
|
382
|
+
def validate_name!(name)
|
|
383
|
+
raise Mana::Error, "invalid identifier: #{name.inspect}" unless name.match?(VALID_IDENTIFIER)
|
|
384
|
+
end
|
|
385
|
+
|
|
386
|
+
def resolve(name)
|
|
387
|
+
validate_name!(name)
|
|
388
|
+
if @binding.local_variable_defined?(name.to_sym)
|
|
389
|
+
@binding.local_variable_get(name.to_sym)
|
|
390
|
+
elsif @binding.receiver.respond_to?(name.to_sym, true)
|
|
391
|
+
@binding.receiver.send(name.to_sym)
|
|
392
|
+
else
|
|
393
|
+
raise NameError, "undefined variable or method '#{name}'"
|
|
394
|
+
end
|
|
395
|
+
end
|
|
396
|
+
|
|
397
|
+
def write_local(name, value)
|
|
398
|
+
validate_name!(name)
|
|
399
|
+
@binding.local_variable_set(name.to_sym, value)
|
|
400
|
+
end
|
|
401
|
+
|
|
402
|
+
def caller_source_path
|
|
403
|
+
# Walk up the call stack to find the first non-mana source file
|
|
404
|
+
loc = @binding.source_location
|
|
405
|
+
return loc[0] if loc.is_a?(Array)
|
|
406
|
+
|
|
407
|
+
# Fallback: search caller_locations
|
|
408
|
+
caller_locations(4, 20)&.each do |frame|
|
|
409
|
+
path = frame.absolute_path || frame.path
|
|
410
|
+
next if path.nil? || path.include?("mana/")
|
|
411
|
+
return path
|
|
412
|
+
end
|
|
413
|
+
nil
|
|
414
|
+
end
|
|
415
|
+
|
|
416
|
+
def serialize_value(val)
|
|
417
|
+
case val
|
|
418
|
+
when String, Integer, Float, TrueClass, FalseClass, NilClass
|
|
419
|
+
val.inspect
|
|
420
|
+
when Symbol
|
|
421
|
+
val.to_s.inspect
|
|
422
|
+
when Array
|
|
423
|
+
"[#{val.map { |v| serialize_value(v) }.join(', ')}]"
|
|
424
|
+
when Hash
|
|
425
|
+
pairs = val.map { |k, v| "#{serialize_value(k)} => #{serialize_value(v)}" }
|
|
426
|
+
"{#{pairs.join(', ')}}"
|
|
427
|
+
else
|
|
428
|
+
ivars = val.instance_variables
|
|
429
|
+
obj_repr = ivars.map do |ivar|
|
|
430
|
+
attr_name = ivar.to_s.delete_prefix("@")
|
|
431
|
+
"#{attr_name}: #{val.instance_variable_get(ivar).inspect}" rescue nil
|
|
432
|
+
end.compact.join(", ")
|
|
433
|
+
"#<#{val.class} #{obj_repr}>"
|
|
434
|
+
end
|
|
435
|
+
end
|
|
436
|
+
|
|
437
|
+
# --- LLM Client ---
|
|
438
|
+
|
|
439
|
+
def llm_call(system, messages)
|
|
440
|
+
backend = Backends.for(@config)
|
|
441
|
+
backend.chat(
|
|
442
|
+
system: system,
|
|
443
|
+
messages: messages,
|
|
444
|
+
tools: self.class.all_tools,
|
|
445
|
+
model: @config.model,
|
|
446
|
+
max_tokens: 4096
|
|
447
|
+
)
|
|
448
|
+
end
|
|
449
|
+
|
|
450
|
+
def extract_tool_uses(content)
|
|
451
|
+
return [] unless content.is_a?(Array)
|
|
452
|
+
|
|
453
|
+
content
|
|
454
|
+
.select { |block| block[:type] == "tool_use" }
|
|
455
|
+
.map { |block| { id: block[:id], name: block[:name], input: block[:input] || {} } }
|
|
456
|
+
end
|
|
457
|
+
end
|
|
458
|
+
end
|
|
459
|
+
end
|