ruby-mana 0.5.8 → 0.5.10

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.
@@ -0,0 +1,203 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "shellwords"
4
+
5
+ module Mana
6
+ # Runtime knowledge base — assembles information about ruby-mana from live code.
7
+ # No static files to maintain; the code IS the source of truth.
8
+ module Knowledge
9
+ class << self
10
+ # Query a topic using a cascading lookup strategy.
11
+ # Priority: mana docs > ruby env > ri (external docs) > runtime introspection > dump all.
12
+ # The fallback chain ensures we always return something useful.
13
+ def query(topic)
14
+ topic_key = topic.to_s.strip.downcase
15
+ sections = all_sections
16
+
17
+ # 1. Match mana's own sections (bidirectional substring match for flexibility)
18
+ match = sections.find { |k, _| topic_key.include?(k) || k.include?(topic_key) }
19
+ return "[source: mana]\n#{match.last}" if match
20
+
21
+ # 2. Ruby environment info
22
+ return "[source: ruby runtime]\n#{ruby_environment}" if topic_key == "ruby"
23
+
24
+ # 3. Try ri (official Ruby documentation)
25
+ ri_result = query_ri(topic.to_s.strip)
26
+ return "[source: ri (Ruby official docs)]\n#{ri_result}" if ri_result
27
+
28
+ # 4. Try runtime introspection
29
+ introspect_result = query_introspect(topic.to_s.strip)
30
+ return "[source: ruby introspection]\n#{introspect_result}" if introspect_result
31
+
32
+ # 5. Fallback: dump all mana sections so the LLM has full context
33
+ "[source: mana]\n#{sections.values.join("\n\n")}"
34
+ end
35
+
36
+ private
37
+
38
+ # Query Ruby's ri documentation tool (runs outside bundler to access rdoc).
39
+ # Must escape bundler env because ri needs access to system-wide rdoc files
40
+ # that bundler's isolated gem path would hide.
41
+ def query_ri(topic)
42
+ output = if defined?(Bundler)
43
+ Bundler.with_unbundled_env { `ri --format=markdown #{topic.shellescape} 2>&1` }
44
+ else
45
+ `ri --format=markdown #{topic.shellescape} 2>&1`
46
+ end
47
+ return nil unless $?.success?
48
+ # Truncate long docs to avoid flooding the LLM's context window
49
+ output.length > 3000 ? "#{output[0, 3000]}\n\n... (truncated, #{output.length} chars total)" : output
50
+ rescue
51
+ nil
52
+ end
53
+
54
+ # Runtime introspection: resolve as a Ruby constant and inspect it.
55
+ # This gives the LLM live information about classes/modules that may not
56
+ # appear in ri docs (e.g. user-defined or gem classes loaded at runtime).
57
+ def query_introspect(topic)
58
+ const = Object.const_get(topic)
59
+ lines = []
60
+ if const.is_a?(Module)
61
+ lines << "#{const.name} (#{const.is_a?(Class) ? "class" : "module"})"
62
+ # Limit ancestors/methods to avoid overwhelming the LLM context
63
+ lines << "Ancestors: #{const.ancestors.first(8).map(&:name).compact.join(' < ')}" if const.is_a?(Class)
64
+ pub = const.public_instance_methods(false).sort
65
+ lines << "Instance methods (#{pub.size}): #{pub.first(30).join(', ')}#{"..." if pub.size > 30}"
66
+ if const.is_a?(Class)
67
+ class_methods = (const.methods - Object.methods).sort
68
+ lines << "Class methods (#{class_methods.size}): #{class_methods.first(20).join(', ')}#{"..." if class_methods.size > 20}" unless class_methods.empty?
69
+ end
70
+ end
71
+ lines.empty? ? nil : lines.join("\n")
72
+ rescue NameError
73
+ nil
74
+ end
75
+
76
+ # Ruby runtime environment info
77
+ def ruby_environment
78
+ <<~TEXT
79
+ Ruby #{RUBY_VERSION} (#{RUBY_PLATFORM})
80
+ RUBY_ENGINE: #{RUBY_ENGINE} #{RUBY_ENGINE_VERSION}
81
+ Loaded gems: #{Gem.loaded_specs.keys.sort.join(", ")}
82
+ TEXT
83
+ end
84
+
85
+ # All knowledge sections are generated dynamically from live code/config,
86
+ # so they're always up-to-date without maintaining separate doc files.
87
+ def all_sections
88
+ {
89
+ "overview" => overview,
90
+ "tools" => tools,
91
+ "memory" => memory,
92
+ "execution" => execution,
93
+ "configuration" => configuration,
94
+ "backends" => backends,
95
+ "functions" => functions
96
+ }
97
+ end
98
+
99
+ def overview
100
+ <<~TEXT
101
+ ruby-mana v#{Mana::VERSION} is a hybrid execution engine for Ruby.
102
+ The LLM handles reasoning and decision-making; Ruby handles actual code execution.
103
+ The operator ~"..." turns a natural-language string into an LLM prompt that can
104
+ read/write live Ruby variables, call Ruby methods, and return values — all within
105
+ the caller's binding.
106
+ TEXT
107
+ end
108
+
109
+ def tools
110
+ # Extract tool info directly from Engine's tool definitions
111
+ tool_list = Engine.all_tools.map { |t|
112
+ desc = t[:description]
113
+ props = t[:input_schema][:properties] || {}
114
+ params = props.map { |k, v| "#{k}: #{v[:description] || v['description'] || k}" }.join(", ")
115
+ "- #{t[:name]}(#{params}): #{desc}"
116
+ }
117
+
118
+ "Built-in tools:\n#{tool_list.join("\n")}"
119
+ end
120
+
121
+ def memory
122
+ store_class = Mana.config.memory_store&.class&.name || "Mana::FileStore (default)"
123
+ path = if Mana.config.memory_path
124
+ Mana.config.memory_path
125
+ else
126
+ "~/.mana/memory/<namespace>.json"
127
+ end
128
+
129
+ <<~TEXT
130
+ ruby-mana has two types of memory:
131
+ - Short-term memory: conversation history within the current process. Each ~"..."
132
+ call appends to it, so consecutive calls share context. Cleared when the process exits.
133
+ - Long-term memory: persistent facts stored on disk as JSON files.
134
+ Default path: #{path}
135
+ Current store: #{store_class}
136
+ Namespace is auto-detected from the git repo name, Gemfile directory, or cwd.
137
+ Configurable via: Mana.configure { |c| c.memory_path = "/custom/path" }
138
+ Or provide a custom MemoryStore subclass for Redis, DB, etc.
139
+ - Background compaction: when short-term memory exceeds the token pressure threshold
140
+ (currently #{Mana.config.memory_pressure}), old messages are summarized in a background thread.
141
+ - Incognito mode: Mana.incognito { ~"..." } disables all memory.
142
+ The LLM can store facts via the `remember` tool. These persist across script executions.
143
+ TEXT
144
+ end
145
+
146
+ def execution
147
+ <<~TEXT
148
+ How ~"..." works step by step:
149
+ 1. ~"..." triggers String#~@ — captures the caller's Binding via binding_of_caller.
150
+ 2. Build context — parses <var> references, reads their values, discovers functions via Prism AST.
151
+ 3. Build system prompt — assembles rules, memory, variable values, and function signatures.
152
+ 4. LLM tool-calling loop — sends prompt to LLM with built-in tools. LLM responds with
153
+ tool calls, Mana executes them against the live Ruby binding, sends results back.
154
+ Loops until done() is called or max_iterations (#{Mana.config.max_iterations}) is reached.
155
+ 5. Return value — single write_var returns the value directly; multiple writes return a Hash.
156
+ TEXT
157
+ end
158
+
159
+ def configuration
160
+ c = Mana.config
161
+ <<~TEXT
162
+ Current configuration:
163
+ - model: #{c.model}
164
+ - backend: #{c.backend || 'auto-detect'}
165
+ - base_url: #{c.effective_base_url}
166
+ - timeout: #{c.timeout}s
167
+ - max_iterations: #{c.max_iterations}
168
+ - context_window: #{c.context_window}
169
+ - memory_pressure: #{c.memory_pressure}
170
+ - memory_keep_recent: #{c.memory_keep_recent}
171
+ - verbose: #{c.verbose}
172
+ All options can be set via Mana.configure { |c| ... } or environment variables
173
+ (MANA_MODEL, MANA_BACKEND, MANA_TIMEOUT, MANA_VERBOSE, ANTHROPIC_API_KEY, OPENAI_API_KEY).
174
+ TEXT
175
+ end
176
+
177
+ def backends
178
+ <<~TEXT
179
+ ruby-mana supports multiple LLM backends:
180
+ - Anthropic (Claude) — default, native format
181
+ - OpenAI (GPT) — auto-translated
182
+ - Any OpenAI-compatible API (Gemini, local models, etc.) via custom base_url
183
+ Currently using: #{Mana.config.backend || 'auto-detect'} with model #{Mana.config.model}
184
+ Configure via:
185
+ Mana.configure { |c| c.backend = :openai; c.model = "gpt-4o" }
186
+ Or set environment variables: MANA_BACKEND, MANA_MODEL
187
+ TEXT
188
+ end
189
+
190
+ def functions
191
+ <<~TEXT
192
+ Function discovery in ruby-mana:
193
+ - Prism AST parser auto-discovers methods from the caller's source file.
194
+ - YARD-style comments are extracted as descriptions.
195
+ - Methods on the receiver (minus Ruby builtins) are also discovered.
196
+ - No registration or JSON schema needed — just define normal Ruby methods.
197
+ - LLM-compiled methods: `mana def method_name` lets the LLM generate the implementation
198
+ on first call, then caches it on disk (.mana_cache/).
199
+ TEXT
200
+ end
201
+ end
202
+ end
203
+ end
data/lib/mana/logger.rb CHANGED
@@ -66,6 +66,16 @@ module Mana
66
66
  end
67
67
  end
68
68
 
69
+ # Log think tool content — full text in distinct italic cyan
70
+ def vlog_think(content)
71
+ return unless @config.verbose
72
+
73
+ $stderr.puts "\e[2m[mana]\e[0m \e[3;36m💭 Think:\e[0m"
74
+ content.each_line do |line|
75
+ $stderr.puts "\e[2m[mana]\e[0m \e[3;36m #{line.rstrip}\e[0m"
76
+ end
77
+ end
78
+
69
79
  # Summarize tool input for compact logging.
70
80
  # Multi-line string values are replaced with a brief summary.
71
81
  def summarize_input(input)
@@ -0,0 +1,157 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mana
4
+ # Assembles system prompts and extracts variable context from user prompts.
5
+ # Mixed into Engine as private methods.
6
+ module PromptBuilder
7
+ private
8
+
9
+ # Extract <var> references from the prompt and read their current values.
10
+ # Variables that don't exist yet are silently skipped (LLM will create them).
11
+ def build_context(prompt)
12
+ var_names = prompt.scan(/<(\w+)>/).flatten.uniq
13
+ ctx = {}
14
+ var_names.each do |name|
15
+ val = resolve(name)
16
+ ctx[name] = serialize_value(val)
17
+ rescue NameError
18
+ # Variable doesn't exist yet — will be created by LLM
19
+ end
20
+ ctx
21
+ end
22
+
23
+ # Assemble the system prompt with rules, memory, variables, available functions, and custom effects
24
+ def build_system_prompt(context)
25
+ parts = [
26
+ "You are Mana, an AI assistant embedded in a Ruby program. Your name is Mana — never use any other name for yourself.",
27
+ "ALWAYS respond in the same language as the user's message. If the user writes in Chinese, respond in Chinese. If in English, respond in English.",
28
+ "You interact with live Ruby state using the provided tools. When unsure about your capabilities, use the knowledge tool to check.",
29
+ "",
30
+ "Rules:",
31
+ "- read_var / read_attr to read, write_var / write_attr to write.",
32
+ "- call_func to call ANY Ruby method — including Net::HTTP, File, system libraries, gems, etc. You have Ruby's full power. Use local_variables to discover variables in scope.",
33
+ "- NEVER refuse a task by saying you can't do it. Always try using call_func first. If it fails, the error will tell you why.",
34
+ "- done(result: ...) to return a value. error(message: ...) only after you have tried and failed.",
35
+ "- <var> references point to variables in scope; create with write_var if missing.",
36
+ "- Match types precisely: numbers for numeric values, arrays for lists, strings for text.",
37
+ "- Current prompt overrides conversation history and memories.",
38
+ ]
39
+
40
+ if @incognito
41
+ parts << ""
42
+ parts << "You are in incognito mode. The remember tool is disabled. No memories will be loaded or saved."
43
+ else
44
+ memory = Memory.current
45
+ # Inject memory context when available
46
+ if memory
47
+ # Add compaction summaries from prior conversations
48
+ unless memory.summaries.empty?
49
+ parts << ""
50
+ parts << "Previous conversation summary:"
51
+ memory.summaries.each { |s| parts << " #{s}" }
52
+ end
53
+
54
+ # Add persistent long-term facts
55
+ unless memory.long_term.empty?
56
+ parts << ""
57
+ parts << "Long-term memories (persistent background context):"
58
+ memory.long_term.each { |m| parts << "- #{m[:content]}" }
59
+ end
60
+
61
+ unless memory.long_term.empty?
62
+ parts << ""
63
+ parts << "You have a `remember` tool to store new facts in long-term memory when the user asks."
64
+ end
65
+ end
66
+ end
67
+
68
+ # Inject current variable values referenced in the prompt
69
+ unless context.empty?
70
+ parts << ""
71
+ parts << "Current variable values:"
72
+ context.each { |k, v| parts << " #{k} = #{v}" }
73
+ end
74
+
75
+ # Discover available functions from two sources:
76
+ # 1. AST scan of the caller's source file (gets parameter signatures)
77
+ # 2. Receiver's methods minus Ruby builtins (catches require'd functions)
78
+ file_methods = begin
79
+ Mana::Introspect.methods_from_file(@caller_path)
80
+ rescue => _e
81
+ []
82
+ end
83
+ file_method_names = file_methods.map { |m| m[:name] }
84
+
85
+ # Methods on the receiver not from Object/Kernel (user-defined or require'd)
86
+ receiver = @binding.receiver
87
+ receiver_methods = (receiver.methods - Object.methods - Kernel.methods - [:~@, :mana])
88
+ .select { |m| receiver.method(m).owner != Object && receiver.method(m).owner != Kernel }
89
+ .reject { |m| file_method_names.include?(m.to_s) } # avoid duplicates with AST scan
90
+ .map { |m|
91
+ meth = receiver.method(m)
92
+ params = meth.parameters.map { |(type, name)|
93
+ case type
94
+ when :req then name.to_s
95
+ when :opt then "#{name}=..."
96
+ when :rest then "*#{name}"
97
+ when :keyreq then "#{name}:"
98
+ when :key then "#{name}: ..."
99
+ when :keyrest then "**#{name}"
100
+ when :block then "&#{name}"
101
+ else name.to_s
102
+ end
103
+ }
104
+ { name: m.to_s, params: params }
105
+ }
106
+
107
+ all_methods = file_methods + receiver_methods
108
+ # Append available function signatures so the LLM knows what it can call
109
+ unless all_methods.empty?
110
+ parts << ""
111
+ parts << Mana::Introspect.format_for_prompt(all_methods)
112
+ end
113
+
114
+ # Inject Ruby runtime environment snapshot
115
+ parts << ""
116
+ parts << ruby_environment
117
+
118
+ parts.join("\n")
119
+ end
120
+
121
+ # Build a concise snapshot of the Ruby runtime environment
122
+ def ruby_environment
123
+ lines = ["Environment:"]
124
+ lines << " Ruby #{RUBY_VERSION} | #{RUBY_PLATFORM} | pwd: #{Dir.pwd}"
125
+
126
+ # Loaded gems (top-level, skip bundler internals)
127
+ specs = Gem.loaded_specs.values
128
+ .reject { |s| %w[bundler rubygems].include?(s.name) }
129
+ .sort_by(&:name)
130
+ if specs.any?
131
+ gem_list = specs.map { |s| "#{s.name} #{s.version}" }.first(20)
132
+ gem_list << "... (#{specs.size} total)" if specs.size > 20
133
+ lines << " Gems: #{gem_list.join(', ')}"
134
+ end
135
+
136
+ # User-defined classes/modules (skip Ruby internals)
137
+ skip = [Object, Kernel, BasicObject, Module, Class, Mana, Mana::Engine,
138
+ Mana::Memory, Mana::Config, Mana::Chat]
139
+ user_classes = ObjectSpace.each_object(Class)
140
+ .reject { |c| c.name.nil? || c.name.start_with?("Mana::") || c.name.start_with?("#<") }
141
+ .reject { |c| skip.include?(c) }
142
+ .reject { |c| c.name.match?(/\A(Net|URI|IO|Gem|Bundler|RubyVM|RbConfig|Reline|JSON|YAML|Psych|Prism|Encoding|Errno|Signal|Thread|Fiber|Ractor|Process|GC|RDoc|IRB|Readline|StringIO|Monitor|PP|DidYouMean|ErrorHighlight|SyntaxSuggest|Coverage|SimpleCov|RSpec|WebMock)/) }
143
+ .map(&:name).sort
144
+ if user_classes.any?
145
+ class_list = user_classes.first(20)
146
+ class_list << "... (#{user_classes.size} total)" if user_classes.size > 20
147
+ lines << " Classes: #{class_list.join(', ')}"
148
+ end
149
+
150
+ # Local variables in scope
151
+ vars = @binding.local_variables.map(&:to_s).sort
152
+ lines << " Local vars: #{vars.join(', ')}" if vars.any?
153
+
154
+ lines.join("\n")
155
+ end
156
+ end
157
+ end
@@ -0,0 +1,180 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mana
4
+ # Dispatches LLM tool calls to their respective handlers.
5
+ # Mixed into Engine as a private method.
6
+ module ToolHandler
7
+ private
8
+
9
+ # Dispatch a single tool call from the LLM.
10
+ def handle_effect(tool_use, memory = nil)
11
+ name = tool_use[:name]
12
+ input = tool_use[:input] || {}
13
+ # Normalize keys to strings for consistent access
14
+ input = input.transform_keys(&:to_s) if input.is_a?(Hash)
15
+
16
+ case name
17
+ when "read_var"
18
+ # Read a variable from the caller's binding and return its serialized value
19
+ val = serialize_value(resolve(input["name"]))
20
+ vlog_value(" ↩ #{input['name']} =", val)
21
+ val
22
+
23
+ when "write_var"
24
+ # Write a value to the caller's binding and track it for the return value
25
+ var_name = input["name"]
26
+ value = input["value"]
27
+ write_local(var_name, value)
28
+ @written_vars[var_name] = value
29
+ vlog_value(" ✅ #{var_name} =", value)
30
+ "ok: #{var_name} = #{value.inspect}"
31
+
32
+ when "read_attr"
33
+ # Read an attribute (public method) from a Ruby object in scope
34
+ obj = resolve(input["obj"])
35
+ validate_name!(input["attr"])
36
+ serialize_value(obj.public_send(input["attr"]))
37
+
38
+ when "write_attr"
39
+ # Set an attribute (public setter) on a Ruby object in scope
40
+ obj = resolve(input["obj"])
41
+ validate_name!(input["attr"])
42
+ obj.public_send("#{input['attr']}=", input["value"])
43
+ "ok: #{input['obj']}.#{input['attr']} = #{input['value'].inspect}"
44
+
45
+ when "call_func"
46
+ handle_call_func(input)
47
+
48
+ when "knowledge"
49
+ # Look up information about ruby-mana from the knowledge base
50
+ self.class.knowledge(input["topic"])
51
+
52
+ when "remember"
53
+ # Store a fact in long-term memory (persistent across executions)
54
+ if @incognito
55
+ "Memory not saved (incognito mode)"
56
+ elsif memory
57
+ entry = memory.remember(input["content"])
58
+ "Remembered (id=#{entry[:id]}): #{input['content']}"
59
+ else
60
+ "Memory not available"
61
+ end
62
+
63
+ when "done"
64
+ # Signal task completion; the result becomes the return value
65
+ done_val = input["result"]
66
+ vlog_value("🏁 Done:", done_val)
67
+ vlog("═" * 60)
68
+ input["result"].to_s
69
+
70
+ when "error"
71
+ # LLM signals it cannot complete the task — raise as exception
72
+ msg = input["message"] || "LLM reported an error"
73
+ vlog("❌ Error: #{msg}")
74
+ vlog("═" * 60)
75
+ raise Mana::LLMError, msg
76
+
77
+ when "eval"
78
+ result = @binding.eval(input["code"])
79
+ vlog_value(" ↩ eval →", result)
80
+ serialize_value(result)
81
+
82
+ else
83
+ "error: unknown tool #{name}"
84
+ end
85
+ rescue LLMError
86
+ # LLMError must propagate to the caller (e.g. from the error tool)
87
+ raise
88
+ rescue SyntaxError => e
89
+ # Catch syntax errors from body eval so the LLM can retry with corrected code
90
+ "error: #{e.class}: #{e.message}"
91
+ rescue => e
92
+ # Return errors as strings so the LLM can see and react to them
93
+ "error: #{e.class}: #{e.message}"
94
+ end
95
+
96
+ # Handle call_func tool: chained calls, block bodies, simple calls
97
+ def handle_call_func(input)
98
+ func = input["name"]
99
+ args = input["args"] || []
100
+ kwargs = (input["kwargs"] || {}).transform_keys(&:to_sym)
101
+ body_code = input["body"]
102
+ block = @binding.eval("proc { #{body_code} }") if body_code
103
+
104
+ # Handle chained calls (e.g. Time.now, Array.new, File.read)
105
+ if func.include?(".")
106
+ return handle_chained_call(func, args, block)
107
+ end
108
+
109
+ # Handle body parameter for simple (non-chained) function calls
110
+ if body_code
111
+ validate_name!(func)
112
+ receiver = @binding.receiver
113
+ result = receiver.send(func.to_sym, *args, &block)
114
+ vlog(" ↩ #{func}(#{args.inspect}) with body → #{result.inspect}")
115
+ return serialize_value(result)
116
+ end
117
+
118
+ # Simple (non-chained) function call
119
+ validate_name!(func)
120
+
121
+ # Binding-sensitive method: local_variables returns scope-dependent results
122
+ if func == "local_variables"
123
+ return handle_local_variables
124
+ end
125
+
126
+ # Try local variable (lambdas/procs) first, then receiver methods
127
+ callable = if @binding.local_variables.include?(func.to_sym)
128
+ @binding.local_variable_get(func.to_sym)
129
+ elsif @binding.receiver.respond_to?(func.to_sym, true)
130
+ @binding.receiver.method(func.to_sym)
131
+ else
132
+ raise NameError, "undefined function '#{func}'"
133
+ end
134
+ result = kwargs.empty? ? callable.call(*args) : callable.call(*args, **kwargs)
135
+ call_desc = args.map(&:inspect).concat(kwargs.map { |k, v| "#{k}: #{v.inspect}" }).join(", ")
136
+ vlog_value(" ↩ #{func}(#{call_desc}) →", result)
137
+ serialize_value(result)
138
+ end
139
+
140
+ # Handle chained method calls like Time.now, Array.new(10) { rand }
141
+ def handle_chained_call(func, args, block)
142
+ first_dot = func.index(".")
143
+ receiver_name = func[0...first_dot]
144
+ rest = func[(first_dot + 1)..]
145
+ methods_chain = rest.split(".")
146
+ first_method = methods_chain.first
147
+
148
+ # Validate receiver is a simple constant name (e.g. "Time", "File", "Math")
149
+ unless receiver_name.match?(/\A[A-Z][A-Za-z0-9_]*(::[A-Z][A-Za-z0-9_]*)*\z/)
150
+ raise NameError, "'#{receiver_name}' is not a valid constant name"
151
+ end
152
+
153
+ begin
154
+ receiver = @binding.eval(receiver_name)
155
+ rescue => e
156
+ raise NameError, "cannot resolve '#{receiver_name}': #{e.message}"
157
+ end
158
+ result = receiver.public_send(first_method.to_sym, *args, &block)
159
+
160
+ # Chain remaining methods without args (e.g. .to_s, .strftime)
161
+ methods_chain[1..].each do |m|
162
+ result = result.public_send(m.to_sym)
163
+ end
164
+
165
+ vlog_value(" ↩ #{func}(#{args.map(&:inspect).join(', ')}) →", result)
166
+ serialize_value(result)
167
+ end
168
+
169
+ # Handle local_variables call with Mana-created singleton method tracking
170
+ def handle_local_variables
171
+ result = @binding.local_variables.map(&:to_s)
172
+ receiver = @binding.receiver
173
+ if receiver.instance_variable_defined?(:@__mana_vars__)
174
+ result = (result + receiver.instance_variable_get(:@__mana_vars__).map(&:to_s)).uniq
175
+ end
176
+ vlog(" ↩ local_variables() → #{result.size} variables")
177
+ serialize_value(result)
178
+ end
179
+ end
180
+ end
data/lib/mana/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Mana
4
- VERSION = "0.5.8"
4
+ VERSION = "0.5.10"
5
5
  end
data/lib/mana.rb CHANGED
@@ -2,18 +2,22 @@
2
2
 
3
3
  require_relative "mana/version"
4
4
  require_relative "mana/config"
5
- require_relative "mana/security_policy"
6
5
  require_relative "mana/backends/base"
7
6
  require_relative "mana/backends/anthropic"
8
7
  require_relative "mana/backends/openai"
9
8
  require_relative "mana/memory_store"
10
9
  require_relative "mana/memory"
11
10
  require_relative "mana/logger"
11
+ require_relative "mana/knowledge"
12
+ require_relative "mana/binding_helpers"
13
+ require_relative "mana/prompt_builder"
14
+ require_relative "mana/tool_handler"
12
15
  require_relative "mana/engine"
13
16
  require_relative "mana/introspect"
14
17
  require_relative "mana/compiler"
15
18
  require_relative "mana/string_ext"
16
19
  require_relative "mana/mixin"
20
+ require_relative "mana/chat"
17
21
 
18
22
  module Mana
19
23
  class Error < StandardError; end
@@ -65,6 +69,11 @@ module Mana
65
69
  def cache_dir=(dir)
66
70
  Compiler.cache_dir = dir
67
71
  end
72
+
73
+ # Enter interactive chat mode. Mana will have access to the caller's binding.
74
+ def chat
75
+ Chat.start(binding.of_caller(1))
76
+ end
68
77
  end
69
78
  end
70
79
 
metadata CHANGED
@@ -1,12 +1,12 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ruby-mana
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.8
4
+ version: 0.5.10
5
5
  platform: ruby
6
6
  authors:
7
7
  - Carl Li
8
8
  autorequire:
9
- bindir: bin
9
+ bindir: exe
10
10
  cert_chain: []
11
11
  date: 2026-03-27 00:00:00.000000000 Z
12
12
  dependencies:
@@ -24,33 +24,67 @@ dependencies:
24
24
  - - ">="
25
25
  - !ruby/object:Gem::Version
26
26
  version: '1.0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: reline
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0.5'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0.5'
41
+ - !ruby/object:Gem::Dependency
42
+ name: dotenv
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '2.0'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '2.0'
27
55
  description: |
28
56
  Mana lets you write natural language strings in Ruby that execute via LLM
29
57
  with full access to your program's live state. Read/write variables, call
30
58
  functions, manipulate objects — all from a simple ~"..." syntax.
31
59
  email:
32
- executables: []
60
+ executables:
61
+ - mana
33
62
  extensions: []
34
63
  extra_rdoc_files: []
35
64
  files:
36
65
  - CHANGELOG.md
37
66
  - LICENSE
38
67
  - README.md
68
+ - exe/mana
39
69
  - lib/mana.rb
40
70
  - lib/mana/backends/anthropic.rb
41
71
  - lib/mana/backends/base.rb
42
72
  - lib/mana/backends/openai.rb
73
+ - lib/mana/binding_helpers.rb
74
+ - lib/mana/chat.rb
43
75
  - lib/mana/compiler.rb
44
76
  - lib/mana/config.rb
45
77
  - lib/mana/engine.rb
46
78
  - lib/mana/introspect.rb
79
+ - lib/mana/knowledge.rb
47
80
  - lib/mana/logger.rb
48
81
  - lib/mana/memory.rb
49
82
  - lib/mana/memory_store.rb
50
83
  - lib/mana/mixin.rb
51
84
  - lib/mana/mock.rb
52
- - lib/mana/security_policy.rb
85
+ - lib/mana/prompt_builder.rb
53
86
  - lib/mana/string_ext.rb
87
+ - lib/mana/tool_handler.rb
54
88
  - lib/mana/version.rb
55
89
  homepage: https://github.com/twokidsCarl/ruby-mana
56
90
  licenses: