ruby-mana 0.2.0 → 0.3.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: fa9b6637652accc3c6223e613713b06f3ef24654324c3498deac36067fbca29f
4
- data.tar.gz: dc9722c916a0bcc6d599d37e24d68fc40870a6c35b0736d211266a7b4085c725
3
+ metadata.gz: ddacfb41b0e2ca07887ffd1c683e211f90529901a4a401393c1c1da91f5da46c
4
+ data.tar.gz: 33db80124c57c45499b0a8ae392463782d6e3c6f221853e9d1353ac7f64838af
5
5
  SHA512:
6
- metadata.gz: 911c263d7ad2e854c0268878ac19ac7e353d5d293e6683ae8453963c7edcb2443526ffdfdd019d24bbe024f729138ea3bad3234c96c16e968b6b997f81d298cb
7
- data.tar.gz: b20334705ffd23d95249e5f3411e54e7e74da25442ebe059745de41ea1dbe88496377b410c3964979a86d2f64cab6e0755291bd5e860d5a116af6cfe535e50a9
6
+ metadata.gz: 2c604e080b7a6d4cf04d54d4782d7c36d9789d76f7e485728c1eb1869757ae320d688a8f222c00d9591116e300fec2f7272d77f04b1d9c402edef714ea628b24
7
+ data.tar.gz: 2ac2008b69a995bf5d1c4a098d25e3cb89b02b6a83e48c15014a7ba1268efecb3f84ca756a4efd3d53a200b6e160d0c79c38f6571eb581895dac6a9764e343e7
data/README.md CHANGED
@@ -135,6 +135,14 @@ Mana.configure do |c|
135
135
  c.temperature = 0
136
136
  c.api_key = ENV["ANTHROPIC_API_KEY"]
137
137
  c.max_iterations = 50
138
+
139
+ # Memory settings
140
+ c.namespace = "my-project" # nil = auto-detect from git/pwd
141
+ c.context_window = 200_000 # nil = auto-detect from model
142
+ c.memory_pressure = 0.7 # compact when tokens exceed 70% of context window
143
+ c.memory_keep_recent = 4 # keep last 4 rounds during compaction
144
+ c.compact_model = nil # nil = use main model for compaction
145
+ c.memory_store = Mana::FileStore.new # default file-based persistence
138
146
  end
139
147
  ```
140
148
 
@@ -166,6 +174,49 @@ Mana.define_effect :search_web,
166
174
 
167
175
  Built-in effects (`read_var`, `write_var`, `read_attr`, `write_attr`, `call_func`, `done`) are reserved and cannot be overridden.
168
176
 
177
+ ### Memory — automatic context sharing
178
+
179
+ Consecutive `~"..."` calls automatically share context. No wrapper block needed:
180
+
181
+ ```ruby
182
+ ~"remember: always translate to Japanese, casual tone"
183
+ ~"translate <text1>, store in <result1>" # uses the preference
184
+ ~"translate <text2>, store in <result2>" # still remembers
185
+ ~"which translation was harder? store in <analysis>" # can reference both
186
+ ```
187
+
188
+ Memory is per-thread and auto-created on the first `~"..."` call.
189
+
190
+ #### Long-term memory
191
+
192
+ The LLM has a `remember` tool that persists facts across script executions:
193
+
194
+ ```ruby
195
+ ~"remember that the user prefers concise output"
196
+ # ... later, in a different script execution ...
197
+ ~"translate <text>" # LLM sees the preference in its long-term memory
198
+ ```
199
+
200
+ Manage long-term memory via Ruby:
201
+
202
+ ```ruby
203
+ Mana.memory.long_term # view all memories
204
+ Mana.memory.forget(id: 2) # remove a specific memory
205
+ Mana.memory.clear_long_term! # clear all long-term memories
206
+ Mana.memory.clear_short_term! # clear conversation history
207
+ Mana.memory.clear! # clear everything
208
+ ```
209
+
210
+ #### Incognito mode
211
+
212
+ Run without any memory — nothing is loaded or saved:
213
+
214
+ ```ruby
215
+ Mana.incognito do
216
+ ~"translate <text>" # no memory, no persistence
217
+ end
218
+ ```
219
+
169
220
  ### LLM-compiled methods
170
221
 
171
222
  `mana def` lets LLM generate a method implementation on first call. The generated code is cached as a real `.rb` file — subsequent calls are pure Ruby with zero API overhead.
@@ -206,9 +257,11 @@ Generated files live in `.mana_cache/` (add to `.gitignore`, or commit them to s
206
257
 
207
258
  1. `~"..."` calls `String#~@`, which captures the caller's `Binding`
