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 +4 -4
- data/README.md +56 -3
- data/lib/mana/config.rb +12 -1
- data/lib/mana/context_window.rb +28 -0
- data/lib/mana/effect_registry.rb +1 -1
- data/lib/mana/engine.rb +69 -5
- data/lib/mana/memory.rb +236 -0
- data/lib/mana/memory_store.rb +69 -0
- data/lib/mana/namespace.rb +39 -0
- data/lib/mana/version.rb +1 -1
- data/lib/mana.rb +15 -0
- metadata +6 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: ddacfb41b0e2ca07887ffd1c683e211f90529901a4a401393c1c1da91f5da46c
|
|
4
|
+
data.tar.gz: 33db80124c57c45499b0a8ae392463782d6e3c6f221853e9d1353ac7f64838af
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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.
|
|
210
|
-
4.
|
|
211
|
-
5.
|
|
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
|
data/lib/mana/effect_registry.rb
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
|
data/lib/mana/memory.rb
ADDED
|
@@ -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
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.
|
|
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-
|
|
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
|