ruby-mana 0.5.11 → 0.5.12
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 +24 -0
- data/README.md +8 -57
- data/lib/mana/config.rb +8 -1
- data/lib/mana/context.rb +86 -0
- data/lib/mana/engine.rb +28 -48
- data/lib/mana/knowledge.rb +5 -17
- data/lib/mana/memory.rb +5 -1
- data/lib/mana/prompt_builder.rb +16 -26
- data/lib/mana/tool_handler.rb +68 -67
- data/lib/mana/version.rb +1 -1
- data/lib/mana.rb +50 -8
- metadata +3 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 0f9221479991a9a07659b6d8aee20d2e0e377f6159e3116fbf01839219d8d011
|
|
4
|
+
data.tar.gz: dc2caf733e6a7f4e330599edff45c72557b81205c7b29382237dfa9b286c903b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 3389dabca2ba36ab0e3cf2e5ae2f780f9507eadce0f7551c3ebcf22792003342364cf45dc01bd0315713b8998a2c4be0ec5aede5d13eca2adf493339be6bd038
|
|
7
|
+
data.tar.gz: 876a82b19006c6b12822ffaf264d89ac5d81c0e56871d595d7027a72f94c0aa57ec11251ab7458b76338b80fb6f756a700828064d266aaf8084298cc84e77ab5
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,29 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.5.12] - 2026-04-04
|
|
4
|
+
|
|
5
|
+
### Changed
|
|
6
|
+
- Reposition ruby-mana as a pure embedded LLM engine; agent features (chat, memory, compaction) moved to ruby-claw
|
|
7
|
+
- Rename `Mana::Memory` to `Mana::Context`; `short_term` to `messages`
|
|
8
|
+
- Remove `incognito` mechanism from mana (moved to claw layer)
|
|
9
|
+
- Remove `remember` tool and `REMEMBER_TOOL` constant (now registered by claw via tool interface)
|
|
10
|
+
- Remove long-term memory injection from prompt builder
|
|
11
|
+
- Refactor `tool_handler.rb`: case/when dispatch replaced with `BUILTIN_TOOLS` + `send("handle_#{name}")` dispatch map
|
|
12
|
+
- Update `eval` tool description to emphasize "define new methods/classes/require" role
|
|
13
|
+
- `config.memory_class` renamed to `config.context_class`
|
|
14
|
+
|
|
15
|
+
### Added
|
|
16
|
+
- `Mana.register_tool(definition, &handler)` — external tool registration interface
|
|
17
|
+
- `Mana.register_prompt_section(&block)` — external prompt injection interface
|
|
18
|
+
- `Mana.tool_handlers` — access registered tool handler map
|
|
19
|
+
- `BUILTIN_TOOLS` constant in ToolHandler for dispatch map
|
|
20
|
+
|
|
21
|
+
### Removed
|
|
22
|
+
- `Mana.incognito` / `Context.incognito?` / `Context.incognito` — incognito is now claw's responsibility
|
|
23
|
+
- `REMEMBER_TOOL` constant
|
|
24
|
+
- `@incognito` instance variable from Engine
|
|
25
|
+
- Long-term memory (`long_term`, `remember`, `forget`) from Context
|
|
26
|
+
|
|
3
27
|
## [0.5.11] - 2026-03-27
|
|
4
28
|
|
|
5
29
|
### Changed
|
data/README.md
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
[](https://rubygems.org/gems/ruby-mana) · [Website](https://twokidscarl.github.io/ruby-mana/) · [RubyGems](https://rubygems.org/gems/ruby-mana) · [GitHub](https://github.com/twokidsCarl/ruby-mana)
|
|
4
4
|
|
|
5
|
+
**Looking for interactive chat, persistent memory, and agent features?** See [ruby-claw](https://github.com/twokidsCarl/ruby-claw).
|
|
6
|
+
|
|
5
7
|
Embed LLM as native Ruby. Write natural language, it just runs. Not an API wrapper — a language construct that weaves LLM into your code.
|
|
6
8
|
|
|
7
9
|
```ruby
|
|
@@ -188,51 +190,6 @@ lint = ->(code) { ~"check #{code} for style issues, store in <issues>" }
|
|
|
188
190
|
|
|
189
191
|
Each nested call gets its own conversation context. The outer LLM only sees the function's return value, keeping its context clean.
|
|
190
192
|
|
|
191
|
-
### Memory
|
|
192
|
-
|
|
193
|
-
Mana has two types of memory:
|
|
194
|
-
|
|
195
|
-
- **Short-term memory** — conversation history within the current process. Each `~"..."` call appends to it, so consecutive calls share context. Persisted to session files by default (survives restarts).
|
|
196
|
-
- **Long-term memory** — persistent facts stored on disk (`.mana/` in your project directory). Survives across script executions. The LLM can save facts via the `remember` tool.
|
|
197
|
-
|
|
198
|
-
```ruby
|
|
199
|
-
~"translate <text1> to Japanese, store in <result1>"
|
|
200
|
-
~"translate <text2> to the same language, store in <result2>" # remembers "Japanese"
|
|
201
|
-
|
|
202
|
-
~"remember that the user prefers concise output"
|
|
203
|
-
# persists to .mana/ — available in future script runs
|
|
204
|
-
```
|
|
205
|
-
|
|
206
|
-
```ruby
|
|
207
|
-
Mana.memory.short_term # view conversation history
|
|
208
|
-
Mana.memory.long_term # view persisted facts
|
|
209
|
-
Mana.memory.forget(id: 2) # remove a specific fact
|
|
210
|
-
Mana.memory.clear! # clear everything
|
|
211
|
-
```
|
|
212
|
-
|
|
213
|
-
#### Compaction
|
|
214
|
-
|
|
215
|
-
When conversation history grows large, Mana automatically compacts old messages into summaries:
|
|
216
|
-
|
|
217
|
-
```ruby
|
|
218
|
-
Mana.configure do |c|
|
|
219
|
-
c.memory_pressure = 0.7 # compact when tokens > 70% of context window
|
|
220
|
-
c.memory_keep_recent = 4 # keep last 4 rounds, summarize the rest
|
|
221
|
-
c.compact_model = nil # nil = use main model for summarization
|
|
222
|
-
c.on_compact = ->(summary) { puts "Compacted: #{summary}" }
|
|
223
|
-
end
|
|
224
|
-
```
|
|
225
|
-
|
|
226
|
-
#### Incognito mode
|
|
227
|
-
|
|
228
|
-
Run without any memory — nothing is loaded or saved:
|
|
229
|
-
|
|
230
|
-
```ruby
|
|
231
|
-
Mana.incognito do
|
|
232
|
-
~"translate <text>" # no memory, no persistence
|
|
233
|
-
end
|
|
234
|
-
```
|
|
235
|
-
|
|
236
193
|
## Configuration
|
|
237
194
|
|
|
238
195
|
All options can be set via environment variables (`.env` file) or `Mana.configure`:
|
|
@@ -267,16 +224,13 @@ Mana.configure do |c|
|
|
|
267
224
|
c.api_key = "sk-..."
|
|
268
225
|
c.verbose = true
|
|
269
226
|
c.timeout = 120
|
|
270
|
-
|
|
271
|
-
# Memory settings
|
|
227
|
+
c.max_iterations = 20 # max tool-call rounds per prompt
|
|
272
228
|
c.namespace = "my-project" # nil = auto-detect from git/pwd
|
|
273
229
|
c.context_window = 128_000 # default: 128_000
|
|
274
|
-
c.memory_pressure = 0.7 # compact when tokens exceed 70% of context window
|
|
275
|
-
c.memory_keep_recent = 4 # keep last 4 rounds during compaction
|
|
276
|
-
c.compact_model = nil # nil = use main model for compaction
|
|
277
230
|
c.memory_store = Mana::FileStore.new # default file-based persistence
|
|
278
|
-
c.
|
|
279
|
-
c.
|
|
231
|
+
c.memory_path = ".mana" # directory for memory files
|
|
232
|
+
c.context_class = nil # custom context class (e.g. from agent frameworks)
|
|
233
|
+
c.knowledge_provider = nil # custom knowledge provider
|
|
280
234
|
end
|
|
281
235
|
```
|
|
282
236
|
|
|
@@ -345,7 +299,6 @@ Unmatched prompts raise `Mana::MockError` with a helpful message suggesting the
|
|
|
345
299
|
numbers = [1, 2, 3]
|
|
346
300
|
~"average of <numbers>, ──→ system prompt:
|
|
347
301
|
store in <result>" - rules + tools
|
|
348
|
-
- memory (short/long-term)
|
|
349
302
|
- variables: numbers = [1,2,3]
|
|
350
303
|
- available functions
|
|
351
304
|
|
|
@@ -365,14 +318,12 @@ Unmatched prompts raise `Mana::MockError` with a helpful message suggesting the
|
|
|
365
318
|
|
|
366
319
|
2. **Build context** — parses `<var>` references from the prompt, reads their current values, discovers available functions via Prism AST (with YARD descriptions if present).
|
|
367
320
|
|
|
368
|
-
3. **Build system prompt** — assembles rules,
|
|
321
|
+
3. **Build system prompt** — assembles rules, variable values, and function signatures into a single system prompt.
|
|
369
322
|
|
|
370
|
-
4. **LLM tool-calling loop** — sends prompt to the LLM with built-in tools (`read_var`, `write_var`, `read_attr`, `write_attr`, `call_func`, `done`, `error`, `eval`, `think
|
|
323
|
+
4. **LLM tool-calling loop** — sends prompt to the LLM with built-in tools (`read_var`, `write_var`, `read_attr`, `write_attr`, `call_func`, `done`, `error`, `eval`, `think`). The LLM responds with tool calls, Mana executes them against the live Ruby binding, and sends results back. This loops until `done` is called or no more tool calls are returned.
|
|
371
324
|
|
|
372
325
|
5. **Return value** — single `write_var` returns the value directly; multiple writes return a Hash. On Ruby 4.0+, a singleton method fallback ensures variables are accessible in the caller's scope.
|
|
373
326
|
|
|
374
|
-
6. **Background compaction** — if short-term memory exceeds the token pressure threshold, old messages are summarized by the LLM in a background thread and replaced with a compact summary.
|
|
375
|
-
|
|
376
327
|
|
|
377
328
|
## License
|
|
378
329
|
|
data/lib/mana/config.rb
CHANGED
|
@@ -13,9 +13,14 @@ module Mana
|
|
|
13
13
|
attr_accessor :model, :temperature, :api_key, :max_iterations, :base_url,
|
|
14
14
|
:backend, :verbose,
|
|
15
15
|
:namespace, :memory_store, :memory_path,
|
|
16
|
-
:context_window
|
|
16
|
+
:context_window,
|
|
17
|
+
:context_class, :knowledge_provider
|
|
17
18
|
attr_reader :timeout
|
|
18
19
|
|
|
20
|
+
# Backward compatibility aliases
|
|
21
|
+
alias_method :memory_class, :context_class
|
|
22
|
+
alias_method :memory_class=, :context_class=
|
|
23
|
+
|
|
19
24
|
DEFAULT_ANTHROPIC_URL = "https://api.anthropic.com"
|
|
20
25
|
DEFAULT_OPENAI_URL = "https://api.openai.com"
|
|
21
26
|
|
|
@@ -36,6 +41,8 @@ module Mana
|
|
|
36
41
|
@memory_store = nil
|
|
37
42
|
@memory_path = nil
|
|
38
43
|
@context_window = 128_000
|
|
44
|
+
@context_class = nil # nil = use Mana::Context; set to custom class
|
|
45
|
+
@knowledge_provider = nil # nil = use Mana::Knowledge; set to custom module with .query(topic)
|
|
39
46
|
end
|
|
40
47
|
|
|
41
48
|
# Set timeout; must be a positive number
|
data/lib/mana/context.rb
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Mana
|
|
4
|
+
class Context
|
|
5
|
+
attr_reader :messages, :summaries
|
|
6
|
+
|
|
7
|
+
# Initialize with empty conversation context
|
|
8
|
+
def initialize
|
|
9
|
+
@messages = []
|
|
10
|
+
@summaries = []
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
# --- Class methods ---
|
|
14
|
+
|
|
15
|
+
class << self
|
|
16
|
+
# Return the current thread's context instance (lazy-initialized).
|
|
17
|
+
# Uses config.context_class if set, otherwise Mana::Context.
|
|
18
|
+
def current
|
|
19
|
+
Thread.current[:mana_context] ||= begin
|
|
20
|
+
klass = Mana.config.context_class || self
|
|
21
|
+
klass.new
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# --- Token estimation ---
|
|
27
|
+
|
|
28
|
+
# Estimate total token count across short-term messages and summaries.
|
|
29
|
+
def token_count
|
|
30
|
+
count = 0
|
|
31
|
+
@messages.each do |msg|
|
|
32
|
+
content = msg[:content]
|
|
33
|
+
case content
|
|
34
|
+
when String
|
|
35
|
+
# Plain text message
|
|
36
|
+
count += estimate_tokens(content)
|
|
37
|
+
when Array
|
|
38
|
+
# Array of content blocks (tool_use, tool_result, text)
|
|
39
|
+
content.each do |block|
|
|
40
|
+
count += estimate_tokens(block[:text] || block[:content] || "")
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
@summaries.each { |s| count += estimate_tokens(s) }
|
|
45
|
+
count
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Rough token estimate: ~4 characters per token
|
|
49
|
+
def estimate_tokens(text)
|
|
50
|
+
return 0 unless text.is_a?(String)
|
|
51
|
+
|
|
52
|
+
(text.length / 4.0).ceil
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# --- Context management ---
|
|
56
|
+
|
|
57
|
+
# Clear conversation history and summaries
|
|
58
|
+
def clear!
|
|
59
|
+
clear_messages!
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Clear conversation history and compaction summaries
|
|
63
|
+
def clear_messages!
|
|
64
|
+
@messages.clear
|
|
65
|
+
@summaries.clear
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# --- Display ---
|
|
69
|
+
|
|
70
|
+
# Human-readable summary: counts and token usage
|
|
71
|
+
def inspect
|
|
72
|
+
"#<Mana::Context messages=#{messages_rounds} rounds, tokens=#{token_count}/#{context_window}>"
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
private
|
|
76
|
+
|
|
77
|
+
# Count conversation rounds (user-prompt messages only, not tool results)
|
|
78
|
+
def messages_rounds
|
|
79
|
+
@messages.count { |m| m[:role] == "user" && m[:content].is_a?(String) }
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def context_window
|
|
83
|
+
Mana.config.context_window
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
data/lib/mana/engine.rb
CHANGED
|
@@ -96,7 +96,7 @@ module Mana
|
|
|
96
96
|
},
|
|
97
97
|
{
|
|
98
98
|
name: "eval",
|
|
99
|
-
description: "
|
|
99
|
+
description: "Define new methods, classes, or require libraries — use this to create new things in the runtime. For reading/writing variables and calling existing functions, use the other tools.",
|
|
100
100
|
input_schema: {
|
|
101
101
|
type: "object",
|
|
102
102
|
properties: {
|
|
@@ -118,18 +118,6 @@ module Mana
|
|
|
118
118
|
}
|
|
119
119
|
].freeze
|
|
120
120
|
|
|
121
|
-
# Separated from TOOLS because it's conditionally excluded in incognito mode
|
|
122
|
-
REMEMBER_TOOL = {
|
|
123
|
-
name: "remember",
|
|
124
|
-
description: "Store a fact in long-term memory. This memory persists across script executions. Use when the user explicitly asks to remember something.",
|
|
125
|
-
input_schema: {
|
|
126
|
-
type: "object",
|
|
127
|
-
properties: { content: { type: "string", description: "The fact to remember" } },
|
|
128
|
-
required: ["content"]
|
|
129
|
-
}
|
|
130
|
-
}.freeze
|
|
131
|
-
|
|
132
|
-
|
|
133
121
|
class << self
|
|
134
122
|
# Entry point for ~"..." prompts. Routes to mock handler or real LLM engine.
|
|
135
123
|
def run(prompt, caller_binding)
|
|
@@ -141,53 +129,47 @@ module Mana
|
|
|
141
129
|
new(caller_binding).execute(prompt)
|
|
142
130
|
end
|
|
143
131
|
|
|
144
|
-
# Built-in tools + remember
|
|
132
|
+
# Built-in tools + registered tools (e.g. remember from claw)
|
|
145
133
|
def all_tools
|
|
146
|
-
|
|
147
|
-
tools << REMEMBER_TOOL unless Memory.incognito?
|
|
148
|
-
tools
|
|
134
|
+
TOOLS.dup + Mana.registered_tools
|
|
149
135
|
end
|
|
150
136
|
|
|
151
|
-
# Query the runtime knowledge base
|
|
137
|
+
# Query the runtime knowledge base.
|
|
138
|
+
# Uses config.knowledge_provider if set, otherwise Mana::Knowledge.
|
|
152
139
|
def knowledge(topic)
|
|
153
|
-
Mana::Knowledge
|
|
140
|
+
provider = Mana.config.knowledge_provider || Mana::Knowledge
|
|
141
|
+
provider.query(topic)
|
|
154
142
|
end
|
|
155
143
|
end
|
|
156
144
|
|
|
157
|
-
# Capture the caller's binding, config, source path
|
|
145
|
+
# Capture the caller's binding, config, and source path
|
|
158
146
|
def initialize(caller_binding, config = Mana.config)
|
|
159
147
|
@binding = caller_binding
|
|
160
148
|
@config = config
|
|
161
149
|
@caller_path = caller_source_path
|
|
162
|
-
@incognito = Memory.incognito?
|
|
163
150
|
end
|
|
164
151
|
|
|
165
152
|
# Main execution loop: build context, call LLM, handle tool calls, iterate until done.
|
|
166
153
|
# Optional &on_text block receives streaming text deltas for real-time display.
|
|
167
154
|
def execute(prompt, &on_text)
|
|
168
|
-
# Track nesting depth to isolate
|
|
155
|
+
# Track nesting depth to isolate context for nested ~"..." calls
|
|
169
156
|
Thread.current[:mana_depth] ||= 0
|
|
170
157
|
Thread.current[:mana_depth] += 1
|
|
171
158
|
nested = Thread.current[:mana_depth] > 1
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
# Nested calls get fresh short-term
|
|
175
|
-
if nested
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
long_term = outer_memory&.long_term || []
|
|
179
|
-
inner_memory.instance_variable_set(:@long_term, long_term)
|
|
180
|
-
inner_memory.instance_variable_set(:@next_id, (long_term.map { |m| m[:id] }.max || 0) + 1)
|
|
181
|
-
Thread.current[:mana_memory] = inner_memory
|
|
159
|
+
outer_context = nil # defined here so ensure block always has access
|
|
160
|
+
|
|
161
|
+
# Nested calls get fresh short-term context
|
|
162
|
+
if nested
|
|
163
|
+
outer_context = Thread.current[:mana_context]
|
|
164
|
+
Thread.current[:mana_context] = Mana::Context.new
|
|
182
165
|
end
|
|
183
166
|
|
|
184
167
|
# Extract <var> references from the prompt and read their current values
|
|
185
168
|
context = build_context(prompt)
|
|
186
169
|
system_prompt = build_system_prompt(context)
|
|
187
170
|
|
|
188
|
-
memory =
|
|
189
|
-
|
|
190
|
-
messages = memory ? memory.short_term : []
|
|
171
|
+
memory = Context.current
|
|
172
|
+
messages = memory.messages
|
|
191
173
|
|
|
192
174
|
# Strip trailing unpaired tool_use messages from prior calls.
|
|
193
175
|
# Both Anthropic and OpenAI reject requests where the last assistant message
|
|
@@ -251,7 +233,7 @@ module Mana
|
|
|
251
233
|
on_text.call(:tool_start, tu[:name], tu[:input])
|
|
252
234
|
end
|
|
253
235
|
end
|
|
254
|
-
result = handle_effect(tu
|
|
236
|
+
result = handle_effect(tu)
|
|
255
237
|
if on_text && !%w[done error].include?(tu[:name])
|
|
256
238
|
on_text.call(:tool_end, tu[:name], result)
|
|
257
239
|
end
|
|
@@ -266,7 +248,7 @@ module Mana
|
|
|
266
248
|
end
|
|
267
249
|
|
|
268
250
|
# Append a final assistant summary so LLM has full context next call
|
|
269
|
-
if
|
|
251
|
+
if done_result
|
|
270
252
|
messages << { role: "assistant", content: [{ type: "text", text: "Done: #{done_result}" }] }
|
|
271
253
|
end
|
|
272
254
|
|
|
@@ -284,14 +266,14 @@ module Mana
|
|
|
284
266
|
rescue => e
|
|
285
267
|
# Rollback: remove messages added during this failed call so they don't
|
|
286
268
|
# pollute short-term memory for subsequent prompts
|
|
287
|
-
if
|
|
269
|
+
if messages.size > messages_start_size
|
|
288
270
|
messages.slice!(messages_start_size..)
|
|
289
271
|
end
|
|
290
272
|
raise e
|
|
291
273
|
ensure
|
|
292
|
-
# Restore outer
|
|
293
|
-
if nested
|
|
294
|
-
Thread.current[:
|
|
274
|
+
# Restore outer context when exiting a nested call
|
|
275
|
+
if nested
|
|
276
|
+
Thread.current[:mana_context] = outer_context
|
|
295
277
|
end
|
|
296
278
|
Thread.current[:mana_depth] -= 1 if Thread.current[:mana_depth]
|
|
297
279
|
end
|
|
@@ -322,13 +304,11 @@ module Mana
|
|
|
322
304
|
write_local(name.to_s, value)
|
|
323
305
|
end
|
|
324
306
|
|
|
325
|
-
# Record in
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
memory.short_term << { role: "assistant", content: [{ type: "text", text: "Done: #{return_value || values.inspect}" }] }
|
|
331
|
-
end
|
|
307
|
+
# Record in context
|
|
308
|
+
memory = Context.current
|
|
309
|
+
if memory
|
|
310
|
+
memory.messages << { role: "user", content: prompt }
|
|
311
|
+
memory.messages << { role: "assistant", content: [{ type: "text", text: "Done: #{return_value || values.inspect}" }] }
|
|
332
312
|
end
|
|
333
313
|
|
|
334
314
|
# Return _return value if set, otherwise the first written value
|
data/lib/mana/knowledge.rb
CHANGED
|
@@ -119,25 +119,13 @@ module Mana
|
|
|
119
119
|
end
|
|
120
120
|
|
|
121
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
122
|
<<~TEXT
|
|
130
|
-
ruby-mana
|
|
131
|
-
- Short-term
|
|
123
|
+
ruby-mana manages conversation context via Mana::Context:
|
|
124
|
+
- Short-term context: conversation history within the current process. Each ~"..."
|
|
132
125
|
call appends to it, so consecutive calls share context. Cleared when the process exits.
|
|
133
|
-
-
|
|
134
|
-
|
|
135
|
-
|
|
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
|
-
- Incognito mode: Mana.incognito { ~"..." } disables all memory.
|
|
140
|
-
The LLM can store facts via the `remember` tool. These persist across script executions.
|
|
126
|
+
- Summaries: compacted conversation summaries from prior rounds.
|
|
127
|
+
Long-term memory and the `remember` tool are provided by agent frameworks (e.g. ruby-claw)
|
|
128
|
+
via Mana's tool registration interface.
|
|
141
129
|
TEXT
|
|
142
130
|
end
|
|
143
131
|
|
data/lib/mana/memory.rb
CHANGED
|
@@ -17,11 +17,15 @@ module Mana
|
|
|
17
17
|
|
|
18
18
|
class << self
|
|
19
19
|
# Return the current thread's memory instance (lazy-initialized).
|
|
20
|
+
# Uses config.memory_class if set (e.g. Claw::Memory), otherwise Mana::Memory.
|
|
20
21
|
# Returns nil in incognito mode.
|
|
21
22
|
def current
|
|
22
23
|
return nil if incognito?
|
|
23
24
|
|
|
24
|
-
Thread.current[:mana_memory] ||=
|
|
25
|
+
Thread.current[:mana_memory] ||= begin
|
|
26
|
+
klass = Mana.config.memory_class || self
|
|
27
|
+
klass.new
|
|
28
|
+
end
|
|
25
29
|
end
|
|
26
30
|
|
|
27
31
|
# Check if the current thread is in incognito mode (no memory)
|
data/lib/mana/prompt_builder.rb
CHANGED
|
@@ -34,37 +34,27 @@ module Mana
|
|
|
34
34
|
"- done(result: ...) to return a value. error(message: ...) only after you have tried and failed.",
|
|
35
35
|
"- <var> references point to variables in scope; create with write_var if missing.",
|
|
36
36
|
"- Match types precisely: numbers for numeric values, arrays for lists, strings for text.",
|
|
37
|
+
"- eval to define new methods, new classes, or require libraries. For operating on existing variables and functions, use read_var/write_var/call_func.",
|
|
37
38
|
"- Current prompt overrides conversation history and memories.",
|
|
38
39
|
]
|
|
39
40
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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
|
|
41
|
+
mana_ctx = Context.current
|
|
42
|
+
# Inject context when available
|
|
43
|
+
if mana_ctx
|
|
44
|
+
# Add compaction summaries from prior conversations
|
|
45
|
+
unless mana_ctx.summaries.empty?
|
|
46
|
+
parts << ""
|
|
47
|
+
parts << "Previous conversation summary:"
|
|
48
|
+
mana_ctx.summaries.each { |s| parts << " #{s}" }
|
|
65
49
|
end
|
|
66
50
|
end
|
|
67
51
|
|
|
52
|
+
# Inject registered prompt sections (e.g. long-term memories from claw)
|
|
53
|
+
Mana.prompt_sections.each do |section_block|
|
|
54
|
+
text = section_block.call
|
|
55
|
+
parts << "" << text if text && !text.empty?
|
|
56
|
+
end
|
|
57
|
+
|
|
68
58
|
# Inject current variable values referenced in the prompt
|
|
69
59
|
unless context.empty?
|
|
70
60
|
parts << ""
|
|
@@ -135,7 +125,7 @@ module Mana
|
|
|
135
125
|
|
|
136
126
|
# User-defined classes/modules (skip Ruby internals)
|
|
137
127
|
skip = [Object, Kernel, BasicObject, Module, Class, Mana, Mana::Engine,
|
|
138
|
-
Mana::Memory, Mana::Config]
|
|
128
|
+
Mana::Memory, Mana::Context, Mana::Config]
|
|
139
129
|
user_classes = ObjectSpace.each_object(Class)
|
|
140
130
|
.reject { |c| c.name.nil? || c.name.start_with?("Mana::") || c.name.start_with?("#<") }
|
|
141
131
|
.reject { |c| skip.include?(c) }
|
data/lib/mana/tool_handler.rb
CHANGED
|
@@ -3,82 +3,27 @@
|
|
|
3
3
|
module Mana
|
|
4
4
|
# Dispatches LLM tool calls to their respective handlers.
|
|
5
5
|
# Mixed into Engine as a private method.
|
|
6
|
+
#
|
|
7
|
+
# Built-in tools are dispatched via instance methods (handle_<name>),
|
|
8
|
+
# which can access @binding, @written_vars, etc.
|
|
9
|
+
# External tools (registered via Mana.register_tool) are dispatched via Procs
|
|
10
|
+
# that only receive input — they cannot access the engine's binding.
|
|
6
11
|
module ToolHandler
|
|
12
|
+
BUILTIN_TOOLS = %w[read_var write_var read_attr write_attr call_func eval knowledge done error].freeze
|
|
13
|
+
|
|
7
14
|
private
|
|
8
15
|
|
|
9
16
|
# Dispatch a single tool call from the LLM.
|
|
10
|
-
def handle_effect(tool_use
|
|
17
|
+
def handle_effect(tool_use)
|
|
11
18
|
name = tool_use[:name]
|
|
12
19
|
input = tool_use[:input] || {}
|
|
13
20
|
# Normalize keys to strings for consistent access
|
|
14
21
|
input = input.transform_keys(&:to_s) if input.is_a?(Hash)
|
|
15
22
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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
|
-
|
|
23
|
+
if BUILTIN_TOOLS.include?(name)
|
|
24
|
+
send("handle_#{name}", input)
|
|
25
|
+
elsif (handler = Mana.tool_handlers[name])
|
|
26
|
+
handler.call(input)
|
|
82
27
|
else
|
|
83
28
|
"error: unknown tool #{name}"
|
|
84
29
|
end
|
|
@@ -91,6 +36,62 @@ module Mana
|
|
|
91
36
|
"error: #{e.class}: #{e.message}"
|
|
92
37
|
end
|
|
93
38
|
|
|
39
|
+
# --- Built-in tool handlers ---
|
|
40
|
+
|
|
41
|
+
def handle_read_var(input)
|
|
42
|
+
val = serialize_value(resolve(input["name"]))
|
|
43
|
+
vlog_value(" ↩ #{input['name']} =", val)
|
|
44
|
+
val
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def handle_write_var(input)
|
|
48
|
+
var_name = input["name"]
|
|
49
|
+
value = input["value"]
|
|
50
|
+
write_local(var_name, value)
|
|
51
|
+
@written_vars[var_name] = value
|
|
52
|
+
vlog_value(" ✅ #{var_name} =", value)
|
|
53
|
+
"ok: #{var_name} = #{value.inspect}"
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def handle_read_attr(input)
|
|
57
|
+
obj = resolve(input["obj"])
|
|
58
|
+
validate_name!(input["attr"])
|
|
59
|
+
serialize_value(obj.public_send(input["attr"]))
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def handle_write_attr(input)
|
|
63
|
+
obj = resolve(input["obj"])
|
|
64
|
+
validate_name!(input["attr"])
|
|
65
|
+
obj.public_send("#{input['attr']}=", input["value"])
|
|
66
|
+
"ok: #{input['obj']}.#{input['attr']} = #{input['value'].inspect}"
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def handle_knowledge(input)
|
|
70
|
+
self.class.knowledge(input["topic"])
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def handle_done(input)
|
|
74
|
+
done_val = input["result"]
|
|
75
|
+
vlog_value("🏁 Done:", done_val)
|
|
76
|
+
vlog("═" * 60)
|
|
77
|
+
input["result"].to_s
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def handle_error(input)
|
|
81
|
+
msg = input["message"] || "LLM reported an error"
|
|
82
|
+
vlog("❌ Error: #{msg}")
|
|
83
|
+
vlog("═" * 60)
|
|
84
|
+
raise Mana::LLMError, msg
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def handle_eval(input)
|
|
88
|
+
result = @binding.eval(input["code"])
|
|
89
|
+
vlog_value(" ↩ eval →", result)
|
|
90
|
+
serialize_value(result)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# --- call_func and helpers ---
|
|
94
|
+
|
|
94
95
|
# Handle call_func tool: chained calls, block bodies, simple calls
|
|
95
96
|
def handle_call_func(input)
|
|
96
97
|
func = input["name"]
|
data/lib/mana/version.rb
CHANGED
data/lib/mana.rb
CHANGED
|
@@ -6,7 +6,8 @@ require_relative "mana/backends/base"
|
|
|
6
6
|
require_relative "mana/backends/anthropic"
|
|
7
7
|
require_relative "mana/backends/openai"
|
|
8
8
|
require_relative "mana/memory_store"
|
|
9
|
-
require_relative "mana/memory"
|
|
9
|
+
require_relative "mana/memory" # kept for backward compatibility until claw migrates
|
|
10
|
+
require_relative "mana/context"
|
|
10
11
|
require_relative "mana/logger"
|
|
11
12
|
require_relative "mana/knowledge"
|
|
12
13
|
require_relative "mana/binding_helpers"
|
|
@@ -42,21 +43,62 @@ module Mana
|
|
|
42
43
|
config.model = model
|
|
43
44
|
end
|
|
44
45
|
|
|
45
|
-
# Reset all global state: config, thread-local
|
|
46
|
+
# Reset all global state: config, thread-local context and mock
|
|
46
47
|
def reset!
|
|
47
48
|
@config = Config.new
|
|
48
|
-
Thread.current[:
|
|
49
|
+
Thread.current[:mana_context] = nil
|
|
50
|
+
Thread.current[:mana_memory] = nil # backward compat for claw transition
|
|
49
51
|
Thread.current[:mana_mock] = nil
|
|
52
|
+
clear_tools!
|
|
50
53
|
end
|
|
51
54
|
|
|
52
|
-
# Access current thread's memory
|
|
55
|
+
# Access current thread's context (public API kept as `memory` for backward compat)
|
|
53
56
|
def memory
|
|
54
|
-
|
|
57
|
+
Context.current
|
|
55
58
|
end
|
|
56
59
|
|
|
57
|
-
#
|
|
58
|
-
|
|
59
|
-
|
|
60
|
+
# --- Tool registration ---
|
|
61
|
+
|
|
62
|
+
# Register an external tool definition with its handler block.
|
|
63
|
+
# tool_definition is a hash with :name, :description, :input_schema.
|
|
64
|
+
# The handler block receives (input) and returns a result string.
|
|
65
|
+
def register_tool(tool_definition, &handler)
|
|
66
|
+
@registered_tools ||= []
|
|
67
|
+
@tool_handlers ||= {}
|
|
68
|
+
@registered_tools << tool_definition
|
|
69
|
+
@tool_handlers[tool_definition[:name]] = handler
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Return a copy of the registered tool definitions
|
|
73
|
+
def registered_tools
|
|
74
|
+
@registered_tools ||= []
|
|
75
|
+
@registered_tools.dup
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Return the name → handler mapping for registered tools
|
|
79
|
+
def tool_handlers
|
|
80
|
+
@tool_handlers ||= {}
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Clear all registered tools, handlers, and prompt sections
|
|
84
|
+
def clear_tools!
|
|
85
|
+
@registered_tools = []
|
|
86
|
+
@tool_handlers = {}
|
|
87
|
+
@prompt_sections = []
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# --- Prompt section registration ---
|
|
91
|
+
|
|
92
|
+
# Register a block that returns text to inject into the system prompt.
|
|
93
|
+
# The block is called each time a prompt is built. Return nil or "" to skip.
|
|
94
|
+
def register_prompt_section(&block)
|
|
95
|
+
@prompt_sections ||= []
|
|
96
|
+
@prompt_sections << block
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# Return the list of registered prompt section blocks
|
|
100
|
+
def prompt_sections
|
|
101
|
+
@prompt_sections ||= []
|
|
60
102
|
end
|
|
61
103
|
|
|
62
104
|
# View generated source for a mana-compiled method
|
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.5.
|
|
4
|
+
version: 0.5.12
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Carl Li
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-
|
|
11
|
+
date: 2026-04-05 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: binding_of_caller
|
|
@@ -57,6 +57,7 @@ files:
|
|
|
57
57
|
- lib/mana/binding_helpers.rb
|
|
58
58
|
- lib/mana/compiler.rb
|
|
59
59
|
- lib/mana/config.rb
|
|
60
|
+
- lib/mana/context.rb
|
|
60
61
|
- lib/mana/engine.rb
|
|
61
62
|
- lib/mana/introspect.rb
|
|
62
63
|
- lib/mana/knowledge.rb
|