208
259
  2. Mana parses `<var>` references and reads existing variables as context
209
- 3. The prompt + context is sent to the LLM with tools: `read_var`, `write_var`, `read_attr`, `write_attr`, `call_func`, `done`
210
- 4. LLM responds with tool calls Mana executes them against the live Ruby binding sends results back
211
- 5. Loop until LLM calls `done` or returns without tool calls
260
+ 3. Memory loads long-term facts and prior conversation into the system prompt
261
+ 4. The prompt + context is sent to the LLM with tools: `read_var`, `write_var`, `read_attr`, `write_attr`, `call_func`, `remember`, `done`
262
+ 5. LLM responds with tool calls Mana executes them against the live Ruby binding → sends results back
263
+ 6. Loop until LLM calls `done` or returns without tool calls
264
+ 7. After completion, memory compaction runs in background if context is getting large
212
265
 
213
266
  ## Safety
214
267
 
data/lib/mana/config.rb CHANGED
@@ -2,7 +2,10 @@
2
2
 
3
3
  module Mana
4
4
  class Config
5
- attr_accessor :model, :temperature, :api_key, :max_iterations, :base_url
5
+ attr_accessor :model, :temperature, :api_key, :max_iterations, :base_url,
6
+ :namespace, :memory_store, :memory_path,
7
+ :context_window, :memory_pressure, :memory_keep_recent,
8
+ :compact_model, :on_compact
6
9
 
7
10
  def initialize
8
11
  @model = "claude-sonnet-4-20250514"
@@ -10,6 +13,14 @@ module Mana
10
13
  @api_key = ENV["ANTHROPIC_API_KEY"]
11
14
  @max_iterations = 50
12
15
  @base_url = "https://api.anthropic.com"
16
+ @namespace = nil
17
+ @memory_store = nil
18
+ @memory_path = nil
19
+ @context_window = nil
20
+ @memory_pressure = 0.7
21
+ @memory_keep_recent = 4
22
+ @compact_model = nil
23
+ @on_compact = nil
13
24
  end
14
25
  end
15
26
  end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mana
4
+ module ContextWindow
5
+ SIZES = {
6
+ /claude-3-5-sonnet/ => 200_000,
7
+ /claude-sonnet-4/ => 200_000,
8
+ /claude-3-5-haiku/ => 200_000,
9
+ /claude-3-opus/ => 200_000,
10
+ /claude-opus-4/ => 200_000,
11
+ /gpt-4o/ => 128_000,
12
+ /gpt-4-turbo/ => 128_000,
13
+ /gpt-3\.5/ => 16_385
14
+ }.freeze
15
+
16
+ DEFAULT = 128_000
17
+
18
+ def self.detect(model_name)
19
+ return DEFAULT unless model_name
20
+
21
+ SIZES.each do |pattern, size|
22
+ return size if model_name.match?(pattern)
23
+ end
24
+
25
+ DEFAULT
26
+ end
27
+ end
28
+ end
@@ -104,7 +104,7 @@ module Mana
104
104
  end
105
105
  end
106
106
 
107
- RESERVED_EFFECTS = %w[read_var write_var read_attr write_attr call_func done].freeze
107
+ RESERVED_EFFECTS = %w[read_var write_var read_attr write_attr call_func done remember].freeze
108
108
 
109
109
  class << self
110
110
  def registry
data/lib/mana/engine.rb CHANGED
@@ -77,6 +77,16 @@ module Mana
77
77
  }
78
78
  ].freeze
79
79
 
80
+ REMEMBER_TOOL = {
81
+ name: "remember",
82
+ description: "Store a fact in long-term memory. This memory persists across script executions. Use when the user explicitly asks to remember something.",
83
+ input_schema: {
84
+ type: "object",
85
+ properties: { content: { type: "string", description: "The fact to remember" } },
86
+ required: ["content"]
87
+ }
88
+ }.freeze
89
+
80
90
  class << self
81
91
  def run(prompt, caller_binding)
82
92
  new(prompt, caller_binding).execute
@@ -93,9 +103,11 @@ module Mana
93
103
  handler_stack.pop
94
104
  end
95
105
 
96
- # Built-in tools + any registered custom effects
106
+ # Built-in tools + remember + any registered custom effects
97
107
  def all_tools
98
- TOOLS + Mana::EffectRegistry.tool_definitions
108
+ tools = TOOLS.dup
109
+ tools << REMEMBER_TOOL unless Memory.incognito?
110
+ tools + Mana::EffectRegistry.tool_definitions
99
111
  end
100
112
  end
101
113
 
@@ -104,12 +116,19 @@ module Mana
104
116
  @binding = caller_binding
105
117
  @config = Mana.config
106
118
  @caller_path = caller_source_path
119
+ @incognito = Memory.incognito?
107
120
  end
108
121
 
109
122
  def execute
110
123
  context = build_context(@prompt)
111
124
  system_prompt = build_system_prompt(context)
112
- messages = [{ role: "user", content: @prompt }]
125
+
126
+ # Use memory's short_term messages (auto per-thread), or fresh if incognito
127
+ memory = @incognito ? nil : Memory.current
128
+ memory&.wait_for_compaction
129
+
130
+ messages = memory ? memory.short_term : []
131
+ messages << { role: "user", content: @prompt }
113
132
 
114
133
  iterations = 0
115
134
  done_result = nil
@@ -128,7 +147,7 @@ module Mana
128
147
 
129
148
  # Process each tool use
130
149
  tool_results = tool_uses.map do |tu|
131
- result = handle_effect(tu)
150
+ result = handle_effect(tu, memory)
132
151
  done_result = (tu[:input][:result] || tu[:input]["result"]) if tu[:name] == "done"
133
152
  { type: "tool_result", tool_use_id: tu[:id], content: result.to_s }
134
153
  end
@@ -138,6 +157,14 @@ module Mana
138
157
  break if tool_uses.any? { |t| t[:name] == "done" }
139
158
  end
140
159
 
160
+ # Append a final assistant summary so LLM has full context next call
161
+ if memory && done_result
162
+ messages << { role: "assistant", content: [{ type: "text", text: "Done: #{done_result}" }] }
163
+ end
164
+
165
+ # Schedule compaction if needed (runs in background)
166
+ memory&.schedule_compaction
167
+
141
168
  done_result
142
169
  end
143
170
 
@@ -172,6 +199,33 @@ module Mana
172
199
  "- Be precise with types: use numbers for numeric values, arrays for lists, strings for text."
173
200
  ]
174
201
 
202
+ # Inject long-term memories or incognito notice
203
+ if @incognito
204
+ parts << ""
205
+ parts << "You are in incognito mode. The remember tool is disabled. No memories will be loaded or saved."
206
+ else
207
+ memory = Memory.current
208
+ if memory
209
+ # Inject summaries from compaction
210
+ unless memory.summaries.empty?
211
+ parts << ""
212
+ parts << "Previous conversation summary:"
213
+ memory.summaries.each { |s| parts << " #{s}" }
214
+ end
215
+
216
+ unless memory.long_term.empty?
217
+ parts << ""
218
+ parts << "Long-term memories (persistent across executions):"
219
+ memory.long_term.each { |m| parts << "- #{m[:content]}" }
220
+ end
221
+
222
+ unless memory.long_term.empty?
223
+ parts << ""
224
+ parts << "You have a `remember` tool to store new facts in long-term memory when the user asks."
225
+ end
226
+ end
227
+ end
228
+
175
229
  unless context.empty?
176
230
  parts << ""
177
231
  parts << "Current variable values:"
@@ -205,7 +259,7 @@ module Mana
205
259
 
206
260
  # --- Effect Handling ---
207
261
 
208
- def handle_effect(tool_use)
262
+ def handle_effect(tool_use, memory = nil)
209
263
  name = tool_use[:name]
210
264
  input = tool_use[:input] || {}
211
265
  # Normalize keys to strings for consistent access
@@ -247,6 +301,16 @@ module Mana
247
301
  result = @binding.receiver.method(func.to_sym).call(*args)
248
302
  serialize_value(result)
249
303
 
304
+ when "remember"
305
+ if @incognito
306
+ "Memory not saved (incognito mode)"
307
+ elsif memory
308
+ entry = memory.remember(input["content"])
309
+ "Remembered (id=#{entry[:id]}): #{input['content']}"
310
+ else
311
+ "Memory not available"
312
+ end
313
+
250
314
  when "done"
251
315
  input["result"].to_s
252
316
 
@@ -0,0 +1,236 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mana
4
+ class Memory
5
+ attr_reader :short_term, :long_term, :summaries
6
+
7
+ def initialize
8
+ @short_term = []
9
+ @long_term = []
10
+ @summaries = []
11
+ @next_id = 1
12
+ @compact_mutex = Mutex.new
13
+ @compact_thread = nil
14
+ load_long_term
15
+ end
16
+
17
+ # --- Class methods ---
18
+
19
+ class << self
20
+ def current
21
+ return nil if incognito?
22
+
23
+ Thread.current[:mana_memory] ||= new
24
+ end
25
+
26
+ def incognito?
27
+ Thread.current[:mana_incognito] == true
28
+ end
29
+
30
+ def incognito(&block)
31
+ previous_memory = Thread.current[:mana_memory]
32
+ previous_incognito = Thread.current[:mana_incognito]
33
+ Thread.current[:mana_incognito] = true
34
+ Thread.current[:mana_memory] = nil
35
+ block.call
36
+ ensure
37
+ Thread.current[:mana_incognito] = previous_incognito
38
+ Thread.current[:mana_memory] = previous_memory
39
+ end
40
+ end
41
+
42
+ # --- Token estimation ---
43
+
44
+ def token_count
45
+ count = 0
46
+ @short_term.each do |msg|
47
+ content = msg[:content]
48
+ case content
49
+ when String
50
+ count += estimate_tokens(content)
51
+ when Array
52
+ content.each do |block|
53
+ count += estimate_tokens(block[:text] || block[:content] || "")
54
+ end
55
+ end
56
+ end
57
+ @long_term.each { |m| count += estimate_tokens(m[:content]) }
58
+ @summaries.each { |s| count += estimate_tokens(s) }
59
+ count
60
+ end
61
+
62
+ # --- Memory management ---
63
+
64
+ def clear!
65
+ clear_short_term!
66
+ clear_long_term!
67
+ end
68
+
69
+ def clear_short_term!
70
+ @short_term.clear
71
+ @summaries.clear
72
+ end
73
+
74
+ def clear_long_term!
75
+ @long_term.clear
76
+ store.clear(namespace)
77
+ end
78
+
79
+ def forget(id:)
80
+ @long_term.reject! { |m| m[:id] == id }
81
+ store.write(namespace, @long_term)
82
+ end
83
+
84
+ def remember(content)
85
+ entry = { id: @next_id, content: content, created_at: Time.now.iso8601 }
86
+ @next_id += 1
87
+ @long_term << entry
88
+ store.write(namespace, @long_term)
89
+ entry
90
+ end
91
+
92
+ # --- Compaction ---
93
+
94
+ def compact!
95
+ wait_for_compaction
96
+ perform_compaction
97
+ end
98
+
99
+ def needs_compaction?
100
+ cw = context_window
101
+ token_count > (cw * Mana.config.memory_pressure)
102
+ end
103
+
104
+ def schedule_compaction
105
+ return unless needs_compaction?
106
+
107
+ @compact_mutex.synchronize do
108
+ return if @compact_thread&.alive?
109
+
110
+ @compact_thread = Thread.new do
111
+ perform_compaction
112
+ rescue => e
113
+ # Silently handle compaction errors — don't crash the main thread
114
+ $stderr.puts "Mana compaction error: #{e.message}" if $DEBUG
115
+ end
116
+ end
117
+ end
118
+
119
+ def wait_for_compaction
120
+ thread = @compact_mutex.synchronize { @compact_thread }
121
+ thread&.join
122
+ end
123
+
124
+ # --- Display ---
125
+
126
+ def inspect
127
+ "#<Mana::Memory long_term=#{@long_term.size}, short_term=#{short_term_rounds} rounds, tokens=#{token_count}/#{context_window}>"
128
+ end
129
+
130
+ private
131
+
132
+ def short_term_rounds
133
+ @short_term.count { |m| m[:role] == "user" && m[:content].is_a?(String) }
134
+ end
135
+
136
+ def estimate_tokens(text)
137
+ return 0 unless text.is_a?(String)
138
+
139
+ # Rough estimate: ~4 chars per token
140
+ (text.length / 4.0).ceil
141
+ end
142
+
143
+ def context_window
144
+ Mana.config.context_window || ContextWindow.detect(Mana.config.model)
145
+ end
146
+
147
+ def store
148
+ Mana.config.memory_store || default_store
149
+ end
150
+
151
+ def default_store
152
+ @default_store ||= FileStore.new
153
+ end
154
+
155
+ def namespace
156
+ Namespace.detect
157
+ end
158
+
159
+ def load_long_term
160
+ return if self.class.incognito?
161
+
162
+ @long_term = store.read(namespace)
163
+ @next_id = (@long_term.map { |m| m[:id] }.max || 0) + 1
164
+ end
165
+
166
+ def perform_compaction
167
+ keep_recent = Mana.config.memory_keep_recent
168
+ # Count user-prompt messages (rounds)
169
+ user_indices = @short_term.each_with_index
170
+ .select { |msg, _| msg[:role] == "user" && msg[:content].is_a?(String) }
171
+ .map(&:last)
172
+
173
+ return if user_indices.size <= keep_recent
174
+
175
+ # Find the cutoff point: keep the last N rounds
176
+ cutoff_user_idx = user_indices[-(keep_recent)]
177
+ old_messages = @short_term[0...cutoff_user_idx]
178
+ return if old_messages.empty?
179
+
180
+ # Build text from old messages for summarization
181
+ text_parts = old_messages.map do |msg|
182
+ content = msg[:content]
183
+ case content
184
+ when String then "#{msg[:role]}: #{content}"
185
+ when Array
186
+ texts = content.map { |b| b[:text] || b[:content] }.compact
187
+ "#{msg[:role]}: #{texts.join(' ')}" unless texts.empty?
188
+ end
189
+ end.compact
190
+
191
+ return if text_parts.empty?
192
+
193
+ summary = summarize(text_parts.join("\n"))
194
+
195
+ # Replace old messages with summary
196
+ @short_term = @short_term[cutoff_user_idx..]
197
+ @summaries << summary
198
+
199
+ Mana.config.on_compact&.call(summary)
200
+ end
201
+
202
+ def summarize(text)
203
+ config = Mana.config
204
+ model = config.compact_model || config.model
205
+ uri = URI("#{config.base_url}/v1/messages")
206
+
207
+ body = {
208
+ model: model,
209
+ max_tokens: 1024,
210
+ system: "Summarize this conversation concisely. Preserve key facts, decisions, and context.",
211
+ messages: [{ role: "user", content: text }]
212
+ }
213
+
214
+ http = Net::HTTP.new(uri.host, uri.port)
215
+ http.use_ssl = uri.scheme == "https"
216
+ http.read_timeout = 60
217
+
218
+ req = Net::HTTP::Post.new(uri)
219
+ req["Content-Type"] = "application/json"
220
+ req["x-api-key"] = config.api_key
221
+ req["anthropic-version"] = "2023-06-01"
222
+ req.body = JSON.generate(body)
223
+
224
+ res = http.request(req)
225
+ return "Summary unavailable" unless res.is_a?(Net::HTTPSuccess)
226
+
227
+ parsed = JSON.parse(res.body, symbolize_names: true)
228
+ content = parsed[:content]
229
+ return "Summary unavailable" unless content.is_a?(Array)
230
+
231
+ content.map { |b| b[:text] }.compact.join("\n")
232
+ rescue => _e
233
+ "Summary unavailable"
234
+ end
235
+ end
236
+ end
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "fileutils"
5
+
6
+ module Mana
7
+ class MemoryStore
8
+ def read(namespace)
9
+ raise NotImplementedError
10
+ end
11
+
12
+ def write(namespace, memories)
13
+ raise NotImplementedError
14
+ end
15
+
16
+ def clear(namespace)
17
+ raise NotImplementedError
18
+ end
19
+ end
20
+
21
+ class FileStore < MemoryStore
22
+ def initialize(base_path = nil)
23
+ @base_path = base_path
24
+ end
25
+
26
+ def read(namespace)
27
+ path = file_path(namespace)
28
+ return [] unless File.exist?(path)
29
+
30
+ data = JSON.parse(File.read(path), symbolize_names: true)
31
+ data.is_a?(Array) ? data : []
32
+ rescue JSON::ParserError
33
+ []
34
+ end
35
+
36
+ def write(namespace, memories)
37
+ path = file_path(namespace)
38
+ FileUtils.mkdir_p(File.dirname(path))
39
+ File.write(path, JSON.pretty_generate(memories))
40
+ end
41
+
42
+ def clear(namespace)
43
+ path = file_path(namespace)
44
+ File.delete(path) if File.exist?(path)
45
+ end
46
+
47
+ private
48
+
49
+ def file_path(namespace)
50
+ File.join(base_dir, "#{namespace}.json")
51
+ end
52
+
53
+ def base_dir
54
+ return File.join(@base_path, "memory") if @base_path
55
+
56
+ custom_path = Mana.config.memory_path
57
+ return File.join(custom_path, "memory") if custom_path
58
+
59
+ xdg = ENV["XDG_DATA_HOME"]
60
+ if xdg && !xdg.empty?
61
+ File.join(xdg, "mana", "memory")
62
+ elsif RUBY_PLATFORM.include?("darwin")
63
+ File.join(Dir.home, "Library", "Application Support", "mana", "memory")
64
+ else
65
+ File.join(Dir.home, ".local", "share", "mana", "memory")
66
+ end
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mana
4
+ module Namespace
5
+ class << self
6
+ def detect
7
+ configured || from_git_repo || from_gemfile_dir || from_pwd || "default"
8
+ end
9
+
10
+ def configured
11
+ ns = Mana.config.namespace
12
+ ns unless ns.nil? || ns.to_s.empty?
13
+ end
14
+
15
+ def from_git_repo
16
+ dir = `git rev-parse --show-toplevel 2>/dev/null`.strip
17
+ return nil if dir.empty?
18
+
19
+ File.basename(dir)
20
+ end
21
+
22
+ def from_gemfile_dir
23
+ dir = Dir.pwd
24
+ loop do
25
+ return File.basename(dir) if File.exist?(File.join(dir, "Gemfile"))
26
+
27
+ parent = File.dirname(dir)
28
+ return nil if parent == dir
29
+
30
+ dir = parent
31
+ end
32
+ end
33
+
34
+ def from_pwd
35
+ File.basename(Dir.pwd)
36
+ end
37
+ end
38
+ end
39
+ 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.2.0"
4
+ VERSION = "0.3.0"
5
5
  end
data/lib/mana.rb CHANGED
@@ -3,6 +3,10 @@
3
3
  require_relative "mana/version"
4
4
  require_relative "mana/config"
5
5
  require_relative "mana/effect_registry"
6
+ require_relative "mana/namespace"
7
+ require_relative "mana/memory_store"
8
+ require_relative "mana/context_window"
9
+ require_relative "mana/memory"
6
10
  require_relative "mana/engine"
7
11
  require_relative "mana/introspect"
8
12
  require_relative "mana/compiler"
@@ -35,6 +39,7 @@ module Mana
35
39
  def reset!
36
40
  @config = Config.new
37
41
  EffectRegistry.clear!
42
+ Thread.current[:mana_memory] = nil
38
43
  end
39
44
 
40
45
  # Define a custom effect that becomes an LLM tool
@@ -47,6 +52,16 @@ module Mana
47
52
  EffectRegistry.undefine(name)
48
53
  end
49
54
 
55
+ # Access current thread's memory
56
+ def memory
57
+ Memory.current
58
+ end
59
+
60
+ # Run a block in incognito mode (no memory)
61
+ def incognito(&block)
62
+ Memory.incognito(&block)
63
+ end
64
+
50
65
  # View generated source for a mana-compiled method
51
66
  def source(method_name, owner: nil)
52
67
  Compiler.source(method_name, owner: owner)
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.2.0
4
+ version: 0.3.0
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-20 00:00:00.000000000 Z
11
+ date: 2026-02-21 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: binding_of_caller
@@ -39,10 +39,14 @@ files:
39
39
  - lib/mana.rb
40
40
  - lib/mana/compiler.rb
41
41
  - lib/mana/config.rb
42
+ - lib/mana/context_window.rb
42
43
  - lib/mana/effect_registry.rb
43
44
  - lib/mana/engine.rb
44
45
  - lib/mana/introspect.rb
46
+ - lib/mana/memory.rb
47
+ - lib/mana/memory_store.rb
45
48
  - lib/mana/mixin.rb
49
+ - lib/mana/namespace.rb
46
50
  - lib/mana/string_ext.rb
47
51
  - lib/mana/version.rb
48
52
  homepage: https://github.com/carlnoah6/ruby-mana